import React, { useCallback, useEffect, useMemo, useState } from "react";
import { createRoot } from "react-dom/client";
import {
  Activity,
  Archive,
  BadgeCheck,
  BarChart3,
  BellRing,
  BriefcaseBusiness,
  CheckCircle2,
  ChevronRight,
  CircleGauge,
  Clock3,
  Database,
  Download,
  ExternalLink,
  FileText,
  Filter,
  Gauge,
  GitBranch,
  HardDrive,
  Home,
  Inbox,
  KeyRound,
  KanbanSquare,
  Layers3,
  Loader2,
  LockKeyhole,
  LogOut,
  Mail,
  MessageSquareText,
  Plus,
  RefreshCw,
  Save,
  Search,
  Send,
  Server,
  Settings,
  ShieldCheck,
  SlidersHorizontal,
  Sparkles,
  Square,
  SquareCheckBig,
  Tags,
  TimerReset,
  UserRound,
  Webhook,
  Wifi,
  XCircle
} from "lucide-react";
import "./styles.css";

type FeedbackEvent = {
  id: number;
  feedbackCode: string;
  eventType: string;
  actor: string;
  fromValue: string;
  toValue: string;
  message: string;
  createdAt: string;
};

type FeedbackComment = {
  id: number;
  feedbackCode: string;
  author: string;
  body: string;
  internal: boolean;
  createdAt: string;
};

type FeedbackRecord = {
  code: string;
  receivedAt: string;
  title: string;
  type: string;
  severity: string;
  category: string;
  priority: string;
  contact: string;
  body: string;
  status: string;
  statusDetail: string;
  note: string;
  publicReply: string;
  handledBy: string;
  assignee: string;
  dueAt: string;
  resolvedAt: string;
  archivedAt: string;
  slaLevel: string;
  sourceChannel: string;
  riskScore: number;
  resolution: string;
  packagePath: string;
  encryptedPackagePath: string;
  packageSha256: string;
  plainPackageSha256: string;
  remoteAddr: string;
  summaryText: string;
  includedFiles: string;
  mailSent: boolean;
  updatedAt: string;
  lastActivityAt: string;
  tags?: string[];
  events?: FeedbackEvent[];
  comments?: FeedbackComment[];
};

type MailRecord = {
  id: number;
  feedbackCode: string;
  kind: string;
  status: string;
  toAddress: string;
  subject: string;
  plainBody: string;
  htmlBody: string;
  attachmentPath: string;
  attachmentName: string;
  errorMessage: string;
  createdAt: string;
  sentAt: string;
};

type WebhookDelivery = {
  id: number;
  webhookName: string;
  event: string;
  status: string;
  attempts: number;
  responseCode: number;
  errorMessage: string;
  payloadSha256: string;
  createdAt: string;
  finishedAt: string;
};

type AuditLog = {
  id: number;
  actor: string;
  type: string;
  target: string;
  message: string;
  ip: string;
  userAgent: string;
  createdAt: string;
};

type BackupInfo = { name: string; bytes: number; createdAt: string };
type WebhookSummary = { name: string; host: string; enabled: boolean; events: string[]; secretConfigured: boolean; timeoutSeconds: number; maxRetries: number };
type ConfigCheck = { key: string; label: string; status: "ok" | "missing" | "warning"; detail: string };

type Overview = {
  feedbackTotal: number;
  todayFeedback: number;
  mailFailed: number;
  mailTotal: number;
  overdue: number;
  statusCounts: Record<string, number>;
  categoryCounts: Record<string, number>;
  priorityCounts: Record<string, number>;
  slaCounts: Record<string, number>;
  storage: { path: string; bytes: number };
  database: { path: string; bytes: number; walMode: string };
  mail: { configured: boolean; host: string; port: number; secure: string; fromAddress: string; developerAddress: string };
  recentEvents: FeedbackEvent[];
};

type FeedbackSummary = {
  total: number;
  today: number;
  mailFailed: number;
  overdue: number;
  statusCounts: Record<string, number>;
  categoryCounts: Record<string, number>;
  priorityCounts: Record<string, number>;
  slaCounts: Record<string, number>;
  recentEvents: FeedbackEvent[];
};

type ConfigHealth = {
  generatedAt: string;
  service: { listen: string; baseDir: string; timestampWindowSeconds: number; maxRequestBytes: number; maxPackageBytes: number };
  security: {
    adminPasswordConfigured: boolean;
    adminPasswordHashConfigured: boolean;
    clientSignatureKeyConfigured: boolean;
    packageEncryptionKeyConfigured: boolean;
    rateLimit: Record<string, number>;
  };
  uploadGuard: Record<string, number | boolean>;
  storage: { path: string; bytes: number; exists: boolean };
  database: { provider: string; path: string; bytes: number; exists: boolean; walMode: string; backupDir: string; lastBackup: string; runtime?: DatabaseRuntime };
  mail: {
    configured: boolean;
    host: string;
    port: number;
    secure: string;
    fromAddress: string;
    developerAddress: string;
    usernameConfigured: boolean;
    passwordConfigured: boolean;
    timeoutSeconds: number;
  };
  webhooks: WebhookSummary[];
  frontend: { distPath: string; indexExists: boolean; assetsExists: boolean };
  editable: EditableConfig;
  checks: ConfigCheck[];
};

type DatabaseRuntime = {
  activeProvider: string;
  configProvider: string;
  sqliteReady: boolean;
  remoteReady: boolean;
  failoverActive: boolean;
  lastError: string;
  lastFailoverAt: string;
  lastRecoveredAt: string;
  lastSyncAt: string;
  lastSyncError: string;
};

type EditableDatabase = {
  provider: string;
  sqlitePath: string;
  host: string;
  port: number;
  name: string;
  user: string;
  password: string;
  passwordConfigured: boolean;
  dsn: string;
  dsnConfigured: boolean;
  sslMode: string;
  maxOpenConns: number;
  maxIdleConns: number;
  connMaxLifetimeSeconds: number;
  failoverEnabled: boolean;
  healthIntervalSeconds: number;
  sync: { enabled: boolean; interval_seconds?: number; intervalSeconds?: number; batch_size?: number; batchSize?: number };
};

type EditableWebhook = WebhookSummary & { url?: string; secret?: string; timeoutSeconds: number; maxRetries: number };

type EditableConfig = {
  listen: string;
  database: EditableDatabase;
  mail: {
    host: string; port: number; secure: string; username: string; password: string; passwordConfigured: boolean;
    fromAddress: string; fromName: string; developerAddress: string; timeoutSeconds: number;
  };
  backup: { dir: string };
  rateLimit: Record<string, number>;
  uploadGuard: Record<string, number | boolean>;
  webhooks: EditableWebhook[];
  security: Record<string, boolean>;
};

type StatusPayload = {
  ok: boolean;
  code?: string;
  status?: string;
  statusLabel?: string;
  statusDetail?: string;
  category?: string;
  priority?: string;
  hasReply?: boolean;
  reply?: string;
  receivedAt?: string;
  updatedAt?: string;
  mailSent?: boolean;
  error?: string;
  message?: string;
};

type Page<T> = { items: T[]; total: number; page: number; perPage: number; totalPages: number; offset: number };
type Captcha = { captchaId: string; image: string };
type ApiResult<T> = T & { ok: boolean; error?: string; message?: string };
type Filters = {
  status: string;
  category: string;
  priority: string;
  mail: string;
  q: string;
  assignee: string;
  tag: string;
  sla: string;
  overdue: string;
  sort: string;
  page: number;
  perPage: number;
};
type AdminTab = "dashboard" | "tickets" | "board" | "mail" | "integrations" | "ops" | "config";

const refreshMs = 10000;
const statusLabels: Record<string, string> = { new: "新反馈", triaged: "已归类", investigating: "处理中", resolved: "已解决", archived: "已归档" };
const categoryLabels: Record<string, string> = { issue: "问题", suggestion: "建议", ui: "界面反馈", other: "其他" };
const priorityLabels: Record<string, string> = { normal: "普通", major: "影响使用", blocking: "阻塞" };
const slaLabels: Record<string, string> = { standard: "标准", elevated: "加急", urgent: "紧急" };
const statusOrder = ["new", "triaged", "investigating", "resolved", "archived"];
const statusColors: Record<string, string> = { new: "#0f6d7a", triaged: "#2563a8", investigating: "#a46805", resolved: "#16865a", archived: "#667486" };

