import { useEffect, useState } from "react";
import {
  Alert,
  App as AntApp,
  Button,
  Checkbox,
  Form,
  Input,
  InputNumber,
  Modal,
  Select,
  Space,
  Table,
  Tag,
  Typography,
} from "antd";
import {
  PlusOutlined,
  ReloadOutlined,
  PlayCircleOutlined,
  PauseCircleOutlined,
  ApiOutlined,
  TableOutlined,
  EyeOutlined,
  DeleteOutlined,
} from "@ant-design/icons";
import {
  AgentAccount,
  BindCode,
  Session,
  SyncBinding,
  SyncChannel,
  SyncInspectResult,
  SyncPreviewResult,
  createSyncChannel,
  deleteSyncChannel,
  dropSyncTable,
  getSyncCheckpointMeta,
  inspectSyncChannel,
  listAgents,
  listApps,
  listSyncBindings,
  listSyncChannels,
  listBindCodes,
  createBindCode,
  revokeBindCode,
  previewSyncTable,
  reconcileSyncChannel,
  restoreSyncChannel,
  startSyncChannel,
  stopSyncChannel,
  testSyncEndpoints,
  updateSyncChannel,
} from "./api";

type EndpointForm = {
  driver: string;
  dsn: string;
  tables: string;
};

type FormValues = {
  name: string;
  direction: string;
  conflict_policy: string;
  poll_interval_ms: number;
  agent_id?: number | null;
  app_slug?: string;
  local: EndpointForm;
  remote: EndpointForm;
  pk_columns: string; // table:pk,table:pk
};

function parsePKs(s: string): Record<string, string> {
  const out: Record<string, string> = {};
  for (const part of (s || "").split(",")) {
    const [t, p] = part.split(":").map((x) => x.trim());
    if (t && p) out[t] = p;
  }
  return out;
}

