/**
 * Auth Dispatcher
 *
 * 對需要認證的零件,在執行前 HTTP POST 到對應的 auth primitive Worker,
 * 取回 auth_headers / auth_query / auth_body 合併進節點 context。
 *
 * 嚴格邊界(rule 02 §2.2):
 *   - 本檔**不做**任何 credential 解密 / template 展開 / JWT 簽章
 *   - 那些全部在 auth primitive WASM 零件內執行(透過 host function `secret_get` 等)
 *   - 本檔只做「查 recipe 決定走哪個 primitive Worker」+「HTTP fetch 取回注入結果」
 *
 * 目前階段接上 `auth_static_key` + `auth_service_account` + `auth_oauth2`,
 * Phase 4 剩 `auth_mtls`（mTLS handshake 在 Worker runtime 層）。
 *
 * 執行時機:graph-executor 在節點 runner 執行前呼叫,取回的 ctx 會:
 *   1. 本 dispatcher 命中 → return enriched ctx
 *   2. 沒命中 → ctx 原樣往下(T10 起舊的 injectCredentials 雙讀 fallback 已移除)
 */

import type { Bindings } from '../types';
import { resolveAuthRecipe, resolveRecipe } from '../routes/recipes';
import { wasmWorkerUrl } from '../lib/component-loader';
import { createArcrunHostFunctions } from '../lib/wasi-shim';
import { getCredentialSecretRefs, touchLastUsed } from '../routes/credentials';

// ── credential-store 遷移 T6/T7（方案 A，D19）＋ D38 圍牆修復（2026-08-07）───────────
//
// 密文值住 cypher-executor 自己的 per-script secrets（T5 寫入）。解密發生在獨立的
// auth_static_key / auth_service_account worker 上，它們讀不到 cypher 的 secrets。
// 故 cypher 這一層先取這個租戶的 credential 目錄（name → secret_ref）→ 用 secret_get(ref)
// （即 env[ref]，T4）取明文 → 塞進送給 auth WASM 的 payload 新欄位 `resolved_secrets`。
// WASM 收到優先用它，沒有才 fallback 舊 KV + crypto_decrypt（那個 fallback 即 T7 雙讀）。
//
// D38（leo 2026-06-14 立、2026-08-07 擴大）：目錄不再直連 D1，改走 KBDB HTTP API
// （`credentials.ts` 的 `getCredentialSecretRefs`，內建 60 秒租戶級快取——這是熱路徑，
// 每次 workflow 執行都會呼叫，映射「幾乎不變」故快取後多數命中零網路呼叫，效能不因改走
// API 而變差，見 credentials.ts 檔頭「效能」段的實測數字）。
//
// 嚴格邊界（rule 02 §2.2）：本檔只做「查目錄拿 ref → secret_get 取值 → 當字串塞 payload」。
// **不解密、不展開模板、不組 JWT**——secret_get 的實作（env[ref]）在 wasi-shim host function
// 內，解密/注入邏輯仍全在 WASM 零件。

/**
 * 對一組 credential name，從新家（cypher per-script secrets）取明文。
 *
 * 流程：查 KBDB credential 目錄（api_key + name，快取命中零網路呼叫）拿 `secret_ref`
 * → 用 `secret_get(ref)`（host function，實作 = env[ref]）取值。
 *
 * ⚠️ 只把「目錄有 ref 且 secret_get 真的取到值」的 name 放進回傳 map。查不到 ref、
 * 或 secret_get 回 null（新家還沒這把值）→ **該 name 缺席**（不是放空字串！），
 * 讓 WASM 對這把 key 走 fallback 舊 KV 路徑（T7 雙讀）。放空字串會讓 WASM 誤判命中用空值。
 *
 * 取到值的 name 順手更新 last_used_at（§2.5 治理面 last_used，見 touchLastUsed——
 * fire-and-forget、非同步、不阻塞本函式回傳，失敗吞掉）。
 *
 * KBDB 不可達 / 這個租戶還沒有任何 credential → 回空 map（整組走 fallback），
 * 不 throw——遷移過渡期（雙讀）本就允許「新家還沒資料」。
 */
