/**
 * preflight.ts — self-hosted 安裝的「偵測先於動作 + 裝完驗收」（§7.8 P0，pip 式）。
 *
 * 核心判準（self-hosted-init.md §7.8）：
 *   - **偵測先於動作**：init 先檢查各前置（node / wrangler / CF 可達），缺的才裝、有的跳過。
 *     不是假設齊備直接動手 → 缺一個就卡（test_arcrun/4 的 D1 大跑去讀原始碼自己想辦法）。
 *   - **裝完驗收**：部署後逐項確認（KV / D1 / migration / cypher 可達），缺哪項明確報哪項
 *     + 給一鍵補裝指令。不是靜默印灰字（原本 harness/MCP 失敗只 console.log 灰字，用戶不知道）。
 *   - **冪等**：重跑檢查後「什麼也沒動」。
 *
 * 本檔只做「偵測 + 報告」，不自己建資源（要不要建由 resource-resolver 判斷，deploy.ts 編排）。
 * 🔴 Arcrun#97：報告裡的 fix 指令也算「產品的一部分」——一句「acr update（冪等重建）」
 *   接在誤報的「缺 KV」後面，就是把使用者直接推去執行那個把實例洗空的動作。
 */

import { execFileSync } from 'node:child_process';
import chalk from 'chalk';
import type { CfAccountClient } from './cf-api.js';

export interface PreflightItem {
  name: string;
  ok: boolean;
  detail?: string;
  /** 缺漏時給用戶的一鍵補救指令（沒有則留空）。*/
  fix?: string;
}

/** node 是否可用 + 版本（init 本身是 node 跑的，能跑到這裡 node 必在，但仍印版本供診斷）。*/
function detectNode(): PreflightItem {
  try {
    const v = execFileSync('node', ['--version'], { stdio: ['ignore', 'pipe', 'ignore'] })
      .toString().trim();
    return { name: 'node', ok: true, detail: v };
  } catch {
    return { name: 'node', ok: false, fix: '安裝 Node.js 18+：https://nodejs.org' };
  }
}

/** wrangler（CF CLI）是否可用 + 版本。self-hosted 部署的硬前置。*/
function detectWrangler(): PreflightItem {
  try {
    const v = execFileSync('wrangler', ['--version'], { stdio: ['ignore', 'pipe', 'ignore'] })
      .toString().trim();
    return { name: 'wrangler', ok: true, detail: v };
  } catch {
    return { name: 'wrangler', ok: false, fix: 'npm i -g wrangler' };
  }
}

/**
 * 安裝前偵測（pip 式：先看環境有什麼）。
 * CF 憑證可達由呼叫端用 CfAccountClient.verifyAccess 接著驗（需要 token，不在這層）。
 * 回傳所有項目 + 是否有 fatal 缺漏（node/wrangler 缺 = 無法繼續）。
 */
export function detectEnvironment(): { items: PreflightItem[]; fatal: boolean } {
  const items = [detectNode(), detectWrangler()];
  const fatal = items.some((i) => !i.ok);
  return { items, fatal };
}

/** 印一組偵測結果（✓/✗ + 版本 + 補救指令）。*/
export function printPreflight(title: string, items: PreflightItem[]): void {
  console.log(chalk.bold(`\n  ${title}`));
  for (const it of items) {
    if (it.ok) {
      console.log(chalk.green(`  ✓ ${it.name}`) + (it.detail ? chalk.gray(`  ${it.detail}`) : ''));
    } else {
      console.log(chalk.yellow(`  ✗ ${it.name}`) + (it.detail ? chalk.gray(`  ${it.detail}`) : ''));
      if (it.fix) console.log(chalk.gray(`      → ${it.fix}`));
    }
  }
}

/**
 * 裝完驗收：逐項確認 self-hosted 環境真的就緒（§7.8 D1 根因：安裝不偵測，缺了不報）。
 * 各項以「實際查 CF / 打 cypher」確認，非看 config 有沒有寫——避免假綠（mindset §7）。
 *
 * @returns items（每項 ok + detail/fix）。呼叫端依 allOk 決定是否 exit 非零 / 印補裝指引。
 */
