/**
 * acr update — 從 Gitea 拉最新 archive，重新部署零件/引擎到用戶自己的 Cloudflare。
 *
 * 與 acr init --self-hosted 走同一條「下載 archive → 注入 KV id → wrangler deploy」的路
 *（同一支 downloadAndDeploy），差別只在：init 是首次（建 KV/D1 + 寫 config），
 *   update 是沿用既有 config 重部署變動的 Worker。因共用 downloadAndDeploy 的內容指紋 manifest，
 *   「新裝零件補上、內容未變者略過」對 update 天然成立 → install ≈ update（Arcrun#4）。
 *
 * 下載源（Arcrun#4，2026-07-07）：由 GitHub codeload 改指 Gitea archive（git.uncle6.me），
 *   讓 D20 防 flag 下不能碰 GitHub 的 self-hosted 用戶也能 acr update 補裝新零件（如 code 零件）。
 *
 * 對應 SDD：.agents/specs/arcrun/sdk-and-website/self-hosted-init.md §3「acr update」
 */

import chalk from 'chalk';
import { loadConfig } from '../lib/config.js';
import {
  wranglerAvailable,
  downloadAndDeploy,
  namespaceHasKnowledge,
  type DeployContext,
} from '../lib/deploy.js';

export async function cmdUpdate(opts: { force?: boolean } = {}): Promise<void> {
  const config = loadConfig();

  if (config.mode !== 'self-hosted') {
    console.log(chalk.yellow('\n  acr update 只用於 self-hosted 模式（部署在你自己的 Cloudflare）。'));
    console.log(chalk.gray('  目前模式：' + config.mode + '。如要 self-host，先跑 acr init --self-hosted。\n'));
    process.exit(1);
  }

  if (!config.cloudflare_account_id || !config.cf_api_token) {
    console.log(chalk.yellow('\n  config 缺 cloudflare_account_id / cf_api_token，無法部署。'));
    console.log(chalk.gray('  請重新跑 acr init --self-hosted。\n'));
    process.exit(1);
  }

  if (!wranglerAvailable()) {
    console.log(chalk.yellow('\n  ✗ 找不到 wrangler（Cloudflare CLI）。請先 npm i -g wrangler。\n'));
    process.exit(1);
  }

  console.log(chalk.bold('\n  acr update — 拉新 release 並重新部署\n'));

  // 🔴 Arcrun#97：這裡**曾經**先「照名字 ensure」一輪 KV + D1 再往下傳。
  //   binding 名（WEBHOOKS）被當成 CF 上的資源標題去找，安裝器建的資源不叫那個名字
  //   ⇒ 每次都對不上 ⇒ 每次都新建一顆空的綁上去 ⇒ 使用者的工作流／登入／子庫從畫面上消失。
  //   現在資源解析整段搬進 downloadAndDeploy：先讀「你已部署的 worker 現在綁著什麼」再決定，
  //   而且是**下載完、看得到這版要哪些 binding 之後**才決定，不再由這裡預先造一批。
  const ctx: DeployContext = {
    accountId: config.cloudflare_account_id,
    apiToken: config.cf_api_token,
    workerSubdomain: extractSubdomain(config.cypher_executor_url),
    // self-hosted → 注入 MULTI_TENANT="false"（mcp-account-source §5.5，修 acr update 部署的 MCP 401）。
    // config 源頭：init 寫 multi_tenant:false + mode:'self-hosted'。acr update 只在 self-hosted 跑。
    selfHosted: config.mode === 'self-hosted' || config.multi_tenant === false,
    // 語義查詢（issue #7）：預設**開**，只有 config 顯式寫 kbdb_embed:false 才關。
    // 🔴 2026-08-09 翻轉預設（leo：「語義搜尋已經確定是一安裝就提供的功能」）。
    //   舊判斷 `=== true` 的實害：config 沒這個欄位（舊 config / 一鍵安裝實例本機補跑 update）
    //   時 redeploy 會把 kbdb 的 [[vectorize]]+[ai] binding 靜默剝掉——一台**原本正常**的
    //   實例就這樣失去語意搜尋，畫面上還被說成「還沒開通」。wrangler deploy 是整份覆蓋，
    //   binding 不在 toml 裡＝直接消失，這正是「裝好的實例壞掉」的機制之一。
    kbdbEmbed: config.kbdb_embed !== false,
  };

  // Arcrun#108：把「你的知識住在哪個命名空間」同步給雲端——但**先驗再寫**。
  //
  // 病灶：你 push 工作流、小幫手上傳知識、MCP 查詢，用的都是 config 的 `api_key`；
  // 而 cypher 讀藏書地圖/搜尋/工作流時，過濾用的 owner_id 來自 worker 的環境變數
  // （repo toml 帶的官方預設 `CONSOLE_TENANT = "leo"`）。兩個來源對不上 ⇒ 你的東西全被濾掉。
  //
  // 為什麼不無條件寫：一鍵安裝的實例，知識可能本來就寫在 `CONSOLE_TENANT` 底下。
  // 無條件蓋成本機 api_key，會把一台**原本正常**的實例指向空的那一格
  // ——那就是 #97/#106 那類「更新一次把人家的東西弄不見」。所以查得到才寫，查不到就不碰。
  if (config.api_key && config.cypher_executor_url) {
    process.stdout.write(chalk.gray('  → 核對雲端要用哪個知識命名空間...'));
    const hasKnowledge = await namespaceHasKnowledge(config.cypher_executor_url, config.api_key);
    if (hasKnowledge === true) {
      ctx.knowledgeNamespace = config.api_key;
      console.log(chalk.green(' ✓'));
      console.log(chalk.gray(`    ARCRUN_NAMESPACE = ${config.api_key}（這個命名空間底下查得到你的知識庫）`));
    } else if (hasKnowledge === false) {
      console.log(chalk.yellow(' ⚠'));
      console.log(chalk.gray(`    ${config.api_key} 底下目前查不到任何知識庫 → 這趟不動雲端的命名空間設定`));
      console.log(chalk.gray('    （若藏書地圖是空的，請把這行連同 acr update 的輸出一起回報）'));
    } else {
      console.log(chalk.yellow(' ⚠'));
      console.log(chalk.gray('    問不到實例（可能正在啟動或版本較舊）→ 這趟不動雲端的命名空間設定'));
    }
  }

  // mode:'update' → 資源解析在「一顆該更新的 worker 都找不到」時會停手而不是重建一整套
  //（Arcrun#97 的另一道門：名字對不上時別假裝這是全新安裝）。
  const result = await downloadAndDeploy(ctx, 'main', { force: opts.force, mode: 'update' });

  // 資源解析階段喊停：什麼都沒建、什麼都沒部。原文照印，然後非零離開——
  // 不能混進「部分失敗」的黃字裡帶過（那正是使用者不會發現的那種失敗）。
  if (result.blocked) {
    console.log(chalk.yellow('\n  ⚠ 更新沒有進行，你的實例維持原樣。\n'));
    console.log('  ' + result.message.split('\n').join('\n  '));
    console.log('');
    process.exit(1);
  }

  if (result.implemented) {
    // message 含部分失敗清單（「部署 X/Y 成功，N 失敗：✗ ...」）——必須印出來，
    // 否則 worker 失敗被綠勾蓋掉（假綠）：cypher 沒部上 → 後面 migrate 打舊 worker 404，
    // 用戶重跑 N 次都不知道根因（壓測 2026-06-11 實證）。
    if (result.message?.includes('失敗')) {
      console.log(chalk.yellow(`\n  ⚠ 部署部分失敗：`));
      console.log(chalk.yellow('    ' + result.message.split('\n').join('\n    ')));
    } else {
      console.log(chalk.green('\n  ✓ 部署完成'));
    }
    // 重跑 seed（薄殼：呼叫 API /init/seed；冪等，覆寫既有）。
    // 修壓測 §4.1.3「update 不做 seed，但 init 提示說 update 會重試 seed」的矛盾。
    const cypherUrl = config.cypher_executor_url
      ?? result.cypherExecutorUrl
      ?? (ctx.workerSubdomain ? `https://arcrun-cypher-executor.${ctx.workerSubdomain}.workers.dev` : '');
    if (cypherUrl) {
      process.stdout.write(chalk.gray('  → 重新 seed recipe（API + auth，由 API 灌入）...'));
      try {
        const res = await fetch(`${cypherUrl}/init/seed`, { method: 'POST' });
        const body = await res.json().catch(() => null) as { success?: boolean; message?: string } | null;
        console.log(res.ok && body?.success
          ? chalk.green(` ✓ ${body.message ?? ''}`)
          : chalk.yellow(` ⚠ ${body?.message ?? `HTTP ${res.status}`}`));
      } catch (e) {
        console.log(chalk.yellow(` ⚠ seed 失敗（${e instanceof Error ? e.message : e}）`));
      }

      // kbdb-base 8.P0：一次性把舊的 per-key cron-idx:{apiKey}:{name} 折進單一 cron-idx:_all。
      // 部署 8.P0 後既有 cron workflow 若不重 push 會停擺（scheduled 只讀新集中 key）→ 這裡冪等補上。
      // 冪等、不刪舊 key、失敗不致命（重跑 acr update 會再試）。
      process.stdout.write(chalk.gray('  → 遷移 cron index（舊 per-key → 集中 key，冪等）...'));
      try {
        const res = await fetch(`${cypherUrl}/webhooks/named/migrate-cron-index`, { method: 'POST' });
        const rawText = await res.text();
        let body: { success?: boolean; migrated?: number; skipped?: number; errors?: string[] } | null = null;
        try { body = JSON.parse(rawText); } catch { /* 非 JSON（如 CF 錯誤頁）→ 用原文 */ }
        if (res.ok && body?.success) {
          console.log(chalk.green(` ✓ migrated ${body.migrated ?? 0}, skipped ${body.skipped ?? 0}`));
        } else {
          // 印 server 回的錯誤內容（截前 200 字）——只回 HTTP status 沒人能診斷
          // （壓測 2026-06-11：404→500 重跑 3 次都看不到根因）。
          const detail = (body?.errors?.join('; ') ?? rawText).slice(0, 200);
          console.log(chalk.yellow(` ⚠ HTTP ${res.status}${detail ? `：${detail}` : ''}`));
          console.log(chalk.yellow('     404 = cypher worker 還是舊版（看上方部署是否有失敗）；500 = server 端錯誤（看上行錯誤內容）'));
        }
      } catch (e) {
        console.log(chalk.yellow(` ⚠ cron index 遷移失敗（${e instanceof Error ? e.message : e}）`));
      }
    }
    console.log('');
  } else {
    console.log(chalk.yellow('  ⚠ 更新尚未自動化：'));
    console.log(chalk.gray('    ' + result.message.split('\n').join('\n    ')) + '\n');
  }
}

/** 從 cypher_executor_url（https://arcrun-cypher-executor.<sub>.workers.dev）抽 subdomain。*/
function extractSubdomain(url?: string): string {
  if (!url) return '';
  const m = url.match(/arcrun-cypher-executor\.([^.]+)\.workers\.dev/);
  return m?.[1] ?? '';
}