/** credential name → 明文值對照（獨立型別別名，避免函式簽章直接內嵌逗號分隔泛型）。 */
type ResolvedSecretMap = Record<string, string>;

export async function resolveSecretsFromNewHome(
  env: Bindings,
  apiKey: string,
  names: string[],
): Promise<ResolvedSecretMap> {
  const resolved: ResolvedSecretMap = {};
  if (names.length === 0) return resolved;

  // 1. 拿這個租戶的 credential 目錄（name → secret_ref，快取層見 credentials.ts）
  const refs = await getCredentialSecretRefs(env, apiKey);
  if (Object.keys(refs).length === 0) return resolved; // 目錄空 / KBDB 不可達 → 整組走 fallback

  // 2. 用 secret_ref 從新家取值（host function secret_get = env[ref]）
  const secretGet = createArcrunHostFunctions(env, apiKey).secret_get;
  if (!secretGet) return resolved; // host function 未就緒 → 走 fallback

  const resolvedNames: string[] = [];
  for (const name of names) {
    const ref = refs[name];
    if (!ref) continue; // 目錄沒這個 name → 缺席，走 fallback
    const value = await secretGet(ref);
    // null（新家沒這把值 / 非 CRED_ 前綴被拒）→ 不放進 map，讓 WASM fallback 舊 KV
    if (value === null) continue;
    resolved[name] = value;
    resolvedNames.push(name);
  }

  // 3. 順手更新 last_used_at（只更新真的從新家取到值的 name；fire-and-forget，非關鍵路徑）
  if (resolvedNames.length > 0) touchLastUsed(env, apiKey, resolvedNames);

  return resolved;
}

/** 對應 Phase 1-4 會部署的 auth primitive Worker */
const SUPPORTED_PRIMITIVES = new Set(['static_key', 'service_account', 'oauth2']);

/** auth primitive 本身的 componentId(避免自引用) */
const AUTH_PRIMITIVE_IDS = new Set([
  'auth_static_key',
  'auth_service_account',
  'auth_oauth2',
  'auth_mtls',
]);

/**
 * 試著對零件做 auth 注入。
 * - 命中(有對應 auth recipe 且 primitive 已支援)→ 回傳注入後的 ctx
 * - 未命中 → 回傳 null(呼叫端繼續跑舊路徑)
 */