/** DSN 脱敏；file 库尽量只露文件名 */
function formatDsnHint(dsn: string): string {
  const masked = (dsn || "").replace(/:[^:@/]+@/, ":***@");
  const fileMatch = masked.match(/(?:^|[\\/])([^\\/?#]+\.db)(?:\?|$)/i);
  if (fileMatch) return fileMatch[1];
  if (masked.length > 56) return `${masked.slice(0, 56)}…`;
  return masked;
}

function bindingLocalLabel(b: SyncBinding): string {
  return (b.database_name || "").trim() || b.local_database_id;
}

function bindingOnlineLabel(b: SyncBinding): string {
  return (b.display_name || "").trim() || b.online_db_id;
}

/** 联调烟雾 Binding：占位 id，易被误认为「库名对不上」 */
function isSmokeBinding(b: SyncBinding): boolean {
  const note = (b.note || "").toLowerCase();
  if (note.includes("smoke") || note.includes("user-jwt")) return true;
  const local = (b.local_database_id || "").toLowerCase();
  const online = (b.online_db_id || "").toLowerCase();
  return local.startsWith("local_") || online.startsWith("online_");
}

/** 产品：与本地一致的库名优先，勿用落库文件名当主标题 */
function channelOnlineTitle(ch: SyncChannel, bindings: SyncBinding[]): string {
  const linked = bindings.filter((b) => b.channel_id === ch.id);
  const localNames = linked.map((b) => (b.database_name || "").trim()).filter(Boolean);
  if (localNames.length) return localNames[0];
  const onlineNames = linked.map((b) => (b.display_name || "").trim()).filter(Boolean);
  if (onlineNames.length) return onlineNames[0];
  if ((ch.name || "").trim()) return ch.name.trim();
  return ch.remote?.driver || "线上库";
}

function channelOnlineIds(ch: SyncChannel, bindings: SyncBinding[]): string {
  const linked = bindings.filter((b) => b.channel_id === ch.id);
  const ids = linked.map((b) => b.online_db_id).filter(Boolean);
  const uniq = [...new Set(ids)];
  if (!uniq.length) return "";
  // 有可读本地名时副文案带上 local↔online id，便于对账
  const localName = linked.map((b) => (b.database_name || "").trim()).find(Boolean);
  if (localName && linked[0]?.local_database_id) {
    return `${linked[0].local_database_id} ↔ ${uniq.join(" · ")}`;
  }
  return uniq.join(" · ");
}

function toChannelBody(v: FormValues, id?: string): Partial<SyncChannel> {
  return {
    id,
    name: v.name,
    direction: v.direction,
    conflict_policy: v.conflict_policy,
    poll_interval_ms: v.poll_interval_ms || 500,
    agent_id: v.agent_id || undefined,
    app_slug: (v.app_slug || "").trim() || undefined,
    local: {
      driver: v.local.driver,
      dsn: v.local.dsn,
      tables: (v.local.tables || "")
        .split(",")
        .map((x) => x.trim())
        .filter(Boolean),
    },
    remote: {
      driver: v.remote.driver,
      dsn: v.remote.dsn,
      tables: (v.remote.tables || "")
        .split(",")
        .map((x) => x.trim())
        .filter(Boolean),
    },
    pk_columns: parsePKs(v.pk_columns),
  };
}

export function SyncPage(props: {
  session: Session;
  busy: boolean;
  setBusy: (v: boolean) => void;
  setError: (v: string) => void;
  setInfo: (v: string) => void;
}) {
  const { session, busy, setBusy, setError, setInfo } = props;
  const { message } = AntApp.useApp();
  const [items, setItems] = useState<SyncChannel[]>([]);
  const [bindings, setBindings] = useState<SyncBinding[]>([]);
  const [bindCodes, setBindCodes] = useState<BindCode[]>([]);
  const [agents, setAgents] = useState<AgentAccount[]>([]);
  const [appOptions, setAppOptions] = useState<{ value: string; label: string }[]>([]);
  const [loading, setLoading] = useState(false);
  const [open, setOpen] = useState(false);
  const [editing, setEditing] = useState<SyncChannel | null>(null);
  const [showSmokeBindings, setShowSmokeBindings] = useState(false);
  const [filterAgentId, setFilterAgentId] = useState<number | undefined>(undefined);
  const [inspectOpen, setInspectOpen] = useState(false);
  const [inspectCh, setInspectCh] = useState<SyncChannel | null>(null);
  const [inspectSide, setInspectSide] = useState<"remote" | "local">("remote");
  const [inspectData, setInspectData] = useState<SyncInspectResult | null>(null);
  const [inspectLoading, setInspectLoading] = useState(false);
  const [previewOpen, setPreviewOpen] = useState(false);
  const [previewData, setPreviewData] = useState<SyncPreviewResult | null>(null);
  const [previewLoading, setPreviewLoading] = useState(false);
  const [form] = Form.useForm<FormValues>();
  const visibleBindings = showSmokeBindings
    ? bindings
    : bindings.filter((b) => !isSmokeBinding(b));
  const smokeHidden = bindings.length - visibleBindings.length;
  const visibleChannels = filterAgentId
    ? items.filter((c) => c.agent_id === filterAgentId || agents.find((a) => a.agent_id === filterAgentId)?.channel_id === c.id)
    : items;

  async function refresh() {
    setLoading(true);
    try {
      const [ch, bind, ag, apps, codes] = await Promise.all([
        listSyncChannels(session),
        listSyncBindings(session).catch(() => ({ items: [] as SyncBinding[] })),
        listAgents(session).catch(() => ({ items: [] as AgentAccount[] })),
        listApps(session).catch(() => ({ items: [] as { slug: string; name: string; status: string }[] })),
        listBindCodes(session).catch(() => ({ items: [] as BindCode[] })),
      ]);
      setItems(ch.items || []);
      setBindings(bind.items || []);
      setAgents(ag.items || []);
      setBindCodes(codes.items || []);
      setAppOptions(
        (apps.items || [])
          .filter((a) => a.status === "published" || !a.status)
          .map((a) => ({ value: a.slug, label: `${a.name || a.slug} (${a.slug})` }))
      );
    } catch (e: any) {
      const msg = e.message || String(e);
      setError(msg);
      message.error(msg);
    } finally {
      setLoading(false);
    }
  }

  useEffect(() => {
    void refresh();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [session.accessToken]);

  function openCreate() {
    setEditing(null);
    form.setFieldsValue({
      name: "本地 B ↔ 线上 A",
      direction: "local_to_remote",
      conflict_policy: "lww_source",
      poll_interval_ms: 500,
      agent_id: undefined,
      app_slug: undefined,
      local: { driver: "sqlite", dsn: "file:./data/local.db", tables: "article" },
      remote: {
        driver: "postgres",
        dsn: "postgres://user:pass@127.0.0.1:5432/app?sslmode=disable",
        tables: "article",
      },
      pk_columns: "article:id",
    });
    setOpen(true);
  }

  function openEdit(ch: SyncChannel) {
    setEditing(ch);
    form.setFieldsValue({
      name: ch.name,
      direction: ch.direction,
      conflict_policy: ch.conflict_policy,
      poll_interval_ms: ch.poll_interval_ms,
      agent_id: ch.agent_id || undefined,
      app_slug: ch.app_slug || undefined,
      local: {
        driver: ch.local.driver,
        dsn: ch.local.dsn,
        tables: (ch.local.tables || []).join(","),
      },
      remote: {
        driver: ch.remote.driver,
        dsn: ch.remote.dsn,
        tables: (ch.remote.tables || []).join(","),
      },
      pk_columns: Object.entries(ch.pk_columns || {})
        .map(([t, p]) => `${t}:${p}`)
        .join(","),
    });
    setOpen(true);
  }

  async function openInspect(ch: SyncChannel, side: "remote" | "local" = "remote") {
    setInspectCh(ch);
    setInspectSide(side);
    setInspectOpen(true);
    setInspectData(null);
    setInspectLoading(true);
    try {
      const data = await inspectSyncChannel(session, ch.id, { side, include_sync_meta: true });
      setInspectData(data);
    } catch (e: any) {
      message.error(e.message || String(e));
      setInspectOpen(false);
    } finally {
      setInspectLoading(false);
    }
  }

  async function openPreview(table: string) {
    if (!inspectCh) return;
    setPreviewLoading(true);
    setPreviewOpen(true);
    setPreviewData(null);
    try {
      const data = await previewSyncTable(session, inspectCh.id, {
        table,
        side: inspectSide,
        limit: 50,
      });
      setPreviewData(data);
    } catch (e: any) {
      message.error(e.message || String(e));
      setPreviewOpen(false);
    } finally {
      setPreviewLoading(false);
    }
  }

  function formatCheckpointTime(iso?: string) {
    if (!iso) return "—";
    const d = new Date(iso);
    if (Number.isNaN(d.getTime())) return iso;
    return d.toLocaleString();
  }

  async function openRestore(r: SyncChannel) {
    setBusy(true);
    try {
      const meta = await getSyncCheckpointMeta(session, r.id);
      if (!meta.latest && !meta.previous) {
        message.warning("尚无成功同步快照。请先完成一次同步（含自动推送），约 30 秒后生成。");
        return;
      }
      let which: "latest" | "previous" = meta.latest ? "latest" : "previous";
      const options: { value: "latest" | "previous"; label: string }[] = [];
      if (meta.latest) {
        options.push({
          value: "latest",
          label: `最近一次 · ${formatCheckpointTime(meta.latest.synced_at)}（${meta.latest.row_count ?? 0} 行 / ${meta.latest.table_count ?? 0} 表）`,
        });
      }
      if (meta.previous) {
        options.push({
          value: "previous",
          label: `上一代 · ${formatCheckpointTime(meta.previous.synced_at)}（${meta.previous.row_count ?? 0} 行 / ${meta.previous.table_count ?? 0} 表）`,
        });
      }
      Modal.confirm({
        title: `数据恢复 · ${r.name || r.id}`,
        width: 560,
        content: (
          <div>
            <p style={{ marginTop: 0 }}>
              将<strong>线上库</strong>恢复为所选同步快照（upsert 并删除快照中不存在的行）。本机数据需宇恒下次同步/pull 从线上补回。
            </p>
            <p style={{ color: "rgba(0,0,0,0.45)", fontSize: 13 }}>
              若误删后又发生成功自动同步，最新快照可能已含删除后状态，请改选「上一代」。
            </p>
            <div style={{ marginTop: 12 }}>
              <Typography.Text type="secondary">恢复到</Typography.Text>
              <Select
                style={{ width: "100%", marginTop: 6 }}
                defaultValue={which}
                options={options}
                onChange={(v) => {
                  which = v;
                }}
              />
            </div>
          </div>
        ),
        okText: "确认恢复",
        okType: "danger",
        cancelText: "取消",
        onOk: async () => {
          try {
            const res = await restoreSyncChannel(session, r.id, { which, confirm: true });
            message.success(
              `已恢复：写入 ${res.upserted ?? 0} 行，清理多余 ${res.deleted ?? 0} 行（快照 ${formatCheckpointTime(res.synced_at)}）`
            );
            setInfo("线上库已按同步快照恢复");
            await refresh();
          } catch (e: any) {
            message.error(e.message || String(e));
            throw e;
          }
        },
      });
    } catch (e: any) {
      message.error(e.message || String(e));
    } finally {
      setBusy(false);
    }
  }

  function confirmDropTable(table: string) {
    if (!inspectCh) return;
    const sideLabel = inspectSide === "remote" ? "线上库" : "本机端（通道 local）";
    Modal.confirm({
      title: `删除表「${table}」？`,
      content: (
        <div>
          <p>
            将从<strong>{sideLabel}</strong>删除该表及同步元数据残留。此操作不可恢复。
          </p>
          <p style={{ marginBottom: 0 }}>
            松离线<strong>不会</strong>把删表同步到另一侧。若本机仍有同名表，下次 push/ensure 可能再次出现在线上。
          </p>
        </div>
      ),
      okText: "确认删除",
      okType: "danger",
      cancelText: "取消",
      onOk: async () => {
        try {
          const res = await dropSyncTable(session, inspectCh.id, {
            table,
            side: inspectSide,
          });
          message.success(res.message || (res.dropped ? "已删除" : "表不存在"));
          if (previewData?.table === table) setPreviewOpen(false);
          await openInspect(inspectCh, inspectSide);
        } catch (e: any) {
          message.error(e.message || String(e));
          throw e;
        }
      },
    });
  }

  async function onSave() {
    const v = await form.validateFields();
    setBusy(true);
    try {
      const body = toChannelBody(v, editing?.id);
      const saved = editing
        ? await updateSyncChannel(session, editing.id, body)
        : await createSyncChannel(session, body);
      const id = (saved as SyncChannel)?.id || editing?.id;
      if (id) {
        try {
          await startSyncChannel(session, id);
          setInfo("同步通道已保存并启动");
        } catch {
          setInfo("同步通道已保存（启动失败时可手动点「启动」）");
        }
      } else {
        setInfo("同步通道已保存");
      }
      setOpen(false);
      await refresh();
    } catch (e: any) {
      message.error(e.message || String(e));
    } finally {
      setBusy(false);
    }
  }

  async function onTest() {
    const v = await form.validateFields();
    setBusy(true);
    try {
      const body = toChannelBody(v);
      const res = await testSyncEndpoints(session, {
        local: body.local!,
        remote: body.remote!,
        side: "both",
      });
      const lok = (res.local as any)?.ok;
      const rok = (res.remote as any)?.ok;
      if (lok && rok) message.success("本地与线上数据库均可连接");
      else message.warning(JSON.stringify(res));
    } catch (e: any) {
      message.error(e.message || String(e));
    } finally {
      setBusy(false);
    }
  }

  return (
    <div className="panel">
      <Typography.Title level={4} style={{ marginTop: 0 }}>
        数据同步
      </Typography.Title>
      <Typography.Paragraph type="secondary">
        仅本<strong>公司顶级权限（管理员）</strong>可配置通道 DSN / 线上库落点；通道按公司隔离。
        终端用户以 <Typography.Text code>Binding</Typography.Text> + 登录 JWT 为同步权限；
        库级选「同步」后该库<strong>全部表</strong>可上云，管理员<strong>不必</strong>按业务表维护通道白名单。
        通道可绑定<strong>智能体 + 模块</strong>：在「用户管理」为智能体填写同步通道/线上库/落库名后，可按智能体筛通道；
        模块发布（智能体 Token）优先写入绑定库。本机库管理模块表见宇恒侧（Z8f）。
        仅线上/首启可用 pull·bootstrap 从线上灌回本机。默认客户<strong>无感</strong>：未开通不走松离线。
        线上库 A 推荐 <Typography.Text code>postgres</Typography.Text>；多服务器每库一条通道。
      </Typography.Paragraph>

      <Alert
        type="info"
        showIcon
        style={{ marginBottom: 12 }}
        message="冲突已自动按 LWW 处理；覆盖审计请到「平台超管 · 租户/公司 · LWW 覆盖日志」查看。公司侧无冲突台。"
      />

      <Space style={{ marginBottom: 12 }} wrap>
        <Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
          新建通道
        </Button>
        <Button icon={<ReloadOutlined />} loading={loading} onClick={() => void refresh()}>
          刷新
        </Button>
        <Select
          allowClear
          placeholder="按智能体筛选通道"
          style={{ minWidth: 220 }}
          value={filterAgentId}
          onChange={(v) => setFilterAgentId(v)}
          options={agents.map((a) => ({
            value: a.agent_id,
            label: `${a.name}${a.channel_id ? " · 已绑通道" : ""}`,
          }))}
        />
      </Space>

      <Table
        rowKey="id"
        loading={loading}
        dataSource={visibleChannels}
        pagination={false}
        locale={{
          emptyText: filterAgentId
            ? "该智能体暂无同步通道"
            : "该公司暂无同步通道（未绑定智能体或尚未创建通道）",
        }}
        columns={[
          {
            title: "通道 ID",
            dataIndex: "id",
            width: 280,
            render: (id: string) => (
              <Typography.Text copyable={{ text: id }} style={{ fontSize: 12 }}>
                {id}
              </Typography.Text>
            ),
          },
          { title: "名称", dataIndex: "name" },
          {
            title: "方向",
            dataIndex: "direction",
            render: (d: string) => <Tag>{d}</Tag>,
          },
          {
            title: "本地",
            render: (_: unknown, r: SyncChannel) => `${r.local.driver}`,
          },
          {
            title: "线上",
            render: (_: unknown, r: SyncChannel) => {
              const title = channelOnlineTitle(r, bindings);
              const ids = channelOnlineIds(r, bindings);
              const path = formatDsnHint(r.remote?.dsn || "");
              return (
                <span>
                  <Typography.Text strong>{title}</Typography.Text>
                  <br />
                  <Typography.Text type="secondary" style={{ fontSize: 12 }}>
                    {r.remote?.driver}
                    {ids ? ` · ${ids}` : ""}
                    {path ? ` · ${path}` : ""}
                  </Typography.Text>
                </span>
              );
            },
          },
          {
            title: "智能体/模块",
            render: (_: unknown, r: SyncChannel) => {
              const ag = agents.find((a) => a.agent_id === r.agent_id);
              const app = appOptions.find((o) => o.value === r.app_slug);
              const moduleLabel = r.app_slug
                ? app
                  ? `${(app.label.split(" (")[0] || app.label).trim()}`
                  : r.app_slug
                : "未绑模块";
              return (
                <span>
                  {ag ? ag.name : r.agent_id ? `#${r.agent_id}` : "—"}
                  <br />
                  <Typography.Text type="secondary" style={{ fontSize: 11 }}>
                    {moduleLabel}
                    {r.app_slug && app ? (
                      <>
                        <br />
                        <span style={{ opacity: 0.75 }}>{r.app_slug}</span>
                      </>
                    ) : null}
                  </Typography.Text>
                </span>
              );
            },
          },
          {
            title: "状态",
            render: (_: unknown, r: SyncChannel) =>
              r.enabled ? <Tag color="green">运行中</Tag> : <Tag>已停止</Tag>,
          },
          {
            title: "推送次数",
            render: (_: unknown, r: SyncChannel) => (
              <span>
                ↑{r.stats?.pushed_ok || 0}
                {r.stats?.pushed_skipped ? `（跳过 ${r.stats.pushed_skipped}）` : ""}
                <br />
                <Typography.Text type="secondary" style={{ fontSize: 11 }}>
                  push 成功次数，非表数
                </Typography.Text>
              </span>
            ),
          },
          {
            title: "操作",
            render: (_: unknown, r: SyncChannel) => (
              <Space wrap>
                <Button size="small" icon={<TableOutlined />} onClick={() => void openInspect(r, "remote")}>
                  查看线上表
                </Button>
                <Button size="small" onClick={() => openEdit(r)}>
                  配置
                </Button>
                {r.enabled ? (
                  <Button
                    size="small"
                    icon={<PauseCircleOutlined />}
                    onClick={async () => {
                      await stopSyncChannel(session, r.id);
                      await refresh();
                    }}
                  >
                    停止
                  </Button>
                ) : (
                  <Button
                    size="small"
                    type="primary"
                    icon={<PlayCircleOutlined />}
                    onClick={async () => {
                      try {
                        await startSyncChannel(session, r.id);
                        setInfo("同步已启动");
                        await refresh();
                      } catch (e: any) {
                        message.error(e.message || String(e));
                      }
                    }}
                  >
                    启动
                  </Button>
                )}
                <Button
                  size="small"
                  onClick={async () => {
                    setBusy(true);
                    try {
                      const res = await reconcileSyncChannel(session, r.id);
                      const parts = (res.reports || []).map(
                        (x) =>
                          `${x.table}: 补推${x.patched_push} 补拉${x.patched_pull}（仅本地${(x.only_local || []).length} 仅线上${(x.only_remote || []).length}）`
                      );
                      message.success(parts.length ? parts.join("；") : "同步修复完成，无差异");
                      setInfo("同步修复完成");
                      await refresh();
                    } catch (e: any) {
                      const msg = e.message || String(e);
                      if (/过于频繁|429|Too Many/i.test(msg)) {
                        message.warning(msg || "对账过于频繁，请稍后再试");
                      } else {
                        message.error(msg);
                      }
                    } finally {
                      setBusy(false);
                    }
                  }}
                >
                  同步修复
                </Button>
                <Button size="small" onClick={() => void openRestore(r)}>
                  数据恢复
                </Button>
                <Button
                  size="small"
                  danger
                  onClick={() => {
                    Modal.confirm({
                      title: `删除通道「${r.name || r.id}」？`,
                      content: r.is_system_default
                        ? "这是公司默认同步通道。删除后将自动重建并重挂库绑定，账号落点不会丢失。"
                        : "删除后，仍挂在此通道上的绑定会自动改挂公司默认同步通道。",
                      okText: "确认删除",
                      okType: "danger",
                      cancelText: "取消",
                      onOk: async () => {
                        setBusy(true);
                        try {
                          const res = await deleteSyncChannel(session, r.id);
                          if (res.heal_error) {
                            message.warning(`通道已删，自愈未完成：${res.heal_error}`);
                          } else if (res.recreated_default) {
                            message.success("已删除；公司默认同步通道已自动重建并重挂绑定");
                          } else {
                            message.success("已删除");
                          }
                          await refresh();
                        } catch (e: any) {
                          message.error(e.message || String(e));
                          throw e;
                        } finally {
                          setBusy(false);
                        }
                      },
                    });
                  }}
                >
                  删除
                </Button>
              </Space>
            ),
          },
        ]}
      />

      <Typography.Title level={5} style={{ marginTop: 28 }}>
        绑定码（发给终端开通）
      </Typography.Title>
      <Typography.Paragraph type="secondary" style={{ marginTop: 0 }}>
        关联本公司默认同步落点；终端用 <Typography.Text code>POST /auth/bind-code/redeem</Typography.Text>{" "}
        兑换。生产联调成员手机请用 <Typography.Text code>13531041944</Typography.Text>，勿用超管号。
      </Typography.Paragraph>
      <Space style={{ marginBottom: 12 }}>
        <Button
          type="primary"
          disabled={busy}
          onClick={async () => {
            setBusy(true);
            try {
              const bc = await createBindCode(session, { max_uses: 1, expires_hours: 168 });
              message.success(`已生成绑定码 ${bc.code}`);
              setInfo(`绑定码 ${bc.code}（可复制发给用户）`);
              await refresh();
            } catch (e: any) {
              message.error(e.message || String(e));
            } finally {
              setBusy(false);
            }
          }}
        >
          生成绑定码
        </Button>
      </Space>
      <Table
        rowKey="code"
        loading={loading}
        dataSource={bindCodes}
        pagination={false}
        locale={{ emptyText: "暂无绑定码" }}
        columns={[
          {
            title: "绑定码",
            dataIndex: "code",
            render: (c: string) => <Typography.Text copyable code>{c}</Typography.Text>,
          },
          {
            title: "通道",
            dataIndex: "channel_id",
            render: (id: string) =>
              id ? <Typography.Text copyable={{ text: id }}>{id.slice(0, 8)}…</Typography.Text> : "—",
          },
          {
            title: "次数",
            render: (_: unknown, r: BindCode) => `${r.used_count}/${r.max_uses}`,
            width: 80,
          },
          {
            title: "过期",
            dataIndex: "expires_at",
            render: (t: string) => (t ? new Date(t).toLocaleString() : "—"),
          },
          {
            title: "状态",
            width: 90,
            render: (_: unknown, r: BindCode) => {
              if (r.revoked) return <Tag color="default">已撤销</Tag>;
              if (r.used_count >= r.max_uses) return <Tag>已用尽</Tag>;
              if (r.expires_at && new Date(r.expires_at).getTime() < Date.now())
                return <Tag color="warning">已过期</Tag>;
              return <Tag color="success">可用</Tag>;
            },
          },
          {
            title: "操作",
            width: 90,
            render: (_: unknown, r: BindCode) => (
              <Button
                size="small"
                danger
                disabled={!!r.revoked || busy}
                onClick={async () => {
                  setBusy(true);
                  try {
                    await revokeBindCode(session, r.code);
                    setInfo(`已撤销 ${r.code}`);
                    await refresh();
                  } catch (e: any) {
                    message.error(e.message || String(e));
                  } finally {
                    setBusy(false);
                  }
                }}
              >
                撤销
              </Button>
            ),
          },
        ]}
      />

      <Typography.Title level={5} style={{ marginTop: 28 }}>
        库绑定（本地 ↔ 线上）
      </Typography.Title>
      <Typography.Paragraph type="secondary" style={{ marginTop: 0 }}>
        <Typography.Text code>local_database_id</Typography.Text> 与{" "}
        <Typography.Text code>online_db_id</Typography.Text>{" "}
        <strong>本就可以不同</strong>（映射关系，不是要求同名）。
        可读名靠宇恒 ensure 时带 <Typography.Text code>database_name</Typography.Text> /{" "}
        <Typography.Text code>display_name</Typography.Text>
        ；通道「线上」列优先用本地可读名。默认隐藏联调烟雾 Binding（
        <Typography.Text code>local_*</Typography.Text> / note=smoke）。
      </Typography.Paragraph>
      <Space style={{ marginBottom: 8 }}>
        <Checkbox
          checked={showSmokeBindings}
          onChange={(e) => setShowSmokeBindings(e.target.checked)}
        >
          显示联调烟雾 Binding
          {smokeHidden > 0 ? `（已隐藏 ${smokeHidden} 条）` : ""}
        </Checkbox>
      </Space>
      <Table
        rowKey="id"
        loading={loading}
        dataSource={visibleBindings}
        pagination={false}
        locale={{ emptyText: "暂无 Binding（客户端选「同步」并登记后出现）" }}
        columns={[
          {
            title: "本地库名",
            render: (_: unknown, b: SyncBinding) => (
              <span>
                <Typography.Text strong>{bindingLocalLabel(b)}</Typography.Text>
                {b.database_name ? (
                  <>
                    <br />
                    <Typography.Text type="secondary" style={{ fontSize: 12 }}>
                      {b.local_database_id}
                    </Typography.Text>
                  </>
                ) : null}
              </span>
            ),
          },
          {
            title: "线上库名",
            render: (_: unknown, b: SyncBinding) => (
              <span>
                <Typography.Text strong>{bindingOnlineLabel(b)}</Typography.Text>
                {b.display_name ? (
                  <>
                    <br />
                    <Typography.Text type="secondary" style={{ fontSize: 12 }}>
                      {b.online_db_id}
                    </Typography.Text>
                  </>
                ) : null}
              </span>
            ),
          },
          {
            title: "映射",
            render: (_: unknown, b: SyncBinding) => (
              <Typography.Text style={{ fontSize: 13 }}>
                {bindingLocalLabel(b)} ↔ {bindingOnlineLabel(b)}
              </Typography.Text>
            ),
          },
          {
            title: "通道",
            dataIndex: "channel_id",
            render: (id: string) => {
              if (!id) return "—";
              const ch = items.find((c) => c.id === id);
              if (!ch) {
                return (
                  <Typography.Text type="danger" style={{ fontSize: 12 }}>
                    通道已删除 · {id.slice(0, 8)}…
                  </Typography.Text>
                );
              }
              const tables = (ch.remote?.tables || []).join(", ");
              return (
                <span>
                  {ch.name || id}
                  <br />
                  <Typography.Text type="secondary" style={{ fontSize: 11 }}>
                    {tables
                      ? `策略表（参考，非整库限制）：${tables}`
                      : "整库同步中（Binding 任意表可 push）"}
                  </Typography.Text>
                </span>
              );
            },
          },
          { title: "备注", dataIndex: "note", ellipsis: true },
          {
            title: "共享",
            width: 70,
            render: (_: unknown, b: SyncBinding) =>
              b.shared ? <Tag color="blue">共享</Tag> : <Typography.Text type="secondary">个人</Typography.Text>,
          },
        ]}
      />

      <Typography.Paragraph type="secondary" style={{ marginTop: 16 }}>
        「同步修复」有最小间隔限流（默认 5 分钟）。
        ↑ 推送次数 = agent push 成功次数，不是业务表数量。
        本地有、线上没有的表（如尚未 push 的业务表）属正常，不是 Binding 映射错了。
        Binding 标 <Typography.Text code>shared=true</Typography.Text> 为公司共享库（仅管理员可设）。
      </Typography.Paragraph>

      <Modal
        title={
          inspectCh
            ? `线上库表 · ${channelOnlineTitle(inspectCh, bindings)}${inspectSide === "local" ? "（本机端）" : ""}`
            : "库表"
        }
        open={inspectOpen}
        onCancel={() => setInspectOpen(false)}
        width={820}
        footer={
          <Space>
            <Button
              loading={inspectLoading}
              onClick={() => inspectCh && void openInspect(inspectCh, inspectSide === "remote" ? "local" : "remote")}
            >
              切换到{inspectSide === "remote" ? "本机端" : "线上"}
            </Button>
            <Button
              loading={inspectLoading}
              onClick={() => inspectCh && void openInspect(inspectCh, inspectSide)}
            >
              刷新
            </Button>
            <Button type="primary" onClick={() => setInspectOpen(false)}>
              关闭
            </Button>
          </Space>
        }
      >
        {inspectData?.dsn_hint ? (
          <Typography.Paragraph type="secondary" style={{ marginTop: 0 }}>
            {inspectData.driver} · {inspectData.dsn_hint}
            {inspectData.message ? ` · ${inspectData.message}` : ""}
          </Typography.Paragraph>
        ) : null}
        <Table
          rowKey="name"
          loading={inspectLoading}
          dataSource={(inspectData?.tables || []).filter((t) => !String(t.name || "").startsWith("_ajz_"))}
          pagination={false}
          size="small"
          locale={{ emptyText: "暂无业务表（尚未 push 或连不上库）" }}
          columns={[
            { title: "表名", dataIndex: "name" },
            {
              title: "行数",
              dataIndex: "row_count",
              width: 90,
              render: (n: number) => <Typography.Text strong>{n}</Typography.Text>,
            },
            {
              title: "字段数",
              dataIndex: "column_count",
              width: 90,
            },
            {
              title: "字段",
              dataIndex: "columns",
              ellipsis: true,
              render: (cols: string[]) => (cols || []).join(", "),
            },
            {
              title: "操作",
              width: 168,
              render: (_: unknown, t: { name: string; row_count: number }) => (
                <Space size={4}>
                  <Button size="small" icon={<EyeOutlined />} onClick={() => void openPreview(t.name)}>
                    预览
                  </Button>
                  <Button
                    size="small"
                    danger
                    icon={<DeleteOutlined />}
                    onClick={() => confirmDropTable(t.name)}
                  >
                    删表
                  </Button>
                </Space>
              ),
            },
          ]}
        />
        <Typography.Paragraph type="secondary" style={{ marginTop: 12, marginBottom: 0 }}>
          {(inspectData?.tables || []).some((t) => String(t.name || "").startsWith("_ajz_")) ? (
            <>
              已隐藏同步系统表 <Typography.Text code>_ajz_*</Typography.Text>
              （版本/outbox，不是业务模块表）。
            </>
          ) : null}
          本机有、这里没有的表 = 尚未 push/ensure。「删表」只删当前查看侧，不会同步到另一侧。
        </Typography.Paragraph>
      </Modal>

      <Modal
        title={previewData ? `预览 · ${previewData.table}（共 ${previewData.total} 行，显示前 ${previewData.limit}）` : "预览"}
        open={previewOpen}
        onCancel={() => setPreviewOpen(false)}
        width={960}
        footer={
          <Button type="primary" onClick={() => setPreviewOpen(false)}>
            关闭
          </Button>
        }
      >
        <Table
          loading={previewLoading}
          dataSource={(previewData?.rows || []).map((row, i) => ({ ...row, __k: i }))}
          rowKey="__k"
          size="small"
          scroll={{ x: true }}
          pagination={false}
          columns={(previewData?.columns || []).map((c) => ({
            title: c,
            dataIndex: c,
            ellipsis: true,
            render: (v: unknown) =>
              v === null || v === undefined ? (
                <Typography.Text type="secondary">null</Typography.Text>
              ) : (
                String(v)
              ),
          }))}
        />
      </Modal>

      <Modal
        title={editing ? "编辑同步通道" : "新建同步通道"}
        open={open}
        onCancel={() => setOpen(false)}
        width={720}
        footer={
          <Space>
            <Button icon={<ApiOutlined />} loading={busy} onClick={() => void onTest()}>
              测试连接
            </Button>
            <Button onClick={() => setOpen(false)}>取消</Button>
            <Button type="primary" loading={busy} onClick={() => void onSave()}>
              保存
            </Button>
          </Space>
        }
      >
        <Form form={form} layout="vertical">
          <Form.Item name="name" label="名称" rules={[{ required: true }]}>
            <Input />
          </Form.Item>
          <Form.Item
            name="agent_id"
            label="所属智能体"
            extra="哪个智能体的库就选哪个；模块数据应落在该智能体对应线上库"
          >
            <Select
              allowClear
              placeholder="可选"
              options={agents.map((a) => ({
                value: a.agent_id,
                label: `${a.name} (#${a.agent_id})`,
              }))}
            />
          </Form.Item>
          <Form.Item
            name="app_slug"
            label="关联模块"
            extra="已发布模块；本地上传/智能体管理的模块数据与此对照"
          >
            <Select allowClear showSearch placeholder="可选" options={appOptions} />
          </Form.Item>
          <Form.Item name="direction" label="方向" rules={[{ required: true }]}>
            <Select
              options={[
                { value: "local_to_remote", label: "本地 → 线上" },
                { value: "remote_to_local", label: "线上 → 本地" },
                { value: "bidirectional", label: "双向" },
              ]}
            />
          </Form.Item>
          <Form.Item
            name="conflict_policy"
            label="冲突策略"
            extra="自动 LWW；覆盖日志仅平台超级管理员可查，公司侧无冲突台"
          >
            <Select
              options={[
                { value: "lww_source", label: "源端覆盖（推荐，B→A）" },
                { value: "lww_target", label: "保留目标" },
                { value: "queue", label: "入队（调试用，租户不可见）" },
              ]}
            />
          </Form.Item>
          <Form.Item name="poll_interval_ms" label="轮询间隔(ms)">
            <InputNumber min={100} max={60000} style={{ width: "100%" }} />
          </Form.Item>

          <Typography.Text strong>本地库</Typography.Text>
          <Form.Item name={["local", "driver"]} label="驱动" rules={[{ required: true }]}>
            <Select options={[{ value: "sqlite" }, { value: "mysql" }, { value: "postgres" }]} />
          </Form.Item>
          <Form.Item
            name={["local", "dsn"]}
            label="DSN"
            rules={[{ required: true }]}
            extra="SQLite 例：file:./data/local.db"
          >
            <Input.TextArea rows={2} />
          </Form.Item>
          <Form.Item
            name={["local", "tables"]}
            label="策略表（参考）"
            rules={[{ required: true }]}
            extra="Z4：不按此名单拒收；用户 JWT + Binding 下任意表可 push。此处仅运维提示。"
          >
            <Input placeholder="article,order" />
          </Form.Item>

          <Typography.Text strong>线上库 A（生产统一 Postgres）</Typography.Text>
          <Form.Item
            name={["remote", "driver"]}
            label="驱动"
            rules={[{ required: true }]}
            extra="生产推荐 postgres；mysql 兼容存量；sqlite 仅联调临时 A，不宜多 writer"
          >
            <Select
              options={[
                { value: "postgres", label: "postgres（推荐）" },
                { value: "mysql", label: "mysql" },
                { value: "sqlite", label: "sqlite（仅联调）" },
              ]}
            />
          </Form.Item>
          <Form.Item
            name={["remote", "dsn"]}
            label="线上 DSN"
            rules={[{ required: true }]}
            extra="Postgres 例：postgres://user:pass@127.0.0.1:5432/dbname?sslmode=disable"
          >
            <Input.TextArea rows={2} />
          </Form.Item>
          <Form.Item
            name={["remote", "tables"]}
            label="策略表（参考）"
            rules={[{ required: true }]}
            extra="非整库限制；与本地提示一致即可。验同步请用「查看线上表」"
          >
            <Input />
          </Form.Item>
          <Form.Item
            name="pk_columns"
            label="主键映射"
            extra="格式 table:pk，多个用逗号。默认每表 id"
          >
            <Input placeholder="article:id" />
          </Form.Item>
        </Form>
      </Modal>
    </div>
  );
}