function App() {
  const path = window.location.pathname;
  if (!path.startsWith("/admin")) {
    return <PublicHome />;
  }

  const [csrfToken, setCsrfToken] = useState(() => sessionStorage.getItem("csrfToken") ?? "");
  const [notice, setNotice] = useState("");
  const api = useMemo(() => createApi(csrfToken, setNotice), [csrfToken]);

  if (!csrfToken) {
    return <LoginPage onLogin={(token) => { sessionStorage.setItem("csrfToken", token); setCsrfToken(token); }} />;
  }

  return <AdminApp api={api} notice={notice} onClearNotice={() => setNotice("")} onLogout={() => { sessionStorage.removeItem("csrfToken"); setCsrfToken(""); }} />;
}

function PublicHome() {
  const [query, setQuery] = useState("");
  const [status, setStatus] = useState<StatusPayload | null>(null);
  const [busy, setBusy] = useState(false);
  const [lastChecked, setLastChecked] = useState("");

  const lookup = useCallback(async (silent = false) => {
    const code = query.trim().toUpperCase();
    if (!code) return;
    if (!silent) setBusy(true);
    try {
      const response = await fetch(`/?api=status&code=${encodeURIComponent(code)}`, { credentials: "include" });
      const data = (await response.json()) as StatusPayload;
      setStatus(data);
      setLastChecked(new Date().toLocaleTimeString());
    } finally {
      if (!silent) setBusy(false);
    }
  }, [query]);

  useEffect(() => {
    if (!status?.ok) return;
    const timer = window.setInterval(() => void lookup(true), 15000);
    return () => window.clearInterval(timer);
  }, [lookup, status?.ok]);

  return (
    <main className="public-shell">
      <header className="public-top">
        <div className="brand-mark"><ShieldCheck size={23} /><span>YMhut Box</span></div>
        <a className="ghost-button compact" href="/admin/"><LockKeyhole size={16} /> 后台入口</a>
      </header>
      <section className="public-hero reveal">
        <div className="public-copy">
          <span className="eyebrow"><Sparkles size={15} /> 反馈服务中心</span>
          <h1>反馈工单状态查询</h1>
          <p>输入反馈编号即可查看处理状态、公开回复、分类和优先级。后台侧已升级为工单流、看板、审计、备份和 Webhook 通知。</p>
          <div className="public-signals">
            <span><Wifi size={16} /> 服务在线</span>
            <span><Database size={16} /> SQLite WAL</span>
            <span><ShieldCheck size={16} /> 服务端验证码</span>
          </div>
        </div>
        <form className="status-lookup" onSubmit={(event) => { event.preventDefault(); void lookup(); }}>
          <label><span>反馈编号</span><div className="input-icon"><Search size={15} /><input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="FB-YYYYMMDD-XXXXXX" /></div></label>
          <button className="primary-button" disabled={busy} type="submit">{busy ? <Loader2 className="spin" size={18} /> : <Search size={18} />} 查询状态</button>
          {lastChecked ? <small className="muted">最近查询 {lastChecked}</small> : null}
        </form>
        <PublicStatusPanel status={status} />
      </section>
    </main>
  );
}

function PublicStatusPanel({ status }: { status: StatusPayload | null }) {
  if (!status) {
    return <section className="public-status empty-state">等待输入反馈编号</section>;
  }
  if (!status.ok) {
    return <section className="public-status error-state"><XCircle size={28} /><strong>{status.message || "未找到反馈工单"}</strong><span>{status.error || "NOT_FOUND"}</span></section>;
  }
  return (
    <section className="public-status">
      <div className="status-head">
        <StatusPill value={status.status || ""} />
        <strong>{status.code}</strong>
      </div>
      <div className="public-status-grid">
        <Info icon={<Layers3 size={15} />} label="分类" value={categoryLabels[status.category || ""] ?? status.category ?? "未分类"} />
        <Info icon={<Gauge size={15} />} label="优先级" value={priorityLabels[status.priority || ""] ?? status.priority ?? "普通"} />
        <Info icon={<Clock3 size={15} />} label="更新时间" value={formatDate(status.updatedAt || "") || "暂无"} />
        <Info icon={<Mail size={15} />} label="通知状态" value={status.mailSent ? "已通知" : "待通知"} />
      </div>
      <section className="report-block"><h3>状态说明</h3><pre>{status.statusDetail || status.statusLabel || "已收到反馈，正在排队处理。"}</pre></section>
      <section className="report-block"><h3>公开回复</h3><pre>{status.hasReply ? status.reply : "暂无公开回复"}</pre></section>
    </section>
  );
}

function LoginPage({ onLogin }: { onLogin: (token: string) => void }) {
  const [captcha, setCaptcha] = useState<Captcha | null>(null);
  const [password, setPassword] = useState("");
  const [captchaText, setCaptchaText] = useState("");
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");

  const loadCaptcha = useCallback(async () => {
    const response = await fetch("/api/auth/captcha", { credentials: "include" });
    const data = (await response.json()) as ApiResult<Captcha>;
    if (data.ok) {
      setCaptcha({ captchaId: data.captchaId, image: data.image });
      setCaptchaText("");
    }
  }, []);

  useEffect(() => { void loadCaptcha(); }, [loadCaptcha]);

  async function submit(event: React.FormEvent) {
    event.preventDefault();
    if (!captcha) return;
    setBusy(true);
    setError("");
    try {
      const response = await fetch("/api/auth/login", {
        method: "POST",
        credentials: "include",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ password, captchaId: captcha.captchaId, captcha: captchaText })
      });
      const data = (await response.json()) as ApiResult<{ csrfToken: string }>;
      if (!response.ok || !data.ok) {
        setError(data.message ?? "登录失败");
        await loadCaptcha();
        return;
      }
      onLogin(data.csrfToken);
    } finally {
      setBusy(false);
    }
  }

  return (
    <main className="login-screen">
      <section className="login-panel reveal">
        <div className="brand-mark"><ShieldCheck size={24} /><span>FeedbackOps</span></div>
        <h1>反馈工单后台</h1>
        <p>验证码由服务端生成并一次性校验。</p>
        <form onSubmit={submit} className="login-form">
          <label><span>后台密码</span><input value={password} onChange={(event) => setPassword(event.target.value)} type="password" autoComplete="current-password" required /></label>
          <label>
            <span>验证码</span>
            <div className="captcha-row">
              <input value={captchaText} onChange={(event) => setCaptchaText(event.target.value)} inputMode="numeric" required />
              <button type="button" className="captcha-button" onClick={() => void loadCaptcha()} title="刷新验证码" aria-label="刷新验证码">
                {captcha ? <img src={captcha.image} alt="验证码" /> : <Loader2 className="spin" />}
              </button>
            </div>
          </label>
          {error ? <p className="error-text">{error}</p> : null}
          <button className="primary-button" type="submit" disabled={busy}>{busy ? <Loader2 className="spin" size={18} /> : <ShieldCheck size={18} />} 登录</button>
        </form>
      </section>
    </main>
  );
}

