/* WAREHOUSE 2.0 · 超級終端 — ADMIN 權力面(允許直接調用管理端點,全程審計)
   端點照抄 1.0 frontend/page-terminal.jsx 與 scripts/ai_service.py:
   /api/cli/exec {line} → {ok,command,status,elapsed_ms,writes,risk,data|error,usage,hint}
   /api/agent/run/stream NDJSON → run_start / step_start / step / final
   /api/admin/sql {sql,force} → read:{rows,count,truncated} write:{affected,backup} 危險→needs_confirm
   /api/cli/commands → {commands:[{command,usage,description,permission,writes,allowed}]} */
(() => {
const W2 = window.W2;
const { t } = window.W2_LANG;
window.W2_LANG.addEN({
  "超級終端": "Super Terminal",
  "人與 AI 同一指令集 · 同一權限 · 同一審計 — 動作直達後端,非只讀駕駛艙": "Humans and AI share one command set, one permission model, one audit trail — actions hit the backend directly; not a read-only cockpit",
  "ADMIN — 權力面 · 全程審計": "ADMIN — POWER SURFACE · FULLY AUDITED",
  "以你的權限執行 · 全程審計": "RUNS WITH YOUR PERMISSIONS · FULLY AUDITED",
  "終端以你的賬號權限執行:能做什麼由權限決定,超權指令會被後端拒絕;每個動作全程審計。": "The terminal runs with your account's permissions: what you can do is decided by them, over-privileged commands are rejected by the backend, and every action is fully audited.",
  "拒絕訪問": "Access denied",
  "此頁是 ADMIN 權力面:指令直達管理端點、可寫庫、可執行 SQL。需要平台所有者或 L5 系統管理員身份。": "This page is an ADMIN power surface: commands hit management endpoints directly, can write the database and run SQL. Platform owner or L5 system-administrator identity is required.",
  "當前帳號 {name} · 最高權限 L{n}": "Current account {name} · highest level L{n}",
  "如需操作,請聯繫系統管理員開通,或改用秘書對話完成業務。": "To proceed, ask a system administrator for access, or use the Secretary chat for business operations.",
  "AI 會話": "AI Chat",
  "快捷命令": "Quick commands",
  "點擊填入輸入行,不會自動執行": "Click to fill the input line — never auto-runs",
  "清屏": "Clear",
  "CLI 指令模式 — 輸入平臺指令;help 全部指令;Tab 補全;↑↓ 歷史;clear 清屏": "CLI mode — type platform commands; help lists all; Tab completes; ↑↓ history; clear wipes the screen",
  "AI 會話模式 — 說自然語言,內核逐步執行,每一步可見;new 開新對話;!指令直通": "AI chat mode — speak naturally, the kernel executes step by step, every step visible; new resets context; ! passes a raw command through",
  "SQL 模式 — 直查當前公司庫;SELECT 限 500 行;危險語句需二次確認(sql!)": "SQL mode — query the company database directly; SELECT capped at 500 rows; dangerous statements need a second confirmation (sql!)",
  "ADMIN 權力面:每個動作即時生效並全程審計,請謹慎執行。": "ADMIN power surface: every action takes effect immediately and is fully audited. Proceed with care.",
  "已切換到 CLI 指令模式:輸入平臺指令,help 查看全部。": "Switched to CLI mode: type platform commands, help lists them all.",
  "已進入 AI 會話模式:直接說要做什麼(多輪連續);!開頭直通指令;new 開新對話。": "AI chat mode: just say what to do (multi-turn); prefix ! to pass a raw command; new starts a fresh conversation.",
  "已進入 SQL 模式:輸入 SQL 直接執行(僅系統管理員,全程審計;SELECT 限 500 行;危險語句需二次確認)。": "SQL mode: statements run directly (admins only, fully audited; SELECT capped at 500 rows; dangerous statements need a second confirmation).",
  "已退出 AI 會話模式,回到 CLI。": "Left AI chat mode, back to CLI.",
  "已開啟新對話(上下文已重置)。": "New conversation started (context reset).",
  "執行失敗": "Command failed",
  "無法連接指令路由器:": "Cannot reach the command router: ",
  "AI 會話失敗:": "AI chat failed: ",
  "SQL 失敗:": "SQL failed: ",
  "用法:": "Usage: ",
  "已寫庫(已審計)": "wrote DB (audited)",
  "第 {n} 步": "step {n}",
  "執行中…": "running…",
  "(本次未調用任何工具)": "(no tools were called this run)",
  "查詢 · {n} 行": "query · {n} rows",
  "(僅顯示前 500)": "(first 500 shown)",
  "(空結果集)": "(empty result set)",
  "寫操作完成 · 影響 {n} 行(已提交,已審計)": "write completed · {n} rows affected (committed, audited)",
  "已自動備份:": "auto-backup: ",
  "危險 SQL": "DANGEROUS SQL",
  "再按一次 Enter 確認執行;Esc 取消": "press Enter again to execute; Esc cancels",
  "已取消危險 SQL。": "Dangerous SQL cancelled.",
  "sql! 強制標記——執行前仍需確認": "sql! force flag — confirmation still required before it runs",
  "僅 L5 系統管理員可用 SQL。": "SQL is available to L5 system administrators only.",
  "輸入平臺指令…(help 全部指令 · Tab 補全 · ↑↓ 歷史)": "Type a platform command… (help lists all · Tab completes · ↑↓ history)",
  "跟內核說要做什麼…(!指令直通 · new 開新對話)": "Tell the kernel what to do… (! passes a raw command · new resets context)",
  "輸入 SQL…(SELECT 限 500 行 · 危險語句需二次確認)": "Type SQL… (SELECT capped at 500 rows · dangerous statements need confirmation)",
  "確認執行:再按一次 Enter;Esc 取消": "Confirm: press Enter again; Esc cancels",
  "AI 智能引擎": "AI engine",
  "規則引擎(非 AI)": "rule engine (non-AI)",
  "寫": "W",
  "需 {p} 權限": "needs {p}",
  "共 {n} 行": "{n} rows total",
  "僅顯示前 {n} 行": "first {n} shown",
  "列已截斷至 {n}": "columns truncated to {n}",
  "展開全部 {n} 行": "Expand all {n} rows",
  "展開全部 {n} 條": "Expand all {n} entries",
  "展開全部 {n} 行輸出": "Expand all {n} output lines",
  "收起": "Collapse",
  "(無返回數據)": "(no data returned)",
  "可用 runs show --id {n} 回看": "replay with runs show --id {n}",
  "… 執行中": "… running",
  "… 內核工作中": "… kernel working",
  "今天倉庫整體情況怎麼樣?": "How is the warehouse doing today?",
  "把低庫存物資列出來,給出補貨建議": "List low-stock items and suggest restocking",
  "最近的審計日誌有沒有異常?": "Any anomalies in the recent audit log?",
  "本地指令:help 指令一覽 · clear 清屏": "Local commands: help lists commands · clear wipes the screen",
  "已確認,強制執行。": "Confirmed — executing with force.",
});

const { useState: _s, useEffect: _e, useMemo: _mm } = React;
const { Icon: I, Btn: B, Tag: T, Label: LB, Folio } = W2;

/* ── 墨面板配色(紙墨紅紀律,僅此頁的暗面)── */
const MONO = { fontFamily: "var(--f-mono)" };
const DK = {
  paper: "var(--paper)",
  dim: "var(--ink-4)",
  dimmer: "var(--ink-3)",
  red: "var(--red)",
  hair: "rgba(245,242,235,.20)",
  hairSoft: "rgba(245,242,235,.10)",
};
const DBTN = { ...MONO, fontSize: 9.5, letterSpacing: ".12em", color: DK.dim,
  border: "1px solid " + DK.hair, background: "transparent", padding: "3px 10px" };

const S = (v) => (v === null || v === undefined || v === "") ? "—" : String(v);
const promptOf = (m) => m === "ai" ? "ai>" : m === "sql" ? "sql>" : "$";
/* 結果裡常見的「行集」:{rows:[...]} 或 data 本身是對象數組(照抄 1.0 termRowsOf) */
const rowsOf = (data) => {
  if (Array.isArray(data)) return data.length && data.every((r) => r && typeof r === "object" && !Array.isArray(r)) ? data : null;
  if (data && Array.isArray(data.rows)) return data.rows;
  return null;
};

/* ── mono 表(行數組;>12 行摺疊)── */
const TTable = ({ rows }) => {
  const [open, setOpen] = _s(false);
  const MAXC = 9, LIM = 12, HARD = 200;
  const allCols = Object.keys(rows[0] || {});
  const cols = allCols.slice(0, MAXC);
  const shown = rows.slice(0, open ? HARD : LIM);
  return (
    <div style={{ margin: "6px 0 2px" }}>
      <div style={{ overflowX: "auto" }}>
        <table style={{ borderCollapse: "collapse", whiteSpace: "nowrap", ...MONO, fontSize: 11.5 }}>
          <thead><tr>{cols.map((c) => (
            <th key={c} style={{ textAlign: "left", padding: "3px 16px 4px 0", fontSize: 9, fontWeight: 600,
              letterSpacing: ".14em", textTransform: "uppercase", color: DK.dim, borderBottom: "1px solid " + DK.hair }}>{c}</th>
          ))}</tr></thead>
          <tbody>{shown.map((r, i) => (
            <tr key={i}>{cols.map((c) => (
              <td key={c} style={{ padding: "3px 16px 3px 0", color: DK.paper, borderBottom: "1px solid " + DK.hairSoft }}>
                {r[c] === null || r[c] === undefined ? "—" : String(r[c])}
              </td>
            ))}</tr>
          ))}</tbody>
        </table>
      </div>
      <div className="row g10" style={{ marginTop: 5 }}>
        <span style={{ ...MONO, fontSize: 9.5, letterSpacing: ".08em", color: DK.dimmer }}>
          {t("共 {n} 行", { n: rows.length })}
          {!open && rows.length > LIM ? " · " + t("僅顯示前 {n} 行", { n: LIM }) : ""}
          {allCols.length > MAXC ? " · " + t("列已截斷至 {n}", { n: MAXC }) : ""}
        </span>
        {rows.length > LIM && (
          <button style={DBTN} onClick={() => setOpen(v => !v)}>
            {open ? "▴ " + t("收起") : "▾ " + t("展開全部 {n} 行", { n: Math.min(rows.length, HARD) })}
          </button>
        )}
      </div>
    </div>
  );
};

/* ── help 指令表(>12 條摺疊)── */
const THelp = ({ commands }) => {
  const [open, setOpen] = _s(false);
  const LIM = 12;
  const list = open ? commands : commands.slice(0, LIM);
  return (
    <div style={{ margin: "6px 0 2px" }}>
      {list.map((c) => (
        <div key={c.command} style={{ marginBottom: 7, opacity: c.allowed === false ? .45 : 1 }}>
          <span style={{ ...MONO, fontSize: 12, fontWeight: 700, color: DK.paper }}>{c.usage || c.command}</span>
          {c.writes && <span style={{ ...MONO, fontSize: 8.5, letterSpacing: ".1em", color: "#fff", background: DK.red, padding: "1px 5px", marginLeft: 8 }}>{t("寫")}</span>}
          {c.allowed === false && <span style={{ ...MONO, fontSize: 9.5, color: DK.dimmer, marginLeft: 8 }}>{t("需 {p} 權限", { p: c.permission || "—" })}</span>}
          <div style={{ fontSize: 11.5, color: DK.dim }}>{c.description || ""}</div>
        </div>
      ))}
      <div className="row g10">
        <span style={{ ...MONO, fontSize: 9.5, letterSpacing: ".08em", color: DK.dimmer }}>{t("本地指令:help 指令一覽 · clear 清屏")}</span>
        {commands.length > LIM && (
          <button style={DBTN} onClick={() => setOpen(v => !v)}>
            {open ? "▴ " + t("收起") : "▾ " + t("展開全部 {n} 條", { n: commands.length })}
          </button>
        )}
      </div>
    </div>
  );
};

/* ── 長文本 / JSON(>14 行摺疊)── */
const TPre = ({ text }) => {
  const [open, setOpen] = _s(false);
  const str = String(text == null ? "" : text);
  const lines = str.split("\n");
  const LIM = 14;
  const shown = open || lines.length <= LIM ? str : lines.slice(0, LIM).join("\n");
  return (
    <div style={{ margin: "4px 0 2px" }}>
      <pre style={{ ...MONO, fontSize: 11.5, lineHeight: 1.6, whiteSpace: "pre-wrap", wordBreak: "break-word", color: DK.paper, margin: 0 }}>{shown}</pre>
      {lines.length > LIM && (
        <button style={{ ...DBTN, marginTop: 4 }} onClick={() => setOpen(v => !v)}>
          {open ? "▴ " + t("收起") : "▾ " + t("展開全部 {n} 行輸出", { n: lines.length })}
        </button>
      )}
    </div>
  );
};

/* ── AI 步驟行(step_start → step 原位落定)── */
const StepLine = ({ step }) => (
  <div style={{ ...MONO, fontSize: 11, letterSpacing: ".03em",
    color: step.running ? DK.dim : step.ok === false ? DK.red : DK.paper, padding: "1px 0" }}>
    {step.running ? "…" : step.ok === false ? "✗" : "✓"} {t("第 {n} 步", { n: S(step.step_no) })}{" "}
    <b>{step.command || step.tool_name || "—"}</b>
    {step.args && Object.keys(step.args).length ? <span style={{ color: DK.dimmer }}> {JSON.stringify(step.args).slice(0, 120)}</span> : null}
    {step.running ? " · " + t("執行中…") : (typeof step.duration_ms === "number" ? ` · ${step.duration_ms}ms` : "")}
    {!step.running && step.error ? <span style={{ color: DK.red }}> · {step.error}</span> : null}
  </div>
);

/* ── AI final:紙面小島 + mdToHtml 富文本 ── */
const AiFinal = ({ item }) => {
  const html = W2.mdToHtml ? W2.mdToHtml(item.message) : null;
  return (
    <div style={{ margin: "8px 0 4px" }}>
      <div style={{ background: "var(--paper)", color: "var(--ink)", padding: "12px 14px", borderLeft: "3px solid var(--red)", maxWidth: 880 }}>
        {html != null
          ? <div className="md" style={{ fontSize: 12.5, lineHeight: 1.65, wordBreak: "break-word" }} dangerouslySetInnerHTML={{ __html: html }}/>
          : <div style={{ whiteSpace: "pre-wrap", fontSize: 12.5, lineHeight: 1.65 }}>{S(item.message)}</div>}
      </div>
      <div style={{ ...MONO, fontSize: 9.5, letterSpacing: ".06em", color: DK.dimmer, marginTop: 4 }}>
        run #{S(item.run_id)} · {item.engine === "deepseek" ? t("AI 智能引擎") : t("規則引擎(非 AI)")} · {S(item.status)}
        {item.run_id != null ? " · " + t("可用 runs show --id {n} 回看", { n: item.run_id }) : ""}
      </div>
    </div>
  );
};

/* runs show 實際返回 {run:{id,engine,status,final_message…}, steps:[DB行]}(ok=0/1、args_json 為字串);
   非流式 ai 信封則是頂層 {run_id, steps, message…}。兩種都歸一成 RunBlock 形狀,回放才不會退化成 JSON 傾印 */
const normDbStep = (s) => {
  let args = s.args;
  if (args == null && typeof s.args_json === "string") { try { args = JSON.parse(s.args_json); } catch (e) { args = null; } }
  return { ...s, args, ok: !(s.ok === 0 || s.ok === false) };
};
const runShapeOf = (data) => {
  if (!data) return null;
  if (Array.isArray(data.steps) && data.run_id != null) return data;
  if (data.run && typeof data.run === "object" && Array.isArray(data.steps))
    return { run_id: data.run.id, engine: data.run.engine, status: data.run.status,
             message: data.run.final_message, steps: data.steps.map(normDbStep) };
  return null;
};

/* ── 一次 run 的整體結果(runs show 等)── */
const RunBlock = ({ data }) => (
  <div style={{ margin: "4px 0" }}>
    {(data.steps || []).map((s, i) => <StepLine key={i} step={{ ...s, running: false }}/>)}
    {!(data.steps || []).length && <div style={{ ...MONO, fontSize: 11, color: DK.dimmer }}>{t("(本次未調用任何工具)")}</div>}
    {data.message && <div style={{ color: DK.paper, whiteSpace: "pre-wrap", fontSize: 12.5, marginTop: 6 }}>{String(data.message)}</div>}
    {data.run_id != null && (
      <div style={{ ...MONO, fontSize: 9.5, color: DK.dimmer, marginTop: 4 }}>
        run #{data.run_id} · {data.engine === "deepseek" ? t("AI 智能引擎") : t("規則引擎(非 AI)")} · {S(data.status)}
      </div>
    )}
  </div>
);

/* ── CLI 信封結果 ── */
const ResBlock = ({ env }) => {
  const data = env.data;
  const rows = rowsOf(data);
  const run = runShapeOf(data);
  return (
    <div>
      <div style={{ ...MONO, fontSize: 10, letterSpacing: ".08em", color: DK.dim }}>
        ✓ {S(env.command)}
        {typeof env.status === "number" ? ` · ${env.status}` : ""}
        {typeof env.elapsed_ms === "number" ? ` · ${env.elapsed_ms}ms` : ""}
        {env.writes ? " · " + t("已寫庫(已審計)") : ""}
      </div>
      {data && Array.isArray(data.commands) ? <THelp commands={data.commands}/>
        : run ? <RunBlock data={run}/>
        : rows ? (rows.length ? <TTable rows={rows}/> : <div style={{ ...MONO, fontSize: 11, color: DK.dimmer }}>{t("(空結果集)")}</div>)
        : typeof (data && (data.message || data.reply)) === "string"
          ? <div style={{ color: DK.paper, whiteSpace: "pre-wrap", fontSize: 12.5, margin: "4px 0" }}>{data.message || data.reply}</div>
        : data == null ? <div style={{ ...MONO, fontSize: 11, color: DK.dimmer }}>{t("(無返回數據)")}</div>
        : <TPre text={JSON.stringify(data, null, 2)}/>}
    </div>
  );
};

/* ── SQL 結果 ── */
const SqlBlock = ({ result }) => {
  const r = result || {};
  if (r.mode === "read") return (
    <div>
      <div style={{ ...MONO, fontSize: 10, letterSpacing: ".08em", color: DK.dim }}>
        ✓ {t("查詢 · {n} 行", { n: S(r.count) })}{r.truncated ? " " + t("(僅顯示前 500)") : ""}
      </div>
      {(r.rows || []).length ? <TTable rows={r.rows}/> : <div style={{ ...MONO, fontSize: 11, color: DK.dimmer }}>{t("(空結果集)")}</div>}
    </div>
  );
  return (
    <div>
      <div style={{ ...MONO, fontSize: 11, color: DK.paper }}>✓ {t("寫操作完成 · 影響 {n} 行(已提交,已審計)", { n: S(r.affected) })}</div>
      {r.backup && <div style={{ ...MONO, fontSize: 9.5, color: DK.dimmer, marginTop: 2 }}>{t("已自動備份:")}{r.backup}</div>}
    </div>
  );
};

/* ── 單條輸出塊 ── */
const TBlock = ({ item }) => {
  if (item.k === "cmd") return (
    <div style={{ marginTop: 14 }}>
      <span style={{ ...MONO, fontSize: 12, fontWeight: 700, background: "var(--paper)", color: "var(--ink)", padding: "2px 9px", wordBreak: "break-all" }}>
        {promptOf(item.mode)} {item.text}
      </span>
    </div>
  );
  if (item.k === "info") return <div style={{ fontSize: 12, color: item.red ? DK.red : DK.dim, marginTop: 3, whiteSpace: "pre-wrap" }}>{item.text}</div>;
  if (item.k === "err") return (
    <div style={{ marginTop: 3 }}>
      <div style={{ fontSize: 12.5, fontWeight: 650, color: DK.red, whiteSpace: "pre-wrap", wordBreak: "break-word" }}>✗ {item.text}</div>
      {item.usage && <div style={{ ...MONO, fontSize: 11, color: DK.dim }}>{t("用法:")}{item.usage}</div>}
      {item.hint && <div style={{ fontSize: 11, color: DK.dimmer }}>{item.hint}</div>}
    </div>
  );
  if (item.k === "res") return <ResBlock env={item.env}/>;
  if (item.k === "sql") return <SqlBlock result={item.result}/>;
  if (item.k === "aistep") return <StepLine step={item.step}/>;
  if (item.k === "aifinal") return <AiFinal item={item}/>;
  return null;
};

/* ── 歡迎屏(mono 藝術字 + 使用說明)── */
const ART = [
  "█   █   ███    ████",
  "█   █  █   █  █    ",
  "█ █ █  █   █   ███ ",
  "█ █ █  █   █      █",
  " █ █    ███   ████ ",
].join("\n");
const Welcome = ({ showSql }) => (
  <div style={{ paddingBottom: 6 }}>
    <pre style={{ fontFamily: "ui-monospace, Consolas, Menlo, monospace", fontSize: 11, lineHeight: 1.25, color: DK.paper, margin: 0 }}>{ART}</pre>
    <div style={{ width: 72, height: 8, background: DK.red, margin: "10px 0 12px" }}/>
    <div style={{ ...MONO, fontSize: 10, letterSpacing: ".2em", color: DK.dim, marginBottom: 12 }}>WAREHOUSE OS — SUPER TERMINAL · 2.0</div>
    <div className="col g6" style={{ fontSize: 12, color: DK.dim, maxWidth: 760, lineHeight: 1.7 }}>
      <div><span style={{ ...MONO, fontWeight: 700, color: DK.paper }}>$&nbsp;&nbsp;&nbsp;</span>{t("CLI 指令模式 — 輸入平臺指令;help 全部指令;Tab 補全;↑↓ 歷史;clear 清屏")}</div>
      <div><span style={{ ...MONO, fontWeight: 700, color: DK.paper }}>ai&gt;&nbsp;</span>{t("AI 會話模式 — 說自然語言,內核逐步執行,每一步可見;new 開新對話;!指令直通")}</div>
      {showSql && <div><span style={{ ...MONO, fontWeight: 700, color: DK.paper }}>sql&gt;</span> {t("SQL 模式 — 直查當前公司庫;SELECT 限 500 行;危險語句需二次確認(sql!)")}</div>}
      <div style={{ color: DK.red, fontWeight: 650 }}>{t("終端以你的賬號權限執行:能做什麼由權限決定,超權指令會被後端拒絕;每個動作全程審計。")}</div>
    </div>
  </div>
);

/* ── 快捷命令(點擊填入輸入行,不自動執行;提煉自 1.0 指令集)── */
const CHIPS = {
  cli: ["help", "whoami", "inv list", "alert list", "report summary", "wf inbox",
        "erp overview", "fin trial-balance", "audit logs", "users list", "ai health"],
  ai: ["今天倉庫整體情況怎麼樣?", "把低庫存物資列出來,給出補貨建議", "最近的審計日誌有沒有異常?"],
  sql: ["SELECT name FROM sqlite_master WHERE type='table' ORDER BY name",
        "SELECT * FROM audit_logs ORDER BY id DESC LIMIT 20",
        "SELECT COUNT(*) AS n FROM items"],
};

/* ── 拒絕面 ── */
const Denied = ({ name, lvl }) => (
  <div className="rise" style={{ borderTop: "2px solid var(--rule)", marginTop: 0 }}>
    <div style={{ maxWidth: 560, margin: "70px auto", border: "1px solid var(--red)", padding: "34px 36px" }}>
      <LB red style={{ marginBottom: 14 }}>ACCESS DENIED</LB>
      <div style={{ fontSize: 30, fontWeight: 800, letterSpacing: "-.03em", marginBottom: 14 }}>{t("拒絕訪問")}</div>
      <div style={{ fontSize: 13, lineHeight: 1.75, color: "var(--ink-2)", marginBottom: 14 }}>
        {t("此頁是 ADMIN 權力面:指令直達管理端點、可寫庫、可執行 SQL。需要平台所有者或 L5 系統管理員身份。")}
      </div>
      <div className="num" style={{ fontSize: 12.5, fontWeight: 650, marginBottom: 6 }}>
        {t("當前帳號 {name} · 最高權限 L{n}", { name: name || "—", n: lvl })}
      </div>
      <div style={{ fontSize: 12, color: "var(--ink-3)", lineHeight: 1.7 }}>
        {t("如需操作,請聯繫系統管理員開通,或改用秘書對話完成業務。")}
      </div>
      <div className="label dim" style={{ marginTop: 18 }}>REQUIRED — OWNER / L5</div>
    </div>
  </div>
);

/* ── 頁面 ── */
const Page = ({ isOwner }) => {
  const u = window.W2_USER || {};
  const lvl = Math.max(0, ...((u.roles || []).map(r => Number(r.level) || 0)));
  const owner = !!window.W2_IS_OWNER || !!isOwner;
  // 終端全員可用:指令按賬號權限過濾(後端 /api/cli/exec 天然把關,無權指令變暗標註);
  // 僅 SQL 模式保留 L11 平台超級管理員門(後端 admin/sql 另有硬閘)。
  const allowed = true;
  const showSql = owner || lvl >= 11;

  const [me, setMe] = _s(null);
  const [items, setItems] = _s([]);
  const [mode, setMode] = _s("cli");
  const [input, setInput] = _s("");
  const [busy, setBusy] = _s(false);
  const [history, setHistory] = _s([]);
  const [pending, setPending] = _s(null);   // 危險 SQL 確認 {query, danger}
  const histRef = React.useRef(-1);
  const convRef = React.useRef(null);       // AI 多輪對話 id
  const cmdCacheRef = React.useRef(null);   // Tab 補全緩存
  const outRef = React.useRef(null);
  const inputRef = React.useRef(null);

  _e(() => { W2.json("/api/auth/me").then(d => setMe(d || {})).catch(() => setMe({})); }, []);
  _e(() => { outRef.current && (outRef.current.scrollTop = outRef.current.scrollHeight); }, [items, busy, pending]);

  const company = _mm(() => {
    const list = (me && me.companies) || [];
    const hit = list.find(c => c && c.slug === W2.tenant());
    return (hit && hit.name) || W2.tenant() || "—";
  }, [me]);

  const push = (...blocks) => setItems(prev => [...prev, ...blocks]);
  const focus = () => setTimeout(() => inputRef.current && inputRef.current.focus(), 40);

  /* step 事件:把對應「執行中」行原位落定(照抄 1.0 settleStep) */
  const settleStep = (ev) => setItems(prev => {
    const next = [...prev];
    for (let i = next.length - 1; i >= 0; i--) {
      const it = next[i];
      if (it.k === "aistep" && it.step.running && (ev.step_no == null || it.step.step_no === ev.step_no)) {
        next[i] = { k: "aistep", step: { ...ev, running: false } };
        return next;
      }
    }
    next.push({ k: "aistep", step: { ...ev, running: false } });
    return next;
  });

  const doExec = async (line) => {
    setBusy(true);
    try {
      const env = await W2.post("/api/cli/exec", { line });
      if (env && env.ok) push({ k: "res", env });
      else push({ k: "err", text: (env && env.error) || t("執行失敗"), usage: env && env.usage, hint: env && env.hint });
    } catch (e) { push({ k: "err", text: t("無法連接指令路由器:") + (e.message || e) }); }
    finally { setBusy(false); focus(); }
  };

  const doAi = async (text) => {
    setBusy(true);
    try {
      await W2.agentStream({ text, conversation_id: convRef.current }, (ev) => {
        if (ev.event === "run_start") convRef.current = ev.conversation_id || convRef.current;
        else if (ev.event === "step_start") push({ k: "aistep", step: { ...ev, running: true } });
        else if (ev.event === "step") settleStep(ev);
        else if (ev.event === "final") push({ k: "aifinal", message: ev.message || "", run_id: ev.run_id, engine: ev.engine, status: ev.status });
      });
    } catch (e) { push({ k: "err", text: t("AI 會話失敗:") + (e.message || e) }); }
    finally { setBusy(false); focus(); }
  };

  const doSql = async (q, force) => {
    setBusy(true);
    try {
      const d = await W2.post("/api/admin/sql", { sql: q, force: !!force });
      if (d && d.needs_confirm) setPending({ query: q, danger: d.danger || "", msg: d.message || "" });
      else push({ k: "sql", result: d || {} });
    } catch (e) { push({ k: "err", text: t("SQL 失敗:") + (e.message || e) }); }
    finally { setBusy(false); focus(); }
  };

  /* SQL 入口:裸語句(sql 模式)或 sql / sql! 前綴(任意模式,1.0 兼容) */
  const sqlPath = async (text) => {
    if (!showSql) { push({ k: "err", text: t("僅 L5 系統管理員可用 SQL。") }); return; }
    let force = false, q = text;
    const m = text.match(/^sql(!?)(\s+|$)/i);
    if (m) { force = m[1] === "!"; q = text.slice(m[0].length).trim(); }
    if (!q) { push({ k: "info", text: t("SQL 模式 — 直查當前公司庫;SELECT 限 500 行;危險語句需二次確認(sql!)") }); return; }
    if (force) { setPending({ query: q, danger: t("sql! 強制標記——執行前仍需確認") }); return; }
    await doSql(q, false);
  };

  const switchMode = (m) => {
    if (m === mode) return;
    setMode(m); setPending(null);
    push({ k: "info", text: m === "ai"
      ? t("已進入 AI 會話模式:直接說要做什麼(多輪連續);!開頭直通指令;new 開新對話。")
      : m === "sql"
        ? t("已進入 SQL 模式:輸入 SQL 直接執行(僅系統管理員,全程審計;SELECT 限 500 行;危險語句需二次確認)。")
        : t("已切換到 CLI 指令模式:輸入平臺指令,help 查看全部。") });
    focus();
  };

  const run = async (line) => {
    const text = (line || "").trim();
    /* 危險 SQL 確認:空 Enter 或原句 Enter = 確認 force;其他輸入 = 取消並照常執行 */
    if (pending) {
      const staged = pending.query;
      if (!text || text === staged || text === "sql! " + staged) {
        setPending(null);
        push({ k: "cmd", mode: "sql", text: "sql! " + staged }, { k: "info", text: t("已確認,強制執行。"), red: true });
        await doSql(staged, true);
        return;
      }
      setPending(null);
      push({ k: "info", text: t("已取消危險 SQL。") });
    }
    if (!text) return;
    setHistory(h => [...h, text]);
    histRef.current = -1;
    push({ k: "cmd", mode, text });
    if (text === "clear" || text === "cls") { setItems([]); return; }
    if (mode === "sql") {
      if (text === "exit" || text === "quit") { switchMode("cli"); return; }
      await sqlPath(text);
      return;
    }
    if (mode === "ai") {
      if (text === "exit" || text === "quit" || text === "q") { setMode("cli"); push({ k: "info", text: t("已退出 AI 會話模式,回到 CLI。") }); return; }
      if (text === "new") { convRef.current = null; push({ k: "info", text: t("已開啟新對話(上下文已重置)。") }); return; }
      if (text.startsWith("!")) { await doExec(text.slice(1).trim()); return; }
      if (text === "help") { await doExec("help"); return; }
      await doAi(text);
      return;
    }
    /* CLI 模式 */
    if (text === "ai") { switchMode("ai"); return; }
    if (text.startsWith("ai ")) { const q = text.slice(3).trim().replace(/^["']+|["']+$/g, ""); if (q) await doAi(q); return; }
    if (/^sql!?(\s|$)/i.test(text)) { await sqlPath(text); return; }
    await doExec(text);
  };

  const ensureCmds = async () => {
    if (!cmdCacheRef.current) {
      try {
        const d = await W2.json("/api/cli/commands");
        cmdCacheRef.current = ((d && d.commands) || []).filter(c => c && c.allowed).map(c => c.command);
      } catch (e) { cmdCacheRef.current = []; }
    }
    return cmdCacheRef.current;
  };

  const onKeyDown = (e) => {
    if (e.key === "Enter" && !busy) { const v = input; setInput(""); run(v); }
    else if (e.key === "Escape") {
      if (pending) { setPending(null); push({ k: "info", text: t("已取消危險 SQL。") }); }
    }
    else if (e.key === "Tab") {
      const bang = mode === "ai" && input.startsWith("!");
      if (!(mode === "cli" || bang) || !input.trim()) return;   // 其餘情況不劫持 Tab
      e.preventDefault();
      const raw = bang ? input.slice(1) : input;
      ensureCmds().then(cmds => {
        const pool = bang ? cmds : [...cmds, "help", "clear", "ai"];
        const matches = pool.filter(c => c.startsWith(raw));
        if (!matches.length) return;
        let prefix = matches[0];
        for (const mm of matches) {
          let i = 0;
          while (i < prefix.length && i < mm.length && prefix[i] === mm[i]) i++;
          prefix = prefix.slice(0, i);
        }
        setInput((bang ? "!" : "") + (matches.length === 1 ? matches[0] + " " : prefix));
        if (matches.length > 1 && prefix === raw) push({ k: "info", text: matches.join("    ") });
      });
    }
    else if (e.key === "ArrowUp") {
      e.preventDefault();
      if (!history.length) return;
      histRef.current = histRef.current === -1 ? history.length - 1 : Math.max(0, histRef.current - 1);
      setInput(history[histRef.current]);
    }
    else if (e.key === "ArrowDown") {
      e.preventDefault();
      if (histRef.current === -1) return;
      histRef.current = histRef.current + 1 >= history.length ? -1 : histRef.current + 1;
      setInput(histRef.current === -1 ? "" : history[histRef.current]);
    }
  };

  const chips = CHIPS[mode] || CHIPS.cli;
  const placeholder = pending ? t("確認執行:再按一次 Enter;Esc 取消")
    : mode === "ai" ? t("跟內核說要做什麼…(!指令直通 · new 開新對話)")
    : mode === "sql" ? t("輸入 SQL…(SELECT 限 500 行 · 危險語句需二次確認)")
    : t("輸入平臺指令…(help 全部指令 · Tab 補全 · ↑↓ 歷史)");

  return (<>
    <Folio no="16" en="TERMINAL" title={t("超級終端")}
      sub={t("人與 AI 同一指令集 · 同一權限 · 同一審計 — 動作直達後端,非只讀駕駛艙")}
      right={showSql
        ? <span className="label" style={{ color: "var(--red)" }}>{t("ADMIN — 權力面 · 全程審計")}</span>
        : <span className="label">{t("以你的權限執行 · 全程審計")}</span>}/>

    {/* 模式 + 快捷命令(chips 只填入輸入行,不自動執行) */}
    <div className="row g14 wrap rise" style={{ padding: "16px 0 12px" }}>
      <div className="seg">
        <button className={mode === "cli" ? "on" : ""} onClick={() => switchMode("cli")}>CLI</button>
        <button className={mode === "ai" ? "on" : ""} onClick={() => switchMode("ai")}>{t("AI 會話")}</button>
        {showSql && <button className={mode === "sql" ? "on" : ""} onClick={() => switchMode("sql")}>SQL</button>}
      </div>
      <LB dim title={t("點擊填入輸入行,不會自動執行")}>{t("快捷命令")}</LB>
      <div className="row g6 wrap" style={{ flex: 1, minWidth: 260 }}>
        {chips.map(c => (
          <button key={c} className="chip mono" style={{ fontSize: 11, maxWidth: 340, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", display: "inline-block", lineHeight: "28px" }}
            title={mode === "ai" ? t(c) : c}
            onClick={() => { setInput(mode === "ai" ? t(c) : c); focus(); }}>
            {mode === "ai" ? t(c) : c}
          </button>
        ))}
      </div>
    </div>

    {/* 墨黑終端面板:頂部 2px 規線 + mono 標題行 */}
    <div className="rise" style={{ borderTop: "2px solid var(--rule)", animationDelay: ".05s" }}>
      <div className="col" style={{ background: "var(--ink)", height: "calc(100vh - 348px)", minHeight: 430 }}>
        {/* 標題行 */}
        <div className="row spread" style={{ padding: "10px 16px", borderBottom: "1px solid " + DK.hair, flexShrink: 0 }}>
          <span style={{ ...MONO, fontSize: 10.5, fontWeight: 600, letterSpacing: ".18em", color: DK.paper }}>
            TERMINAL — {company} · <span style={{ color: DK.red }}>AUDIT ON</span>
          </span>
          <div className="row g10">
            <span style={{ ...MONO, fontSize: 9, letterSpacing: ".16em", color: DK.dimmer }}>
              MODE: {mode.toUpperCase()}{busy ? " · BUSY" : ""}
            </span>
            <button style={DBTN} onClick={() => setItems([])} title="clear">{t("清屏")}</button>
          </div>
        </div>
        {/* 輸出區 */}
        <div ref={outRef} onClick={() => inputRef.current && inputRef.current.focus()}
          style={{ flex: 1, minHeight: 0, overflowY: "auto", padding: "14px 16px", cursor: "text",
            fontSize: 12.5, lineHeight: 1.65, color: DK.paper }}>
          <Welcome showSql={showSql}/>
          {items.map((item, i) => <TBlock key={i} item={item}/>)}
          {busy && <div style={{ ...MONO, fontSize: 11, color: DK.dim, marginTop: 6 }}>{mode === "ai" ? t("… 內核工作中") : t("… 執行中")}</div>}
        </div>
        {/* 危險 SQL 確認條(Swiss 防呆:紅底反白,再次 Enter 才執行) */}
        {pending && (
          <div className="row g10 fade" style={{ padding: "9px 16px", background: DK.red, color: "#fff", flexShrink: 0 }}>
            <span style={{ ...MONO, fontSize: 10, fontWeight: 700, letterSpacing: ".14em" }}>⚠ {t("危險 SQL")}</span>
            <span style={{ ...MONO, fontSize: 11, flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
              {pending.danger ? pending.danger + " — " : ""}{pending.query}
            </span>
            <span style={{ ...MONO, fontSize: 10, fontWeight: 700, letterSpacing: ".06em", whiteSpace: "nowrap" }}>{t("再按一次 Enter 確認執行;Esc 取消")}</span>
          </div>
        )}
        {/* 輸入行 */}
        <div className="row g10" style={{ padding: "11px 16px", borderTop: "1px solid " + DK.hair, flexShrink: 0 }}>
          <span style={{ ...MONO, fontSize: 13, fontWeight: 800, color: pending || mode === "sql" ? DK.red : DK.paper }}>
            {promptOf(mode)}
          </span>
          <input ref={inputRef} value={input} disabled={busy} autoFocus spellCheck={false}
            onChange={e => setInput(e.target.value)} onKeyDown={onKeyDown} placeholder={busy ? "" : placeholder}
            style={{ flex: 1, background: "transparent", border: "none", outline: "none", color: DK.paper,
              ...MONO, fontSize: 13, caretColor: pending || mode === "sql" ? DK.red : DK.paper }}/>
          <span className="blink-dot" style={{ background: busy ? DK.red : DK.dimmer, width: 7, height: 7 }}/>
        </div>
      </div>
    </div>
  </>);
};

window.W2.PAGES["terminal"] = Page;
})();
