/**
 * 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 灰字，用戶不知道）。
 *   - **冪等**：重跑檢查後「什麼也沒動」（ensureKvNamespace / ensureD1Database 本就冪等）。
 *
 * 本檔只做「偵測 + 報告」，不自己建資源（建資源仍走 cf-api 的 ensure*，由 init 編排）。
 */

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;
  requiredKv: readonly string[];
  expectD1Name?: string;
  cypherUrl?: string;
}): Promise<{ items: PreflightItem[]; allOk: boolean }> {
  const items: PreflightItem[] = [];

  // KV：實查 CF 上現有 namespace，比對必需清單
  try {
    const existing = await opts.cf.listKvNamespaces();
    const have = new Set(existing.keys());
    const missing = opts.requiredKv.filter((t) => !have.has(t));
    items.push(
      missing.length === 0
        ? { name: `KV namespaces (${opts.requiredKv.length})`, ok: true }
        : { name: 'KV namespaces', ok: false, detail: `缺 ${missing.join(', ')}`, fix: 'acr update（冪等重建）' },
    );
  } catch (e) {
    items.push({ name: 'KV namespaces', ok: false, detail: msg(e), fix: 'acr update' });
  }

  // D1：實查 CF 上是否有該庫
  if (opts.expectD1Name) {
    try {
      const dbs = await opts.cf.listD1Databases();
      items.push(
        dbs.has(opts.expectD1Name)
          ? { name: `D1 ${opts.expectD1Name}`, ok: true }
          : { name: `D1 ${opts.expectD1Name}`, ok: false, detail: '不存在', fix: 'CF token 補勾「Account / D1 / Edit」權限 → 重產 token 填回 .env → acr update' },
      );
    } 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'
        : 'acr update（冪等重試）';
      items.push({ name: `D1 ${opts.expectD1Name}`, 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);
}