function AdminApp({ api, notice, onClearNotice, onLogout }: { api: ReturnType<typeof createApi>; notice: string; onClearNotice: () => void; onLogout: () => void }) {
  const [tab, setTab] = useState<AdminTab>("dashboard");
  const [overview, setOverview] = useState<Overview | null>(null);
  const [summary, setSummary] = useState<FeedbackSummary | null>(null);
  const [page, setPage] = useState<Page<FeedbackRecord> | null>(null);
  const [mails, setMails] = useState<Page<MailRecord> | null>(null);
  const [config, setConfig] = useState<ConfigHealth | null>(null);
  const [auditLogs, setAuditLogs] = useState<Page<AuditLog> | null>(null);
  const [webhookDeliveries, setWebhookDeliveries] = useState<Page<WebhookDelivery> | null>(null);
  const [backups, setBackups] = useState<BackupInfo[]>([]);
  const [filters, setFilters] = useState<Filters>({ status: "", category: "", priority: "", mail: "", q: "", assignee: "", tag: "", sla: "", overdue: "", sort: "last", page: 1, perPage: 50 });
  const [selectedCode, setSelectedCode] = useState("");
  const [detail, setDetail] = useState<FeedbackRecord | null>(null);
  const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
  const [loading, setLoading] = useState(false);
  const [refreshing, setRefreshing] = useState(false);
  const [lastRefresh, setLastRefresh] = useState("");

  const loadOverview = useCallback(async () => {
    const data = await api.get<{ overview: Overview }>("/api/admin/overview");
    if (data?.overview) setOverview(data.overview);
  }, [api]);
  const loadSummary = useCallback(async () => {
    const data = await api.get<{ summary: FeedbackSummary }>("/api/admin/feedbacks/summary");
    if (data?.summary) setSummary(data.summary);
  }, [api]);
  const loadConfig = useCallback(async () => {
    const data = await api.get<{ config: ConfigHealth }>("/api/admin/config");
    if (data?.config) setConfig(data.config);
  }, [api]);
  const loadTickets = useCallback(async (silent = false) => {
    if (!silent) setLoading(true);
    try {
      const query = filterQuery(filters);
      const data = await api.get<{ page: Page<FeedbackRecord> }>(`/api/admin/feedbacks?${query}`);
      if (data?.page) {
        setPage(data.page);
        const first = data.page.items[0]?.code ?? "";
        if (!selectedCode || !data.page.items.some((item) => item.code === selectedCode)) setSelectedCode(first);
      }
    } finally {
      if (!silent) setLoading(false);
    }
  }, [api, filters, selectedCode]);
  const loadDetail = useCallback(async (code: string) => {
    if (!code) { setDetail(null); return; }
    const data = await api.get<{ feedback: FeedbackRecord }>(`/api/admin/feedbacks/${code}`);
    if (data?.feedback) setDetail(data.feedback);
  }, [api]);
  const loadMails = useCallback(async () => {
    const data = await api.get<{ page: Page<MailRecord> }>("/api/admin/mails?perPage=50");
    if (data?.page) setMails(data.page);
  }, [api]);
  const loadAudit = useCallback(async () => {
    const data = await api.get<{ page: Page<AuditLog> }>("/api/admin/audit-logs?perPage=50");
    if (data?.page) setAuditLogs(data.page);
  }, [api]);
  const loadWebhooks = useCallback(async () => {
    const data = await api.get<{ page: Page<WebhookDelivery>; webhooks: WebhookSummary[] }>("/api/admin/webhooks/deliveries?perPage=50");
    if (data?.page) setWebhookDeliveries(data.page);
  }, [api]);
  const loadBackups = useCallback(async () => {
    const data = await api.get<{ backups: BackupInfo[] }>("/api/admin/backups");
    if (data?.backups) setBackups(data.backups);
  }, [api]);

  const refreshAll = useCallback(async (silent = false) => {
    if (silent) setRefreshing(true);
    await Promise.all([
      loadOverview(),
      loadSummary(),
      loadTickets(silent),
      selectedCode ? loadDetail(selectedCode) : Promise.resolve(),
      loadMails(),
      loadConfig(),
      loadAudit(),
      loadWebhooks(),
      loadBackups()
    ]);
    setLastRefresh(new Date().toLocaleTimeString());
    if (silent) setRefreshing(false);
  }, [loadOverview, loadSummary, loadTickets, selectedCode, loadDetail, loadMails, loadConfig, loadAudit, loadWebhooks, loadBackups]);

  useEffect(() => { void refreshAll(false); }, [refreshAll]);
  useLiveInterval(() => { void refreshAll(true); }, refreshMs);

  async function logout() {
    await api.post("/api/auth/logout", {});
    onLogout();
  }

  return (
    <main className="admin-shell">
      <aside className="rail">
        <div className="brand-mark"><BriefcaseBusiness size={23} /><span>FeedbackOps</span></div>
        <nav>
          <NavButton active={tab === "dashboard"} onClick={() => setTab("dashboard")} icon={<Home size={18} />} text="工作台" />
          <NavButton active={tab === "tickets"} onClick={() => setTab("tickets")} icon={<Inbox size={18} />} text="工单流" />
          <NavButton active={tab === "board"} onClick={() => setTab("board")} icon={<KanbanSquare size={18} />} text="看板" />
          <NavButton active={tab === "mail"} onClick={() => setTab("mail")} icon={<Mail size={18} />} text="通知" />
          <NavButton active={tab === "integrations"} onClick={() => setTab("integrations")} icon={<Webhook size={18} />} text="集成" />
          <NavButton active={tab === "ops"} onClick={() => setTab("ops")} icon={<Database size={18} />} text="运维" />
          <NavButton active={tab === "config"} onClick={() => setTab("config")} icon={<Settings size={18} />} text="配置" />
        </nav>
        <button className="ghost-button" onClick={() => void logout()}><LogOut size={18} /> 退出</button>
      </aside>

      <section className="workbench">
        <header className="hero-bar reveal">
          <div>
            <span className="eyebrow"><Activity size={15} /> {tabTitle(tab)}</span>
            <h1>{tabHeading(tab)}</h1>
            <p>{tabDescription(tab)}</p>
          </div>
          <div className="hero-actions">
            <span className={`refresh-stamp ${refreshing ? "live" : ""}`}><Clock3 size={15} /> {lastRefresh || "等待刷新"}</span>
            <button className="icon-button" onClick={() => void refreshAll(false)} title="刷新" aria-label="刷新"><RefreshCw className={refreshing ? "spin" : ""} size={18} /></button>
          </div>
        </header>

        {notice ? <button className="notice" onClick={onClearNotice}>{notice}</button> : null}
        {tab === "dashboard" ? (overview ? <Dashboard overview={overview} summary={summary} config={config} onTestMail={() => void api.post("/api/admin/mails/test", {}).then(() => loadMails())} /> : <SkeletonGrid />) : null}
        {tab === "tickets" ? <TicketWorkspace api={api} page={page} detail={detail} filters={filters} loading={loading} selectedCode={selectedCode} selectedCodes={selectedCodes} onFilters={setFilters} onSelect={setSelectedCode} onSelectMany={setSelectedCodes} onSaved={(next) => { setDetail(next); void refreshAll(true); }} /> : null}
        {tab === "board" ? <BoardWorkspace api={api} page={page} onMoved={() => void refreshAll(true)} /> : null}
        {tab === "mail" ? <MailWorkspace page={mails} onTestMail={() => void api.post("/api/admin/mails/test", {}).then(() => loadMails())} /> : null}
        {tab === "integrations" ? <IntegrationWorkspace api={api} config={config} page={webhookDeliveries} onChanged={() => void refreshAll(true)} /> : null}
        {tab === "ops" ? <OpsWorkspace api={api} backups={backups} auditLogs={auditLogs} onChanged={() => void refreshAll(true)} /> : null}
        {tab === "config" ? <ConfigWorkspace api={api} config={config} overview={overview} onChanged={() => void refreshAll(true)} /> : null}
      </section>
    </main>
  );
}

function NavButton({ active, onClick, icon, text }: { active: boolean; onClick: () => void; icon: React.ReactNode; text: string }) {
  return <button className={active ? "active" : ""} onClick={onClick}>{icon} {text}</button>;
}

function Dashboard({ overview, summary, config, onTestMail }: { overview: Overview; summary: FeedbackSummary | null; config: ConfigHealth | null; onTestMail: () => void }) {
  const counts = overview.statusCounts ?? {};
  return (
    <section className="dashboard-grid reveal">
      <Metric icon={<Inbox size={18} />} label="总反馈" value={overview.feedbackTotal} accent="cyan" />
      <Metric icon={<TimerReset size={18} />} label="今日新增" value={overview.todayFeedback} accent="blue" />
      <Metric icon={<Activity size={18} />} label="处理中" value={counts.investigating ?? 0} accent="amber" />
      <Metric icon={<CheckCircle2 size={18} />} label="已解决" value={counts.resolved ?? 0} accent="green" />
      <Metric icon={<XCircle size={18} />} label="逾期/邮件失败" value={(overview.overdue ?? 0) + overview.mailFailed} accent="red" />
      <article className="chart-panel status-panel"><div className="card-title"><CircleGauge size={18} /> 状态分布</div><StatusDonut counts={counts} total={overview.feedbackTotal} /></article>
      <article className="chart-panel"><div className="card-title"><BarChart3 size={18} /> 分类统计</div><MiniBars values={summary?.categoryCounts ?? overview.categoryCounts ?? {}} labels={categoryLabels} /></article>
      <article className="chart-panel"><div className="card-title"><Gauge size={18} /> SLA 与优先级</div><MiniBars values={summary?.slaCounts ?? overview.slaCounts ?? {}} labels={slaLabels} /><div className="divider" /><MiniBars values={summary?.priorityCounts ?? overview.priorityCounts ?? {}} labels={priorityLabels} /></article>
      <article className="activity-card"><div className="card-title"><GitBranch size={18} /> 最近处理动态</div><div className="mini-timeline">{overview.recentEvents?.length ? overview.recentEvents.map((event) => <EventLine key={event.id} event={event} compact />) : <span className="muted">暂无动态</span>}</div></article>
      <article className="activity-card"><div className="card-title"><BellRing size={18} /> 通知与配置</div><div className="mail-signal"><Mail size={20} /><strong>{overview.mail.configured ? "SMTP 已配置" : "SMTP 待完善"}</strong><small>{overview.mail.host || "未配置主机"} · 失败 {overview.mailFailed}</small></div><button className="ghost-button compact" onClick={onTestMail}><Send size={16} /> 测试邮件</button><div className="config-line"><HardDrive size={15} /> 存储 {formatBytes(overview.storage.bytes)}</div><div className="config-line"><Database size={15} /> 数据库 {formatBytes(overview.database.bytes)} · WAL {overview.database.walMode || "unknown"}</div></article>
      {config ? <section className="health-strip">{config.checks.slice(0, 8).map((check) => <HealthChip key={check.key} check={check} />)}</section> : null}
    </section>
  );
}

