/**
 * console-ui build — 把 cypher-executor 的三支 UI renderer 在「建置時」跑一次，
 * 產出純靜態 HTML 到 public/，交給 Cloudflare Pages 託管。
 *
 * 為什麼這樣做（cypher-ui-split 第一刀）：
 *   原本 console/portal/dashboard 的 HTML 由 cypher-executor Worker 在「每次請求時」
 *   用 template literal 組出來 → 5,240 行 UI 字串永遠躺在 Worker bundle 裡（748KB），
 *   連 /health 這種什麼都不做的請求都要付 5-7ms CPU（免費層上限 10ms）。
 *   UI 是靜態的（單檔 HTML＋原生 JS、零外部資源），本來就該待在 Pages。
 *
 * 保持原特性（leo 反覆強調簡化）：
 *   - 零打包工具、零 npm 依賴：本檔只用 node 內建 fs/path，正則抽出 renderer 的
 *     template literal 後求值。不引入 esbuild/vite/rollup。
 *   - 產出仍是「單檔 HTML＋原生 JS hash routing、零外部資源」。
 *
 * 唯一的行為差異＝API base：
 *   原本 UI 與 API 同源，fetch 全用相對路徑（'/kbdb/search'）。搬上 Pages 後跨網域，
 *   故注入 window.ARCRUN_API_BASE，並把 fetch 的相對路徑改成 API_BASE + path。
 *   見下方 rewriteFetchPaths()。
 */
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..');
const SRC = join(ROOT, '..', 'cypher-executor', 'src');
const OUT = join(ROOT, 'public');

// ── 建置期組態（原本是 Worker 的 env var，現在是建置參數）────────────────
// Pages 是靜態站，沒有 per-request env；品牌/profile 這類「一個部署一個值」的
// 設定改在建置時決定（要換值＝重跑 build 再部署，符合靜態站模型）。
// 具名部署目標（deploy.targets.json）：一個目標＝帳號＋profile＋apiBase 綁在一起。
// 帶 DEPLOY_TARGET=personal|enterprise 就套用該組值；個別環境變數仍可覆蓋（除錯用）。
// 立此檔的原因見 deploy.targets.json 的 _readme——散在部署指令裡的參數帶漏過三次。
const TARGET_NAME = process.env.DEPLOY_TARGET || '';
let TARGET = {};
if (TARGET_NAME) {
  const targets = JSON.parse(readFileSync(join(ROOT, 'deploy.targets.json'), 'utf8'));
  TARGET = targets[TARGET_NAME];
  if (!TARGET) {
    const names = Object.keys(targets).filter((k) => !k.startsWith('_'));
    throw new Error(`未知的 DEPLOY_TARGET："${TARGET_NAME}"。可用：${names.join(' / ')}`);
  }
  console.log(`部署目標：${TARGET_NAME} — ${TARGET.description}`);
}

const CFG = {
  brand: process.env.CONSOLE_BRAND || TARGET.brand || 'Arcrun',
  profile: process.env.CONSOLE_PROFILE || TARGET.profile || 'full',
  registryBase: process.env.REGISTRY_BASE || 'https://registry.arcrun.dev',
  sourceWebBase: process.env.PORTAL_SOURCE_WEB_BASE || '',
  // API base 走 runtime 注入（見 public/config.js），這裡只放預設值
  apiBase: process.env.ARCRUN_API_BASE || TARGET.apiBase || '',
};

/**
 * 讀 TS 原始碼並取出整個 renderer 函式的**函式主體**（不只 template literal）。
 *
 * 取整個 body 而非只取反引號區塊，是因為 renderer 在 return 之前會先算區域變數
 * （如 console.ts 的 rag/views/home 由 profile 推導）。只搬模板＝把那段推導邏輯
 * 複製一份到本檔＝雙份真相會漂移。連 body 一起求值 → 推導邏輯永遠只有一份。
 */
/**
 * renderer 原始檔的位置：本專案 `console-ui/src/` 優先，找不到才回退 cypher-executor。
 *
 * 為什麼要這層（2026-07-22 修）：`5a16484` 把 UI 搬出 cypher-executor 時，
 * **刪了 console.ts / portal-ui.ts 卻只搬走 build 產物（HTML），原始檔沒跟著搬**
 * → build.mjs 讀不到來源，`npm run build` 從那天起就 ENOENT 死掉，
 * 線上 HTML 是刪檔前烤好的、之後再也無法重建（profile 改了也不會生效）。
 * 現已從 git 撈回放進 console-ui/src/——UI 原始碼跟著 UI 專案走，才是那一刀的原意。
 * console-dashboard.ts 仍在 cypher-executor（它同時含 API），故保留回退路徑。
 */