export async function tryAuthDispatch(
  componentId: string,
  input: Record<string, unknown>,
  env: Bindings,
  apiKey: string,
): Promise<Record<string, unknown> | null> {
  if (AUTH_PRIMITIVE_IDS.has(componentId)) {
    // auth primitive 本身不需要再做 auth
    return null;
  }

  // 決定 auth service name：
  //   1. 若 API recipe 宣告了 auth_service（例 recipe:kbdb_get → "kbdb"）→ 用它，
  //      讓多個 recipe 共用同一把 auth_recipe（不必每個 action 複製 auth recipe）。
  //   2. 否則 fallback 到把 componentId 當 service name（向後相容舊行為）。
  let service = componentId;
  const apiRecipe = await resolveRecipe(componentId, env.RECIPES);
  if (apiRecipe?.auth_service) {
    service = apiRecipe.auth_service;
  }

  const recipe = await resolveAuthRecipe(service, env.RECIPES);
  if (!recipe) return null;
  if (!SUPPORTED_PRIMITIVES.has(recipe.primitive)) return null;

  // T6：先從新家（cypher per-script secrets）取 recipe 需要的 credential 明文。
  // 拿到的塞進 `resolved_secrets`，WASM 優先用；沒拿到的 name 缺席 → WASM fallback 舊 KV（T7）。
  const secretNames = recipe.required_secrets
    .filter(s => !s.optional)
    .map(s => s.key);
  const resolvedSecrets = await resolveSecretsFromNewHome(env, apiKey, secretNames);

  // 走新路徑:HTTP POST 到對應 auth primitive Worker
  // 走 workers.dev 避開同 zone 死鎖（P0 #9）
  const primitiveUrl = wasmWorkerUrl(`auth_${recipe.primitive}`, env.WORKER_SUBDOMAIN);
  const res = await fetch(primitiveUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      action: 'authenticate',
      api_key: apiKey,
      service,
      // 只在有取到值時帶上（空物件也無妨，WASM 對 nil/空 map 同樣 fallback）
      resolved_secrets: resolvedSecrets,
    }),
  });

  if (!res.ok) {
    const text = await res.text().catch(() => '');
    throw new Error(
      `auth primitive "${recipe.primitive}" 回傳 ${res.status}: ${text.slice(0, 200)}`,
    );
  }

  const result = await res.json().catch(() => null) as {
    success?: boolean;
    error?: string;
    auth_headers?: Record<string, string>;
    auth_query?: Record<string, string>;
    auth_body?: Record<string, string>;
    auth_path?: Record<string, string>;
  } | null;

  if (!result || result.success === false) {
    throw new Error(
      `auth primitive 失敗: ${result?.error ?? '未知錯誤'}`,
    );
  }

  return {
    ...input,
    _auth_headers: result.auth_headers ?? {},
    _auth_query: result.auth_query ?? {},
    _auth_body: result.auth_body ?? {},
    _auth_path: result.auth_path ?? {},
  };
}

// ── 用戶面 {{credential.NAME}} 注入（design §8）────────────────────────────────

/** 匹配 {{credential.NAME}}（NAME 為 word 字元） */
const CREDENTIAL_REF = /\{\{credential\.(\w+)\}\}/g;

/** 遞迴收集任意值（string / 物件 / 陣列）裡所有 {{credential.NAME}} 的 NAME */
function collectCredentialNames(value: unknown, out: Set<string>): void {
  if (typeof value === 'string') {
    for (const m of value.matchAll(CREDENTIAL_REF)) out.add(m[1]);
  } else if (Array.isArray(value)) {
    for (const v of value) collectCredentialNames(v, out);
  } else if (value && typeof value === 'object') {
    for (const v of Object.values(value as Record<string, unknown>)) collectCredentialNames(v, out);
  }
}

/** 遞迴把 {{credential.NAME}} 替換成 resolved[NAME]（未知 name 原樣保留） */
function replaceCredentialRefs(value: unknown, resolved: Record<string, string>): unknown {
  if (typeof value === 'string') {
    return value.replace(CREDENTIAL_REF, (orig, name: string) =>
      Object.prototype.hasOwnProperty.call(resolved, name) ? resolved[name] : orig,
    );
  }
  if (Array.isArray(value)) return value.map((v) => replaceCredentialRefs(v, resolved));
  if (value && typeof value === 'object') {
    const out: Record<string, unknown> = {};
    for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
      out[k] = replaceCredentialRefs(v, resolved);
    }
    return out;
  }
  return value;
}

/**
 * 展開節點 data 裡用戶寫的 `{{credential.NAME}}`（design §8）。
 *
 * 嚴格邊界（rule 02 §2.2）：本函式**不解密**。偵測到 {{credential.X}} 後，把 names 交給
 * auth_static_key WASM 的 `resolve_credentials` action（WASM 內 kv_get + crypto_decrypt），
 * 拿回明文後只做字串回填。本檔不解密、不持有任何金鑰。
 *
 * - 無 {{credential.}} → 原樣回傳（不打 WASM，零開銷）
 * - 解密失敗 / 缺 credential → throw（誠實報錯，不假綠）
 */