function TicketWorkspace(props: {
  api: ReturnType<typeof createApi>;
  page: Page<FeedbackRecord> | null;
  detail: FeedbackRecord | null;
  filters: Filters;
  loading: boolean;
  selectedCode: string;
  selectedCodes: string[];
  onFilters: React.Dispatch<React.SetStateAction<Filters>>;
  onSelect: (code: string) => void;
  onSelectMany: (codes: string[]) => void;
  onSaved: (next: FeedbackRecord) => void;
}) {
  const { api, page, detail, filters, loading, selectedCode, selectedCodes, onFilters, onSelect, onSelectMany, onSaved } = props;
  const toggle = (code: string) => onSelectMany(selectedCodes.includes(code) ? selectedCodes.filter((item) => item !== code) : [...selectedCodes, code]);
  async function bulk(status: string) {
    if (!selectedCodes.length) return;
    await api.patch("/api/admin/feedbacks/bulk", { codes: selectedCodes, status });
    onSelectMany([]);
  }
  async function exportCSV() {
    const blob = await api.download(`/api/admin/feedbacks/export?${filterQuery(filters)}`);
    if (!blob) return;
    const url = URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = url;
    link.download = "feedbacks.csv";
    link.click();
    URL.revokeObjectURL(url);
  }
  return (
    <section className="ticket-grid reveal">
      <aside className="filter-pane">
        <div className="card-title"><Filter size={18} /> 筛选</div>
        <label><span>关键词</span><div className="input-icon"><Search size={15} /><input value={filters.q} onChange={(event) => onFilters((value) => ({ ...value, q: event.target.value, page: 1 }))} placeholder="编号、标题、联系人" /></div></label>
        <div className="two-inputs"><label><span>指派</span><input value={filters.assignee} onChange={(event) => onFilters((value) => ({ ...value, assignee: event.target.value, page: 1 }))} placeholder="处理人" /></label><label><span>标签</span><input value={filters.tag} onChange={(event) => onFilters((value) => ({ ...value, tag: event.target.value, page: 1 }))} placeholder="tag" /></label></div>
        <Segment label="状态" value={filters.status} options={statusLabels} onChange={(status) => onFilters((value) => ({ ...value, status, page: 1 }))} />
        <Segment label="分类" value={filters.category} options={categoryLabels} onChange={(category) => onFilters((value) => ({ ...value, category, page: 1 }))} />
        <Segment label="优先级" value={filters.priority} options={priorityLabels} onChange={(priority) => onFilters((value) => ({ ...value, priority, page: 1 }))} />
        <Segment label="SLA" value={filters.sla} options={slaLabels} onChange={(sla) => onFilters((value) => ({ ...value, sla, page: 1 }))} />
        <Segment label="通知" value={filters.mail} options={{ sent: "已通知", pending: "未通知" }} onChange={(mail) => onFilters((value) => ({ ...value, mail, page: 1 }))} />
        <label className="checkline"><input type="checkbox" checked={filters.overdue === "true"} onChange={(event) => onFilters((value) => ({ ...value, overdue: event.target.checked ? "true" : "", page: 1 }))} /> 只看逾期</label>
        <label><span>排序</span><select value={filters.sort} onChange={(event) => onFilters((value) => ({ ...value, sort: event.target.value, page: 1 }))}><option value="last">最近活动</option><option value="priority">优先级</option><option value="due">截止时间</option><option value="oldest">最早提交</option></select></label>
        <button className="ghost-button compact" onClick={() => void exportCSV()}><Download size={16} /> 导出 CSV</button>
      </aside>

      <section className="queue-pane">
        <div className="pane-title"><span><Inbox size={18} /> 工单队列</span><small>{page ? `${page.total} 条` : loading ? "加载中" : "0 条"}</small></div>
        {selectedCodes.length ? <div className="bulk-bar"><span>已选 {selectedCodes.length} 条</span><button onClick={() => void bulk("triaged")}>归类</button><button onClick={() => void bulk("investigating")}>处理</button><button onClick={() => void bulk("resolved")}>解决</button><button onClick={() => void bulk("archived")}>归档</button></div> : null}
        <div className="ticket-list">
          {loading && !page ? <SkeletonList /> : null}
          {page?.items.map((item) => (
            <article key={item.code} className={`ticket-row ${selectedCode === item.code ? "active" : ""}`} onClick={() => onSelect(item.code)}>
              <button className="check-button" onClick={(event) => { event.stopPropagation(); toggle(item.code); }} aria-label="选择工单">{selectedCodes.includes(item.code) ? <SquareCheckBig size={18} /> : <Square size={18} />}</button>
              <div className="ticket-main">
                <div className="ticket-top"><StatusPill value={item.status} /><PriorityPill value={item.priority} /><SLAPill value={item.slaLevel} /><span className="category-tag">{categoryLabels[item.category] ?? item.category}</span></div>
                <strong>{item.title}</strong>
                <p>{item.statusDetail || item.body || item.summaryText || "暂无摘要"}</p>
                <small>{item.code} · {item.assignee || "未指派"} · {formatDate(item.lastActivityAt || item.receivedAt)}</small>
              </div>
              <ChevronRight size={18} />
            </article>
          ))}
          {!page?.items.length && !loading ? <div className="empty-state">没有符合条件的工单</div> : null}
        </div>
        {page ? <Pager page={page} onPage={(next) => onFilters((value) => ({ ...value, page: next }))} /> : null}
      </section>

      <TicketDetail api={api} detail={detail} onSaved={onSaved} />
    </section>
  );
}