export async function verifyInstall(opts: {
  cf: CfAccountClient;
  /** binding → KV namespace id（部署實際用上的那幾顆）。*/
  kvNamespaceIds: Record<string, string>;
  /** 部署實際用上的 D1 id（沒有 D1 就不傳）。*/
  d1DatabaseId?: string;
  cypherUrl?: string;
}): Promise<{ items: PreflightItem[]; allOk: boolean }> {
  const items: PreflightItem[] = [];

  // KV：核對「部署實際綁上去的那幾顆 id」在帳號上還在不在。
  // 🔴 Arcrun#97：這裡**不能**用「帳號上有沒有叫 WEBHOOKS 的 namespace」來驗。
  //   安裝器裝出來的實例，資源名字是 arcrun-rag-<instance>-kv-webhooks——照名字驗會誤報「缺」，
  //   而那句誤報底下就寫著「fix: acr update（冪等重建）」⇒ 使用者照做，就被重建成空的。
  //   驗的對象永遠是 id（我們真的綁上去的那顆），不是名字。
  const kvBindings = Object.entries(opts.kvNamespaceIds);
  try {
    const ids = new Set((await opts.cf.listKvNamespaces()).values());
    const missing = kvBindings.filter(([, id]) => !ids.has(id)).map(([b]) => b);
    items.push(
      missing.length === 0
        ? { name: `KV namespaces (${kvBindings.length})`, ok: true }
        : {
            name: 'KV namespaces',
            ok: false,
            detail: `這幾個 binding 綁著的 namespace 在帳號上找不到：${missing.join(', ')}`,
            fix: '先確認那幾顆是被刪了還是 token 看不到——不要直接重跑安裝（會綁到空的）',
          },
    );
  } catch (e) {
    items.push({ name: 'KV namespaces', ok: false, detail: msg(e), fix: '檢查 CF token 的 KV 讀取權限' });
  }

  // D1：同理，核對實際綁上去的那顆 id 還在不在（不是核對有沒有叫 arcrun-kbdb 的庫）。
  if (opts.d1DatabaseId) {
    try {
      const ids = new Set((await opts.cf.listD1Databases()).values());
      items.push(
        ids.has(opts.d1DatabaseId)
          ? { name: `D1 ${opts.d1DatabaseId}`, ok: true }
          : {
              name: `D1 ${opts.d1DatabaseId}`,
              ok: false,
              detail: '這顆 D1 在帳號上找不到',
              fix: '先確認它是被刪了還是 token 看不到——不要直接重跑安裝（會綁到空的）',
            },
      );
    } catch (e) {
      // D1 讀不到最常見根因：CF token 沒勾 D1 權限（KV/Worker 能建但 D1 報 Authentication error）。
      const m = msg(e);
      const fix = /auth/i.test(m)
        ? 'token 缺 D1 權限：CF token 補勾「Account / D1 / Edit」→ 重產 token 填回 .env → acr update'
        : '檢查 CF token 的 D1 讀取權限';
      items.push({ name: `D1 ${opts.d1DatabaseId}`, ok: false, detail: m, fix });
    }
  }

  // cypher-executor 可達（打 /health，不只看 config 有 URL）
  if (opts.cypherUrl) {
    try {
      const res = await fetch(`${opts.cypherUrl}/health`, { method: 'GET' });
      items.push(
        res.ok
          ? { name: 'cypher-executor 可達', ok: true, detail: opts.cypherUrl }
          : { name: 'cypher-executor 可達', ok: false, detail: `HTTP ${res.status} @ ${opts.cypherUrl}`, fix: 'acr update（重部署）' },
      );
    } catch (e) {
      items.push({ name: 'cypher-executor 可達', ok: false, detail: msg(e), fix: 'acr update（重部署）；或 worker 剛部署稍候再試' });
    }
  }

  return { items, allOk: items.every((i) => i.ok) };
}

function msg(e: unknown): string {
  return e instanceof Error ? e.message : String(e);
}
