/**
 * verify-live.mjs — 驗「線上網址現在真的在跑的那一份」＝「我們手上這一份」。
 *
 * 用法：
 *   node scripts/verify-live.mjs                  驗全部服役中目標的全部對外網址
 *   node scripts/verify-live.mjs personal         只驗某個目標
 *   node scripts/verify-live.mjs --wait           容忍 CF Pages 生效延遲（重試）
 *   node scripts/verify-live.mjs --url <網址>     只對某個網址驗世代（不需要是宣告目標）
 *   npm run verify
 *
 * 兩層，缺一不可：
 *   ① 組態層：apiBase／profile 的 views/home ＝ deploy.targets.json 宣告值
 *   ② 世代層：線上資產的位元組指紋 ＝ repo public/ 的指紋
 *
 * 為什麼要第二層（2026-08-08，leo：「已經發生過一次這個錯誤，把舊版界面上到 prod，
 * 你要確定不可再犯」）：當天實測三個對外網址，第一層**三項全過**，
 * 而它們跑的是 07-22 那一代的 portal（82,911 bytes、金色 serif 舊品牌），
 * repo 是 343,969 bytes 的新品牌世代。
 * ⇒ **組態可以完全正確，同時展示一套早就被淘汰的介面，而機械檢查一片綠。**
 * 第二層就是為了讓這個狀態不可能無聲存在。
 *
 * 🔴 一律帶 no-cache（快取害人誤判過）。curl|grep 不算驗前端，但 config.js／VIEWS／HOME
 *    與世代指紋都是**純文字資產比對**，抓原始碼比對是這幾項的正確驗法；
 *    「頁面真的能用」另外走瀏覽器實載。
 * 🔴 frozen 目標（見 deploy.targets.json）連抓都不抓——不是我們的帳號，不碰。
 */
import {
  GENERATION_ASSETS,
  fingerprintOf,
  generationOfDir,
  loadTargets,
  parseApiBase,
  readState,
  resolveTarget,
} from './targets.mjs';

const NOCACHE = { 'Cache-Control': 'no-cache', Pragma: 'no-cache' };

async function get(url) {
  const res = await fetch(`${url}${url.includes('?') ? '&' : '?'}_nc=${Date.now()}`, {
    headers: NOCACHE,
    cache: 'no-store',
    redirect: 'follow',
  });
  const buf = Buffer.from(await res.arrayBuffer());
  return { status: res.status, bytes: buf, text: buf.toString('utf8') };
}

/** 抓線上的世代資產，算指紋。抓不到的當 MISSING（照樣算，缺檔本來就是另一代）。 */
async function liveGeneration(base) {
  const entries = [];
  const detail = {};
  for (const { file, urlPath } of GENERATION_ASSETS) {
    try {
      const r = await get(`${base.replace(/\/$/, '')}${urlPath}`);
      const ok = r.status === 200;
      entries.push({ file, bytes: ok ? r.bytes : null });
      detail[file] = { status: r.status, text: ok ? r.text : null };
    } catch (e) {
      entries.push({ file, bytes: null });
      detail[file] = { status: `連線失敗：${e.message}`, text: null };
    }
  }
  return { ...fingerprintOf(entries), detail };
}

/** 驗一個網址。t 給 null＝只驗世代（ad-hoc 模式）。 */
export async function verifyUrl(t, url, want) {
  const checks = [];
  const base = url.replace(/\/$/, '');
  const live = await liveGeneration(base);

  // ── 世代層 ──────────────────────────────────────────────
  const genOk = live.digest === want.digest;
  const diffs = Object.entries(want.assets)
    .filter(([f, a]) => live.assets[f]?.sha !== a.sha)
    .map(([f, a]) => {
      const l = live.assets[f] ?? {};
      const st = live.detail[f]?.status;
      return `${f}：repo ${a.size ?? '缺'} bytes / 線上 ${l.missing ? `抓不到（${st}）` : `${l.size} bytes`}`;
    });
  checks.push({
    name: '世代',
    ok: genOk,
    want: `${want.digest.slice(0, 12)}（repo public/）`,
    got: genOk
      ? `${live.digest.slice(0, 12)}`
      : `${live.digest.slice(0, 12)}\n           不同的資產：\n             ${diffs.join('\n             ')}`,
  });

  if (!t) return { url, ok: genOk, checks };

  // ── 組態層 ──────────────────────────────────────────────
  try {
    const cfg = await get(`${base}/config.js`);
    const got = cfg.status === 200 ? parseApiBase(cfg.text) : `HTTP ${cfg.status}`;
    checks.push({ name: 'apiBase', ok: got === t.apiBase, want: t.apiBase, got: got ?? '(config.js 裡找不到 apiBase)' });
  } catch (e) {
    checks.push({ name: 'apiBase', ok: false, want: t.apiBase, got: `連線失敗：${e.message}` });
  }

  const con = live.detail['console/index.html'];
  const conText = con?.text;
  const views = conText?.match(/var VIEWS = (\[[^\]]*\]);/);
  const home = conText?.match(/var HOME = "([^"]*)";/);
  const gotViews = conText ? (views ? views[1] : '(找不到 VIEWS)') : `HTTP ${con?.status}`;
  const gotHome = conText ? (home ? home[1] : '(找不到 HOME)') : `HTTP ${con?.status}`;
  checks.push({
    name: `profile(${t.profile}).views`,
    ok: gotViews === JSON.stringify(t.views),
    want: JSON.stringify(t.views),
    got: gotViews,
  });
  checks.push({ name: `profile(${t.profile}).home`, ok: gotHome === t.home, want: t.home, got: gotHome });

  return { url, ok: checks.every((c) => c.ok), checks };
}