function TicketDetail({ api, detail, onSaved }: { api: ReturnType<typeof createApi>; detail: FeedbackRecord | null; onSaved: (next: FeedbackRecord) => void }) {
  const [form, setForm] = useState({ status: "new", category: "issue", priority: "normal", slaLevel: "standard", statusDetail: "", handledBy: "", assignee: "", dueAt: "", resolution: "", note: "", publicReply: "", tags: "" });
  const [comment, setComment] = useState("");
  const [saving, setSaving] = useState(false);
  const [dirty, setDirty] = useState(false);
  const [editingCode, setEditingCode] = useState("");

  useEffect(() => {
    if (!detail) return;
    if (detail.code !== editingCode || !dirty) {
      setForm({
        status: detail.status || "new",
        category: detail.category || detail.type || "issue",
        priority: detail.priority || detail.severity || "normal",
        slaLevel: detail.slaLevel || "standard",
        statusDetail: detail.statusDetail || "",
        handledBy: detail.handledBy || "",
        assignee: detail.assignee || "",
        dueAt: toLocalInput(detail.dueAt),
        resolution: detail.resolution || "",
        note: detail.note || "",
        publicReply: detail.publicReply || "",
        tags: (detail.tags ?? []).join(", ")
      });
      setEditingCode(detail.code);
      setDirty(false);
    }
  }, [detail, dirty, editingCode]);

  function updateForm(next: Partial<typeof form>) {
    setForm((value) => ({ ...value, ...next }));
    setDirty(true);
  }
  if (!detail) return <aside className="detail-pane empty-state">选择一条工单查看详情</aside>;

  const payload = (next = form) => ({
    status: next.status,
    category: next.category,
    priority: next.priority,
    slaLevel: next.slaLevel,
    statusDetail: next.statusDetail,
    handledBy: next.handledBy,
    assignee: next.assignee,
    dueAt: fromLocalInput(next.dueAt),
    resolution: next.resolution,
    note: next.note,
    publicReply: next.publicReply,
    tags: splitTags(next.tags)
  });

  async function save(nextForm = form) {
    setSaving(true);
    try {
      const data = await api.patch<{ feedback: FeedbackRecord }>(`/api/admin/feedbacks/${detail.code}`, payload(nextForm));
      if (data?.feedback) {
        setDirty(false);
        onSaved(data.feedback);
      }
    } finally {
      setSaving(false);
    }
  }
  async function quickStatus(status: string) {
    const next = { ...form, status };
    setForm(next);
    setDirty(true);
    await save(next);
  }
  async function addComment() {
    if (!comment.trim()) return;
    const data = await api.post<{ comment: FeedbackComment }>(`/api/admin/feedbacks/${detail.code}/comments`, { author: form.handledBy || form.assignee || "admin", body: comment, internal: true });
    if (data?.comment) {
      setComment("");
      onSaved({ ...detail, comments: [data.comment, ...(detail.comments ?? [])] });
    }
  }

  return (
    <aside className="detail-pane">
      <header className="detail-head">
        <div><StatusPill value={form.status} /><h2>{detail.title}</h2><p>{detail.code} · {formatDate(detail.receivedAt)}</p></div>
        <button className="primary-button compact" onClick={() => void save()} disabled={saving}>{saving ? <Loader2 className="spin" size={16} /> : <Save size={16} />} 保存</button>
      </header>
      <div className="quick-status">{statusOrder.map((status) => <button key={status} className={form.status === status ? "active" : ""} onClick={() => void quickStatus(status)}>{statusLabels[status]}</button>)}</div>
      <div className="info-grid">
        <Info icon={<UserRound size={15} />} label="联系方式" value={detail.contact || "未填写"} />
        <Info icon={<Activity size={15} />} label="来源地址" value={detail.remoteAddr || "未知"} />
        <Info icon={<Mail size={15} />} label="邮件通知" value={detail.mailSent ? "已通知" : "待通知"} />
        <Info icon={<Tags size={15} />} label="标签" value={(detail.tags ?? []).join(", ") || "无"} />
      </div>
      <section className="report-block"><h3>用户反馈</h3><pre>{detail.body || "无正文"}</pre></section>
      <section className="report-block"><h3>反馈包摘要</h3><pre>{detail.summaryText || "无摘要"}</pre></section>
      <section className="workflow-form">
        <label><span>状态</span><select value={form.status} onChange={(event) => updateForm({ status: event.target.value })}>{Object.entries(statusLabels).map(([value, label]) => <option key={value} value={value}>{label}</option>)}</select></label>
        <label><span>分类</span><select value={form.category} onChange={(event) => updateForm({ category: event.target.value })}>{Object.entries(categoryLabels).map(([value, label]) => <option key={value} value={value}>{label}</option>)}</select></label>
        <label><span>优先级</span><select value={form.priority} onChange={(event) => updateForm({ priority: event.target.value })}>{Object.entries(priorityLabels).map(([value, label]) => <option key={value} value={value}>{label}</option>)}</select></label>
        <label><span>SLA</span><select value={form.slaLevel} onChange={(event) => updateForm({ slaLevel: event.target.value })}>{Object.entries(slaLabels).map(([value, label]) => <option key={value} value={value}>{label}</option>)}</select></label>
        <label><span>处理人</span><input value={form.handledBy} onChange={(event) => updateForm({ handledBy: event.target.value })} placeholder="admin" /></label>
        <label><span>指派给</span><input value={form.assignee} onChange={(event) => updateForm({ assignee: event.target.value })} placeholder="负责人" /></label>
        <label><span>截止时间</span><input type="datetime-local" value={form.dueAt} onChange={(event) => updateForm({ dueAt: event.target.value })} /></label>
        <label><span>标签</span><input value={form.tags} onChange={(event) => updateForm({ tags: event.target.value })} placeholder="crash, ui" /></label>
        <label className="full"><span>状态说明</span><input value={form.statusDetail} onChange={(event) => updateForm({ statusDetail: event.target.value })} placeholder="当前处理说明" /></label>
        <label className="full"><span>解决说明</span><textarea rows={3} value={form.resolution} onChange={(event) => updateForm({ resolution: event.target.value })} /></label>
        <label className="full"><span>内部备注</span><textarea rows={4} value={form.note} onChange={(event) => updateForm({ note: event.target.value })} /></label>
        <label className="full"><span>公开回复</span><textarea rows={4} value={form.publicReply} onChange={(event) => updateForm({ publicReply: event.target.value })} /></label>
      </section>
      <section className="comment-box">
        <div className="card-title"><MessageSquareText size={18} /> 内部评论</div>
        <div className="comment-input"><textarea rows={3} value={comment} onChange={(event) => setComment(event.target.value)} placeholder="记录处理进展、复现信息或后续动作" /><button className="ghost-button compact" onClick={() => void addComment()}><Plus size={16} /> 添加</button></div>
        <div className="comment-list">{detail.comments?.length ? detail.comments.map((item) => <article key={item.id}><strong>{item.author || "admin"}</strong><small>{formatDate(item.createdAt)}</small><p>{item.body}</p></article>) : <span className="muted">暂无评论</span>}</div>
      </section>
      <details className="package-box"><summary>包路径与校验</summary><code>{detail.packagePath}</code><code>{detail.encryptedPackagePath}</code><code>{detail.packageSha256}</code><code>{detail.plainPackageSha256}</code><code>{detail.includedFiles || "无文件清单"}</code></details>
      <section className="timeline"><h3>处理时间线</h3>{detail.events?.length ? detail.events.map((event) => <EventLine key={event.id} event={event} />) : <span className="muted">暂无时间线</span>}</section>
    </aside>
  );
}

function BoardWorkspace({ api, page, onMoved }: { api: ReturnType<typeof createApi>; page: Page<FeedbackRecord> | null; onMoved: () => void }) {
  const [dragCode, setDragCode] = useState("");
  const items = page?.items ?? [];
  async function move(code: string, status: string) {
    const item = items.find((candidate) => candidate.code === code);
    if (!item) return;
    await api.patch(`/api/admin/feedbacks/${code}`, payloadFromRecord(item, { status }));
    onMoved();
  }
  return (
    <section className="board-grid reveal">
      {statusOrder.map((status) => (
        <div key={status} className="board-column" onDragOver={(event) => event.preventDefault()} onDrop={() => { if (dragCode) void move(dragCode, status); setDragCode(""); }}>
          <div className="board-title"><StatusPill value={status} /><small>{items.filter((item) => item.status === status).length}</small></div>
          {items.filter((item) => item.status === status).map((item) => (
            <article key={item.code} className="board-card" draggable onDragStart={() => setDragCode(item.code)}>
              <strong>{item.title}</strong>
              <p>{item.statusDetail || item.summaryText || "暂无摘要"}</p>
              <div><PriorityPill value={item.priority} /><SLAPill value={item.slaLevel} /></div>
              <small>{item.assignee || "未指派"} · {item.code}</small>
            </article>
          ))}
          {!items.filter((item) => item.status === status).length ? <div className="empty-mini">拖入工单</div> : null}
        </div>
      ))}
    </section>
  );
}

function MailWorkspace({ page, onTestMail }: { page: Page<MailRecord> | null; onTestMail: () => void }) {
  return (
    <section className="mail-workspace reveal">
      <div className="pane-title"><span><Mail size={18} /> 通知记录</span><button className="ghost-button compact" onClick={onTestMail}><Send size={16} /> 测试通知</button></div>
      <div className="mail-grid">{page?.items.map((mail) => <details key={mail.id} className="mail-card"><summary><StatusPill value={mail.status} /><strong>{mail.subject}</strong><small>{mail.toAddress} · {formatDate(mail.createdAt)}</small></summary><pre>{mail.errorMessage || mail.plainBody || "无正文"}</pre>{mail.attachmentPath ? <code>{mail.attachmentPath}</code> : null}</details>)}{!page?.items.length ? <div className="empty-state">暂无邮件记录</div> : null}</div>
    </section>
  );
}