function resolveSource(file) {
  const local = join(ROOT, 'src', file.replace(/^routes\//, ''));
  if (existsSync(local)) return local;
  return join(SRC, file);
}

function extractRendererBody(file, fnName) {
  const code = readFileSync(resolveSource(file), 'utf8');
  const start = code.indexOf(`function ${fnName}(`);
  if (start < 0) throw new Error(`找不到 ${fnName} in ${file}`);
  const braceStart = code.indexOf('{', code.indexOf(')', start));
  if (braceStart < 0) throw new Error(`${fnName} 找不到函式主體`);
  // 掃到配對的收尾大括號；需略過字串／template literal／註解裡的括號
  let i = braceStart + 1;
  let depth = 1;
  let mode = null; // null | "'" | '"' | '`' | 'line' | 'block'
  let tplDepth = 0;
  while (i < code.length && depth > 0) {
    const ch = code[i];
    const nx = code[i + 1];
    if (mode === null) {
      if (ch === '\\') { i += 2; continue; }
      if (ch === '/' && nx === '/') { mode = 'line'; i += 2; continue; }
      if (ch === '/' && nx === '*') { mode = 'block'; i += 2; continue; }
      if (ch === "'" || ch === '"') { mode = ch; i++; continue; }
      if (ch === '`') { mode = '`'; tplDepth = 0; i++; continue; }
      if (ch === '{') depth++;
      else if (ch === '}') depth--;
      i++;
      continue;
    }
    if (mode === 'line') { if (ch === '\n') mode = null; i++; continue; }
    if (mode === 'block') { if (ch === '*' && nx === '/') { mode = null; i += 2; continue; } i++; continue; }
    if (ch === '\\') { i += 2; continue; }
    if (mode === '`') {
      // template literal 內的 ${ … } 是真程式碼，其中的引號/括號要照常計數才不會誤判收尾
      if (ch === '$' && nx === '{') { tplDepth++; i += 2; continue; }
      if (ch === '}' && tplDepth > 0) { tplDepth--; i++; continue; }
      if (ch === '`' && tplDepth === 0) { mode = null; i++; continue; }
      i++;
      continue;
    }
    if (ch === mode) mode = null;
    i++;
  }
  // 去掉 TS 的型別註記（本 body 只有 `const x: T =` 這種簡單形態）
  return code.slice(braceStart + 1, i - 1).replace(/\bconst\s+(\w+):\s*[\w<>[\]|]+\s*=/g, 'const $1 =');
}

/** 取出 lib/taipei-time.ts 匯出的 TAIPEI_CLIENT_JS 字串常數（UI 內嵌的客戶端時間工具）。 */
function extractTaipeiClientJs() {
  const code = readFileSync(join(SRC, 'lib', 'taipei-time.ts'), 'utf8');
  // 形態＝字串陣列 .join('\n')（見 lib/taipei-time.ts），直接求值該陣列表達式
  const m = code.match(/export const TAIPEI_CLIENT_JS\s*=\s*(\[[\s\S]*?\]\.join\('\\n'\));/);
  if (!m) throw new Error('找不到 TAIPEI_CLIENT_JS');
  return new Function(`return ${m[1]};`)();
}

/**
 * 求值 renderer 函式主體。用 new Function 而非 eval——只餵建置期組態，
 * 輸入是本 repo 自己的原始碼（非使用者輸入），無注入面。
 */
function render(body, vars) {
  const names = Object.keys(vars);
  const fn = new Function(...names, body);
  return fn(...names.map((n) => vars[n]));
}

/**
 * 把 UI 內原生 JS 的相對路徑 fetch 改成打 API base。
 *
 * 只改 `fetch('/...` 與 `fetch("/...`（開頭是單斜線＝同源絕對路徑）這一種形態，
 * 其餘（fetch(url, …) 這類變數形式）另由各檔的 url 組法在下面單獨處理。
 */
function rewriteFetchPaths(html, file) {
  // ① fetch('/xxx  →  fetch(API_BASE + '/xxx
  let out = html.replace(/fetch\((['"])\/(?!\/)/g, 'fetch(API_BASE + $1/');
  // ② 變數式 fetch(url, ...)：url 由上方 var url = '/kbdb/search?...' 組成 →
  //    把這類「以單斜線開頭的路徑字面值指派」也補上 API_BASE
  out = out.replace(/(\bvar\s+url\s*=\s*)(['"])\/(?!\/)/g, '$1API_BASE + $2/');
  // ③ portal 的 adminApi(method, path, body)：path 由呼叫端傳字面值進來，①②
  //    都掃不到（8 個呼叫點）。在 helper 內部補前綴＝一處修好全部，不必改 8 個呼叫點。
  out = out.replace(
    /(function adminApi\(method, path, body\) \{)/,
    '$1\n    path = API_BASE + path;'
  );

  // 防呆：搬完後不該再有「直接 fetch 同源相對路徑」的殘留。掃到就讓建置失敗，
  // 免得漏網的呼叫點在 Pages 上打到 Pages 自己（404）才被發現。
  // 註：adminApi 的呼叫端仍是相對路徑字面值——那是對的，前綴由 helper 內部（③）加。
  const unprefixed = [...out.matchAll(/fetch\((['"])\/(?!\/)[^'"]*/g)].map((m) => m[0]);
  if (unprefixed.length) {
    throw new Error(
      `${file}：有 ${unprefixed.length} 個相對路徑 fetch 沒被改寫成 API_BASE：\n  ` +
        [...new Set(unprefixed)].join('\n  ')
    );
  }
  // adminApi 形態存在時，必須確認 helper 已被加上前綴（否則 8 個呼叫點全會打錯家）
  if (/function adminApi\(method, path, body\)/.test(out) && !/path = API_BASE \+ path;/.test(out)) {
    throw new Error(`${file}：偵測到 adminApi helper 但前綴注入失敗`);
  }
  return out;
}

/** 在頁面 <head> 注入 config.js（runtime 決定 API base），並定義 API_BASE 供內嵌 JS 用。 */
function injectApiBase(html) {
  const snippet = `<script src="/config.js"></script>
<script>window.ARCRUN_API_BASE = (window.ARCRUN_CONFIG && window.ARCRUN_CONFIG.apiBase) || ${JSON.stringify(CFG.apiBase)};</script>`;
  const withCfg = html.replace('</head>', `${snippet}\n</head>`);
  // 內嵌的 IIFE 裡宣告 API_BASE（各頁的主 <script> 都是 (function(){ … })() 形態）
  return withCfg.replace(
    /<script>\s*\(function\s*\(\)\s*\{/,
    '<script>\n(function () {\n  var API_BASE = window.ARCRUN_API_BASE || \'\';'
  );
}

function build(name, file, fnName, vars) {
  const body = extractRendererBody(file, fnName);
  let html = render(body, vars);
  html = rewriteFetchPaths(html, name);
  html = injectApiBase(html);
  const dest = join(OUT, name);
  mkdirSync(dirname(dest), { recursive: true });
  writeFileSync(dest, html, 'utf8');
  console.log(`  ${name.padEnd(24)} ${(Buffer.byteLength(html) / 1024).toFixed(1)} KB`);
}

const TAIPEI_CLIENT_JS = extractTaipeiClientJs();

mkdirSync(OUT, { recursive: true });
console.log('console-ui build →', OUT);

// /console — Admin Console 完整版（console.ts renderConsoleHtml）
build('console/index.html', 'routes/console.ts', 'renderConsoleHtml', {
  registryBase: CFG.registryBase,
  brand: CFG.brand,
  profile: CFG.profile,
  TAIPEI_CLIENT_JS,
});

// /portal — RAG Portal（portal-ui.ts renderPortalHtml）
build('portal/index.html', 'routes/portal-ui.ts', 'renderPortalHtml', {
  brand: CFG.brand,
  sourceWebBase: CFG.sourceWebBase,
  TAIPEI_CLIENT_JS,
});

// /console/dashboard — 駕駛艙（console-dashboard.ts renderDashboardHtml）
build('console/dashboard/index.html', 'routes/console-dashboard.ts', 'renderDashboardHtml', {
  brand: CFG.brand,
  TAIPEI_CLIENT_JS,
});

// config.js：部署後可直接改這一檔切 API 目標，不必重 build
writeFileSync(
  join(OUT, 'config.js'),
  `// Arcrun UI runtime 組態——改這一行就能切 API 目標，不必重新 build。
window.ARCRUN_CONFIG = { apiBase: ${JSON.stringify(CFG.apiBase)} };
`,
  'utf8'
);
console.log('  config.js');
console.log('done.');