export async function verifyTarget(name, { wait = false } = {}) {
  const t = resolveTarget(name);
  if (t.frozen) return { name, target: t, skipped: true, ok: true, results: [] };
  const want = generationOfDir();
  const attempts = wait ? 8 : 1;
  let results = [];
  for (let i = 1; i <= attempts; i++) {
    results = [];
    for (const url of t.verifyUrls) results.push(await verifyUrl(t, url, want));
    if (results.every((r) => r.ok) || i === attempts) break;
    process.stdout.write(`  … 尚未生效，5s 後重試（${i}/${attempts - 1}）\n`);
    await new Promise((r) => setTimeout(r, 5000));
  }
  return { name, target: t, ok: results.every((r) => r.ok), results };
}

export function printReport(reports) {
  for (const r of reports) {
    console.log(`\n【${r.name}】${r.target.description}`);
    if (r.skipped) {
      console.log(`  ⏸️  已凍結，不抓不驗：${r.target.frozen}`);
      continue;
    }
    console.log(`  宣告：profile=${r.target.profile}  apiBase=${r.target.apiBase}`);
    for (const u of r.results) {
      console.log(`  ${u.ok ? '✅' : '❌'} ${u.url}`);
      for (const c of u.checks) {
        if (c.ok) console.log(`       ✓ ${c.name} = ${c.got}`);
        else console.log(`       ✗ ${c.name}\n           我們手上：${c.want}\n           線上跑的：${c.got}`);
      }
    }
  }
}

export async function verifyAll(names, opts) {
  const reports = [];
  for (const n of names) reports.push(await verifyTarget(n, opts));
  return reports;
}

const isCli = process.argv[1] && import.meta.url === `file://${process.argv[1]}`;
if (isCli) {
  const args = process.argv.slice(2);
  const wait = args.includes('--wait');
  const urlIdx = args.indexOf('--url');

  if (args.includes('--offline-lag')) {
    // 不連網，只問一句：「我手上這一代，有沒有真的送出去過？」
    // 給 Stop hook 用（每回合都跑，所以不准連網、不准慢）。
    // 唯一的事實來源是 .deploy-state.json，而它**只在線上實測通過後**才被寫（見 deploy.mjs）
    // ⇒ 它說綠就是真的有人驗過線上，不是「我跑過部署指令」。
    const here = generationOfDir().digest;
    const state = readState();
    const stale = [];
    for (const n of loadTargets().active) {
      const s = state[n];
      if (!s) stale.push(`${n}：沒有任何一次通過線上實測的部署紀錄（線上是哪一代，現在沒人知道）`);
      else if (s.generation !== here) {
        stale.push(`${n}：最後一次驗過的是 ${s.generation.slice(0, 12)}（${s.verifiedAt.slice(0, 10)}），現在手上是 ${here.slice(0, 12)}`);
      }
    }
    if (stale.length) {
      console.log(stale.join('\n'));
      process.exit(1);
    }
    process.exit(0);
  }

  if (urlIdx !== -1) {
    // ad-hoc：只問「這個網址上跑的是不是當代的」——不需要它是宣告過的目標。
    const url = args[urlIdx + 1];
    if (!url) {
      console.error('用法：node scripts/verify-live.mjs --url <網址>');
      process.exit(2);
    }
    const want = generationOfDir();
    const r = await verifyUrl(null, url, want);
    console.log(`\n【世代檢查】${url}`);
    for (const c of r.checks) {
      if (c.ok) console.log(`  ✅ ${c.name} = ${c.got}`);
      else console.log(`  ❌ ${c.name}\n      我們手上：${c.want}\n      線上跑的：${c.got}`);
    }
    if (!r.ok) {
      console.error('\n❌ 這個網址上跑的不是當代的前端——它展示的是一套已經被淘汰的介面。');
      process.exit(1);
    }
    console.log('\n✅ 這個網址上跑的＝我們手上這一份。');
    process.exit(0);
  }

  const picked = args.filter((a) => !a.startsWith('--'));
  const names = picked.length ? picked : loadTargets().names;
  const reports = await verifyAll(names, { wait });
  printReport(reports);
  const bad = reports.filter((r) => !r.ok);
  if (bad.length) {
    console.error(`\n❌ ${bad.length} 個目標與宣告／當代不符：${bad.map((b) => b.name).join('、')}`);
    console.error('   （線上實際在跑的 ≠ 我們手上這一份——這正是要被擋掉的那個病）');
    process.exit(1);
  }
  console.log('\n✅ 所有服役中目標：線上組態＝宣告值，線上世代＝repo 當代。');
}