function IntegrationWorkspace({ api, config, page, onChanged }: { api: ReturnType<typeof createApi>; config: ConfigHealth | null; page: Page<WebhookDelivery> | null; onChanged: () => void }) {
  return (
    <section className="integration-grid reveal">
      <div className="pane-title span-all"><span><Webhook size={18} /> 通知集成</span><button className="ghost-button compact" onClick={() => void api.post("/api/admin/webhooks/test", {}).then(onChanged)}><Send size={16} /> 测试 Webhook</button></div>
      <section className="integration-list">{config?.webhooks?.length ? config.webhooks.map((hook) => <article key={hook.name || hook.host} className="integration-card"><div><strong>{hook.name || "Webhook"}</strong><small>{hook.host || "未设置 host"}</small></div><StatusPill value={hook.enabled ? "sent" : "archived"} /><p>事件：{hook.events?.join(", ") || "全部"} · secret {hook.secretConfigured ? "已配置" : "未配置"} · 重试 {hook.maxRetries}</p></article>) : <div className="empty-state">尚未在 config.txt 配置 Webhook</div>}</section>
      <section className="delivery-list"><div className="card-title"><BellRing size={18} /> 最近投递</div>{page?.items.map((item) => <article key={item.id} className="delivery-row"><StatusPill value={item.status} /><strong>{item.event}</strong><small>{item.webhookName} · 尝试 {item.attempts} · HTTP {item.responseCode || "-"}</small><p>{item.errorMessage || item.payloadSha256}</p></article>)}{!page?.items.length ? <div className="empty-state">暂无投递记录</div> : null}</section>
    </section>
  );
}

function OpsWorkspace({ api, backups, auditLogs, onChanged }: { api: ReturnType<typeof createApi>; backups: BackupInfo[]; auditLogs: Page<AuditLog> | null; onChanged: () => void }) {
  async function createBackup() {
    await api.post("/api/admin/backups/database", {});
    onChanged();
  }
  async function downloadBackup(name: string) {
    const blob = await api.download(`/api/admin/backups/${encodeURIComponent(name)}`);
    if (!blob) return;
    const url = URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = url;
    link.download = name;
    link.click();
    URL.revokeObjectURL(url);
  }
  return (
    <section className="ops-grid reveal">
      <section className="ops-panel"><div className="pane-title"><span><Database size={18} /> 数据库备份</span><button className="ghost-button compact" onClick={() => void createBackup()}><Archive size={16} /> 创建备份</button></div>{backups.length ? backups.map((backup) => <article key={backup.name} className="backup-row"><div><strong>{backup.name}</strong><small>{formatDate(backup.createdAt)} · {formatBytes(backup.bytes)}</small></div><button className="icon-button" onClick={() => void downloadBackup(backup.name)} title="下载备份" aria-label="下载备份"><Download size={17} /></button></article>) : <div className="empty-state">暂无备份</div>}</section>
      <section className="ops-panel"><div className="card-title"><ShieldCheck size={18} /> 审计日志</div>{auditLogs?.items.map((log) => <article key={log.id} className="audit-row"><strong>{log.type}</strong><small>{log.actor || "system"} · {formatDate(log.createdAt)} · {log.ip}</small><p>{log.target ? `${log.target} · ` : ""}{log.message}</p></article>)}{!auditLogs?.items.length ? <div className="empty-state">暂无审计日志</div> : null}</section>
    </section>
  );
}