export async function resolveCredentialRefs(
  data: Record<string, unknown>,
  env: Bindings,
  apiKey: string,
): Promise<Record<string, unknown>> {
  const names = new Set<string>();
  collectCredentialNames(data, names);
  if (names.size === 0) return data;

  const nameList = [...names];

  // T6：先從新家取這些 credential 的明文，塞 `resolved_secrets`；WASM 優先用它，
  // 沒取到的 name 缺席 → WASM 的 resolve_credentials 分支 fallback 舊 KV（T7）。
  const resolvedSecrets = await resolveSecretsFromNewHome(env, apiKey, nameList);

  // ── 全部取到 → 不必打 auth_static_key worker（Arcrun#124，2026-08-14）────────────
  //
  // 這條路徑上，那顆 worker 唯一比這裡多做的事，是「幫**沒取到**的 name 回頭翻舊 KV」
  // （T7 雙讀 fallback）。當 nameList 每一個都已經從新家取到明文時，它收到
  // `resolved_secrets` 後就是原樣回傳——回填本來就由下面的 `replaceCredentialRefs`
  // 在本檔做。**所以那趟往返在「全取到」時不產生任何結果差異。**
  //
  // 為什麼這不是效能微調，是修 bug：`wasmWorkerUrl` 推導出的
  // `arcrun-auth-static-key.{WORKER_SUBDOMAIN}.workers.dev` **在網頁安裝器裝出來的實例上不存在**
  // （首裝只裝 cypher-executor / kbdb / http_request / code 四顆，auth primitive 不在其中，
  // 也沒有「用到才長」的機制）。於是 fetch 撞 CF 邊緣的 404 `error code: 1042`
  // ⇒ 下面的 `!res.ok` throw ⇒ **凡是工作流用到 {{credential.X}} 的節點全部 500**。
  // 實測：leo21c 與全新安裝的 youlin 兩台，問答與寫入路徑一字不差地同死。
  //
  // 🔴 給下一個人：**這個 1042 跟 d2048e2／95a1462 那兩輪的 1042 不是同一個病。**
  // 那兩輪是「same-zone fetch 被 CF 擋」（解法＝`global_fetch_strictly_public` flag，
  // 且 service binding 的解法**已被 revert、不要再拿回來**）。本案是「**目標 worker 根本不存在**」
  // ——同一個錯誤碼、兩種原因。照舊帳去動 flag 或 binding 對本案完全無效（flag 早就在了）。
  //
  // 邊界（rule 02 §2.2）不變：本檔仍然不解密、不展開模板、不組 JWT。
  // 明文的來源是 `secret_get`（host function，實作 = env[ref]），與改動前完全同一條路；
  // 這裡只是不再為了「拿一份自己已經拿到的東西」去 fetch 一顆可能不存在的 worker。
  //
  // 刻意保留 WASM 路徑：只要有任何一個 name 沒從新家取到（舊租戶、遷移過渡期的 KV 存量），
  // 就照舊全量交給 WASM 走 T7 雙讀——**不縮減既有能力，只在等價時省掉那一跳**。
  if (nameList.every((n) => Object.prototype.hasOwnProperty.call(resolvedSecrets, n))) {
    return replaceCredentialRefs(data, resolvedSecrets) as Record<string, unknown>;
  }

  const url = wasmWorkerUrl('auth_static_key', env.WORKER_SUBDOMAIN);
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      action: 'resolve_credentials',
      api_key: apiKey,
      names: nameList,
      resolved_secrets: resolvedSecrets,
    }),
  });

  if (!res.ok) {
    const text = await res.text().catch(() => '');
    throw new Error(`credential resolve 回傳 ${res.status}: ${text.slice(0, 200)}`);
  }

  const result = (await res.json().catch(() => null)) as {
    success?: boolean;
    error?: string;
    credentials?: Record<string, string>;
  } | null;

  if (!result || result.success === false) {
    throw new Error(`credential resolve 失敗: ${result?.error ?? '未知錯誤'}`);
  }

  return replaceCredentialRefs(data, result.credentials ?? {}) as Record<string, unknown>;
}