function ConfigWorkspace({ api, config, overview, onChanged }: { api: ReturnType<typeof createApi>; config: ConfigHealth | null; overview: Overview | null; onChanged: () => void }) {
  const [draft, setDraft] = useState<EditableConfig | null>(null);
  const [dirty, setDirty] = useState(false);
  const [busy, setBusy] = useState("");
  const [message, setMessage] = useState("");
  useEffect(() => { if (config?.editable && !dirty) setDraft(structuredClone(config.editable)); }, [config, dirty]);
  if (!config || !draft) return <SkeletonGrid />;
  const runtime = config.database.runtime;
  const db = draft.database;
  const editDraft = (updater: (value: EditableConfig) => EditableConfig) => {
    setDirty(true);
    setDraft((value) => value ? updater(value) : value);
  };
  const setDb = (next: Partial<EditableDatabase>) => editDraft((value) => ({ ...value, database: { ...value.database, ...next } }));
  const setMail = (next: Partial<EditableConfig["mail"]>) => editDraft((value) => ({ ...value, mail: { ...value.mail, ...next } }));
  const setHook = (index: number, next: Partial<EditableWebhook>) => setDraft((value) => {
    setDirty(true);
    if (!value) return value;
    const hooks = [...value.webhooks];
    hooks[index] = { ...hooks[index], ...next };
    return { ...value, webhooks: hooks };
  });
  const dbPayload = () => ({
    provider: db.provider,
    sqlite_path: db.sqlitePath,
    host: db.host,
    port: Number(db.port || 0),
    name: db.name,
    user: db.user,
    password: db.password,
    dsn: db.dsnConfigured && db.dsn === "***" ? "" : db.dsn,
    ssl_mode: db.sslMode,
    max_open_conns: Number(db.maxOpenConns || 10),
    max_idle_conns: Number(db.maxIdleConns || 4),
    conn_max_lifetime_seconds: Number(db.connMaxLifetimeSeconds || 300),
    failover_enabled: db.failoverEnabled,
    health_interval_seconds: Number(db.healthIntervalSeconds || 30),
    sync: {
      enabled: db.sync?.enabled !== false,
      interval_seconds: Number(db.sync?.intervalSeconds ?? db.sync?.interval_seconds ?? 86400),
      batch_size: Number(db.sync?.batchSize ?? db.sync?.batch_size ?? 500)
    }
  });
  const snakeRecord = (record: Record<string, number | boolean>) => Object.fromEntries(Object.entries(record ?? {}).map(([key, value]) => [key.replace(/[A-Z]/g, (match) => `_${match.toLowerCase()}`), value]));
  async function action(name: string, run: () => Promise<unknown>, options: { refresh?: boolean; syncDraft?: boolean } = {}) {
    setBusy(name); setMessage("");
    try {
      const result = await run();
      setMessage(`${name} 已完成`);
      if (options.syncDraft) {
        const nextConfig = (result as { config?: ConfigHealth } | null)?.config;
        if (nextConfig?.editable) setDraft(structuredClone(nextConfig.editable));
        setDirty(false);
      }
      if (options.refresh !== false) onChanged();
    } catch (error) {
      setMessage(`${name} 失败：${error instanceof Error ? error.message : String(error)}`);
    } finally {
      setBusy("");
    }
  }
  const save = () => action("保存配置", () => api.patch<{ config: ConfigHealth }>("/api/admin/config", {
    listen: draft.listen,
    database: dbPayload(),
    mail: {
      host: draft.mail.host,
      port: Number(draft.mail.port || 0),
      secure: draft.mail.secure,
      username: draft.mail.username,
      password: draft.mail.password,
      from_address: draft.mail.fromAddress,
      from_name: draft.mail.fromName,
      developer_address: draft.mail.developerAddress,
      timeout_seconds: Number(draft.mail.timeoutSeconds || 20)
    },
    backup: draft.backup,
    rateLimit: snakeRecord(draft.rateLimit),
    uploadGuard: snakeRecord(draft.uploadGuard),
    webhooks: draft.webhooks.map((hook) => ({ name: hook.name, url: hook.url || "", secret: hook.secret || "", enabled: hook.enabled, events: hook.events, timeout_seconds: hook.timeoutSeconds, max_retries: hook.maxRetries }))
  }), { syncDraft: true });
  return (
    <section className="config-page reveal">
      <div className="config-checks">{config.checks.map((check) => <HealthChip key={check.key} check={check} />)}</div>
      {message ? <button className="notice" onClick={() => setMessage("")}>{message}</button> : null}
      {dirty ? <div className="notice subtle">有未保存的配置修改，实时刷新不会覆盖当前表单。</div> : null}
      <div className="config-grid">
        <article className="config-section span-all">
          <div className="pane-title"><span><Database size={18} /> 数据库与热备</span><div className="inline-actions"><button className="ghost-button compact" disabled={!!busy} onClick={() => void action("测试数据库", () => api.post("/api/admin/config/test/database", dbPayload()), { refresh: false })}>测试连接</button><button className="ghost-button compact" disabled={!!busy || db.provider === "sqlite"} onClick={() => void action("导入 SQLite", () => api.post("/api/admin/database/import", {}))}>导入 SQLite</button><button className="ghost-button compact" disabled={!!busy || db.provider === "sqlite"} onClick={() => void action("同步热备", () => api.post("/api/admin/database/sync", {}))}>同步</button></div></div>
          <div className="workflow-form">
            <label><span>数据库类型</span><select value={db.provider} onChange={(event) => setDb({ provider: event.target.value })}><option value="sqlite">SQLite</option><option value="mysql">MySQL</option><option value="postgres">PostgreSQL</option></select></label>
            <label><span>SQLite 路径</span><input value={db.sqlitePath || ""} onChange={(event) => setDb({ sqlitePath: event.target.value })} /></label>
            <label><span>主机</span><input value={db.host || ""} onChange={(event) => setDb({ host: event.target.value })} disabled={db.provider === "sqlite"} /></label>
            <label><span>端口</span><input type="number" value={db.port || 0} onChange={(event) => setDb({ port: Number(event.target.value) })} disabled={db.provider === "sqlite"} /></label>
            <label><span>数据库名</span><input value={db.name || ""} onChange={(event) => setDb({ name: event.target.value })} disabled={db.provider === "sqlite"} /></label>
            <label><span>用户名</span><input value={db.user || ""} onChange={(event) => setDb({ user: event.target.value })} disabled={db.provider === "sqlite"} /></label>
            <label><span>密码</span><input type="password" value={db.password || ""} onChange={(event) => setDb({ password: event.target.value })} placeholder={db.passwordConfigured ? "留空保持原密码" : "未配置"} disabled={db.provider === "sqlite"} /></label>
            <label><span>SSL</span><input value={db.sslMode || "disable"} onChange={(event) => setDb({ sslMode: event.target.value })} disabled={db.provider === "sqlite"} /></label>
            <label className="full"><span>DSN</span><input value={db.dsn || ""} onChange={(event) => setDb({ dsn: event.target.value })} placeholder={db.dsnConfigured ? "已配置，留空保持" : "可选"} disabled={db.provider === "sqlite"} /></label>
            <label className="checkline"><input type="checkbox" checked={db.failoverEnabled} onChange={(event) => setDb({ failoverEnabled: event.target.checked })} /> 远端失败自动切换 SQLite</label>
            <label className="checkline"><input type="checkbox" checked={db.sync?.enabled !== false} onChange={(event) => setDb({ sync: { ...db.sync, enabled: event.target.checked } })} /> 启用热备同步</label>
          </div>
          <div className={`db-runtime ${runtime?.failoverActive ? "warning" : "ok"}`}><strong>当前主库：{runtime?.activeProvider || config.database.provider}</strong><span>配置：{runtime?.configProvider || config.database.provider} · SQLite {runtime?.sqliteReady ? "可用" : "异常"} · 远端 {runtime?.remoteReady ? "可用" : "未连接"}</span><small>{runtime?.lastError || `最近同步：${formatDate(runtime?.lastSyncAt || "") || "暂无"}`}</small></div>
        </article>

        <article className="config-section">
          <div className="card-title"><Server size={18} /> 基础服务</div>
          <div className="workflow-form single"><label><span>监听地址</span><input value={draft.listen || ""} onChange={(event) => editDraft((value) => ({ ...value, listen: event.target.value }))} /></label><label><span>备份目录</span><input value={draft.backup?.dir || ""} onChange={(event) => editDraft((value) => ({ ...value, backup: { dir: event.target.value } }))} /></label></div>
          <dl><div><dt>服务根目录</dt><dd>{config.service.baseDir}</dd></div><div><dt>数据库大小</dt><dd>{formatBytes(config.database.bytes || overview?.database.bytes || 0)}</dd></div></dl>
        </article>

        <article className="config-section">
          <div className="card-title"><Mail size={18} /> 邮件通知</div>
          <div className="workflow-form single"><label><span>SMTP 主机</span><input value={draft.mail.host || ""} onChange={(event) => setMail({ host: event.target.value })} /></label><label><span>端口</span><input type="number" value={draft.mail.port || 0} onChange={(event) => setMail({ port: Number(event.target.value) })} /></label><label><span>账号</span><input value={draft.mail.username || ""} onChange={(event) => setMail({ username: event.target.value })} /></label><label><span>密码</span><input type="password" value={draft.mail.password || ""} onChange={(event) => setMail({ password: event.target.value })} placeholder={draft.mail.passwordConfigured ? "留空保持原密码" : "未配置"} /></label><label><span>发件地址</span><input value={draft.mail.fromAddress || ""} onChange={(event) => setMail({ fromAddress: event.target.value })} /></label><label><span>开发者邮箱</span><input value={draft.mail.developerAddress || ""} onChange={(event) => setMail({ developerAddress: event.target.value })} /></label></div>
        </article>

        <article className="config-section span-all">
          <div className="pane-title"><span><Webhook size={18} /> Webhook</span><button className="ghost-button compact" onClick={() => editDraft((value) => ({ ...value, webhooks: [...value.webhooks, { name: "webhook", host: "", url: "", enabled: true, events: ["feedback.created"], secretConfigured: false, secret: "", timeoutSeconds: 5, maxRetries: 2 }] }))}><Plus size={16} /> 添加</button></div>
          <div className="webhook-editor">{draft.webhooks.length ? draft.webhooks.map((hook, index) => <article key={`${hook.name}-${index}`} className="integration-card"><div className="workflow-form"><label><span>名称</span><input value={hook.name || ""} onChange={(event) => setHook(index, { name: event.target.value })} /></label><label><span>URL</span><input value={hook.url || ""} onChange={(event) => setHook(index, { url: event.target.value })} /></label><label><span>Secret</span><input type="password" value={hook.secret || ""} onChange={(event) => setHook(index, { secret: event.target.value })} placeholder={hook.secretConfigured ? "留空保持" : "未配置"} /></label><label><span>事件</span><input value={(hook.events || []).join(",")} onChange={(event) => setHook(index, { events: event.target.value.split(",").map((item) => item.trim()).filter(Boolean) })} /></label><label className="checkline"><input type="checkbox" checked={hook.enabled} onChange={(event) => setHook(index, { enabled: event.target.checked })} /> 启用</label></div><div className="inline-actions"><button className="ghost-button compact" disabled={!!busy} onClick={() => void action("测试 Webhook", () => api.post("/api/admin/config/test/webhook", { name: hook.name, url: hook.url || "", secret: hook.secret || "", enabled: hook.enabled, events: hook.events, timeout_seconds: hook.timeoutSeconds, max_retries: hook.maxRetries }), { refresh: false })}>测试</button><button className="ghost-button compact" onClick={() => editDraft((value) => ({ ...value, webhooks: value.webhooks.filter((_, itemIndex) => itemIndex !== index) }))}>删除</button></div></article>) : <div className="empty-state">尚未配置 Webhook</div>}</div>
        </article>
      </div>
      <div className="config-footer"><button className="ghost-button" disabled={!!busy} onClick={() => void save()}><Save size={16} /> 保存并热生效</button><Clock3 size={15} /> 配置状态生成于 {formatDate(config.generatedAt)} {busy ? `· ${busy} 中...` : ""}</div>
    </section>
  );
}

function ConfigSection({ icon, title, rows }: { icon: React.ReactNode; title: string; rows: Array<[string, string]> }) {
  return <article className="config-section"><div className="card-title">{icon} {title}</div><dl>{rows.map(([key, value]) => <div key={key}><dt>{key}</dt><dd>{value}</dd></div>)}</dl></article>;
}

function StatusDonut({ counts, total }: { counts: Record<string, number>; total: number }) {
  const radius = 46;
  const circumference = 2 * Math.PI * radius;
  const safeTotal = Math.max(1, total);
  let offset = 0;
  return <div className="donut-wrap"><svg viewBox="0 0 120 120" aria-hidden="true"><circle className="donut-bg" cx="60" cy="60" r={radius} />{statusOrder.map((status) => { const value = counts[status] ?? 0; const dash = (value / safeTotal) * circumference; const segment = <circle key={status} className="donut-segment" cx="60" cy="60" r={radius} stroke={statusColors[status]} strokeDasharray={`${dash} ${circumference - dash}`} strokeDashoffset={-offset} />; offset += dash; return segment; })}</svg><div><strong>{total}</strong><span>总工单</span></div><div className="donut-legend">{statusOrder.map((status) => <span key={status}><i style={{ background: statusColors[status] }} />{statusLabels[status]} {counts[status] ?? 0}</span>)}</div></div>;
}

function MiniBars({ values, labels }: { values: Record<string, number>; labels: Record<string, string> }) {
  const max = Math.max(1, ...Object.keys(labels).map((key) => values[key] ?? 0));
  return <div className="status-bars">{Object.entries(labels).map(([key, label]) => <Bar key={key} label={label} value={values[key] ?? 0} total={max} />)}</div>;
}

function Segment({ label, value, options, onChange }: { label: string; value: string; options: Record<string, string>; onChange: (value: string) => void }) {
  return <div className="segment-group"><span>{label}</span><button className={value === "" ? "active" : ""} onClick={() => onChange("")}>全部</button>{Object.entries(options).map(([key, text]) => <button key={key} className={value === key ? "active" : ""} onClick={() => onChange(key)}>{text}</button>)}</div>;
}

function Metric({ icon, label, value, accent }: { icon: React.ReactNode; label: string; value: number; accent: string }) {
  return <article className={`metric ${accent}`}><span>{icon}</span><div><small>{label}</small><strong>{value}</strong></div></article>;
}

function Bar({ label, value, total }: { label: string; value: number; total: number }) {
  return <div className="bar-row"><span>{label}</span><div><i style={{ width: `${Math.min(100, (value / Math.max(1, total)) * 100)}%` }} /></div><b>{value}</b></div>;
}

function StatusPill({ value }: { value: string }) {
  return <span className={`status-pill ${value}`}>{statusLabels[value] ?? statusText(value)}</span>;
}
function PriorityPill({ value }: { value: string }) {
  return <span className={`priority-pill ${value}`}>{priorityLabels[value] ?? (value || "普通")}</span>;
}
function SLAPill({ value }: { value: string }) {
  return <span className={`sla-pill ${value}`}>{slaLabels[value] ?? (value || "标准")}</span>;
}
function HealthChip({ check }: { check: ConfigCheck }) {
  return <div className={`health-chip ${check.status}`}><span className={`health-dot ${check.status}`} /><strong>{check.label}</strong><small>{check.detail || (check.status === "ok" ? "正常" : "待配置")}</small></div>;
}
function Info({ icon, label, value }: { icon: React.ReactNode; label: string; value: string }) {
  return <div className="info-cell"><span>{icon}{label}</span><strong>{value}</strong></div>;
}
function EventLine({ event, compact = false }: { event: FeedbackEvent; compact?: boolean }) {
  return <div className={`event-line ${compact ? "compact" : ""}`}><span className="dot" /><div><strong>{event.message || event.eventType}</strong><small>{event.actor || "system"} · {formatDate(event.createdAt)}</small>{event.fromValue || event.toValue ? <em>{event.fromValue || "空"} → {event.toValue || "空"}</em> : null}</div></div>;
}
function Pager<T>({ page, onPage }: { page: Page<T>; onPage: (page: number) => void }) {
  return <div className="pager"><span>{page.total ? `${page.offset + 1}-${Math.min(page.offset + page.perPage, page.total)} / ${page.total}` : "0 / 0"}</span><button disabled={page.page <= 1} onClick={() => onPage(page.page - 1)}>上一页</button><button disabled={page.page >= page.totalPages} onClick={() => onPage(page.page + 1)}>下一页</button></div>;
}
function SkeletonGrid() {
  return <section className="dashboard-grid">{Array.from({ length: 8 }).map((_, index) => <div key={index} className="skeleton" />)}</section>;
}
function SkeletonList() {
  return <>{Array.from({ length: 5 }).map((_, index) => <div key={index} className="skeleton row" />)}</>;
}
function useLiveInterval(callback: () => void, delay: number) {
  useEffect(() => { const timer = window.setInterval(callback, delay); return () => window.clearInterval(timer); }, [callback, delay]);
}

function createApi(csrfToken: string, onNotice: (message: string) => void) {
  async function request<T>(path: string, init: RequestInit = {}) {
    const response = await fetch(path, { ...init, credentials: "include", headers: { "Content-Type": "application/json", ...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}), ...(init.headers ?? {}) } });
    const data = (await response.json().catch(() => ({ ok: false, message: "响应解析失败" }))) as ApiResult<T>;
    if (!response.ok || !data.ok) {
      onNotice(data.message ?? "请求失败");
      if (response.status === 401) { sessionStorage.removeItem("csrfToken"); window.location.reload(); }
      return null;
    }
    if (init.method && init.method !== "GET") onNotice("操作已完成");
    return data;
  }
  async function download(path: string) {
    const response = await fetch(path, { credentials: "include", headers: { ...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}) } });
    if (!response.ok) {
      onNotice("下载失败");
      return null;
    }
    return response.blob();
  }
  return {
    get: <T,>(path: string) => request<T>(path),
    post: <T,>(path: string, body: unknown) => request<T>(path, { method: "POST", body: JSON.stringify(body) }),
    patch: <T,>(path: string, body: unknown) => request<T>(path, { method: "PATCH", body: JSON.stringify(body) }),
    download
  };
}

function filterQuery(filters: Filters) {
  const query = new URLSearchParams({ page: String(filters.page), perPage: String(filters.perPage) });
  for (const key of ["status", "category", "priority", "mail", "q", "assignee", "tag", "sla", "overdue", "sort"] as const) {
    if (filters[key]) query.set(key, filters[key]);
  }
  return query.toString();
}
function payloadFromRecord(item: FeedbackRecord, patch: Partial<FeedbackRecord>) {
  const next = { ...item, ...patch };
  return { status: next.status, category: next.category, priority: next.priority, statusDetail: next.statusDetail, handledBy: next.handledBy, assignee: next.assignee, dueAt: next.dueAt, slaLevel: next.slaLevel, resolution: next.resolution, note: next.note, publicReply: next.publicReply, tags: next.tags ?? [] };
}
function splitTags(value: string) {
  return value.split(",").map((item) => item.trim()).filter(Boolean);
}
function toLocalInput(value: string) {
  if (!value) return "";
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return "";
  const pad = (num: number) => String(num).padStart(2, "0");
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
}
function fromLocalInput(value: string) {
  if (!value) return "";
  const date = new Date(value);
  return Number.isNaN(date.getTime()) ? "" : date.toISOString();
}
function tabTitle(tab: AdminTab) {
  return { dashboard: "实时工作台", tickets: "工单处理", board: "状态看板", mail: "通知中心", integrations: "通知集成", ops: "安全运维", config: "配置健康" }[tab];
}
function tabHeading(tab: AdminTab) {
  return { dashboard: "反馈运营工作台", tickets: "反馈工单流", board: "工单状态看板", mail: "邮件通知记录", integrations: "Webhook 通知集成", ops: "审计与备份", config: "系统配置健康" }[tab];
}
function tabDescription(tab: AdminTab) {
  return {
    dashboard: "统计、图表和最近动态每 10 秒自动刷新。",
    tickets: "筛选、查看并实时更新每条反馈工单的处理信息。",
    board: "按状态拖动工单，快速推进处理流程。",
    mail: "查看通知投递结果，并发送测试通知。",
    integrations: "查看 Webhook 配置健康、最近投递和测试发送。",
    ops: "只读备份下载和审计日志，不做破坏性维护。",
    config: "只读展示服务、存储、数据库、邮件、安全与上传保护配置。"
  }[tab];
}
function statusText(value: string) {
  if (value === "sent") return "已发送";
  if (value === "failed") return "失败";
  if (value === "pending") return "待处理";
  return value || "未知";
}
function formatBytes(bytes: number) {
  if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
  const units = ["B", "KB", "MB", "GB"];
  let value = bytes;
  let index = 0;
  while (value >= 1024 && index < units.length - 1) { value /= 1024; index++; }
  return `${value.toFixed(value >= 10 || index === 0 ? 0 : 1)} ${units[index]}`;
}
function formatDate(value: string) {
  if (!value) return "";
  const date = new Date(value);
  return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
}

createRoot(document.getElementById("root")!).render(<App />);
