/**
 * 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';

// ── credential-store 遷移 T6/T7（方案 A，D19）────────────────────────────────
//
// 密文值住 cypher-executor 自己的 per-script secrets（T5 寫入）。解密發生在獨立的
// auth_static_key / auth_service_account worker 上，它們讀不到 cypher 的 secrets。
// 故 cypher 這一層先查 D1 拿 secret_ref → 用 secret_get(ref)（即 env[ref]，T4）取明文
// → 塞進送給 auth WASM 的 payload 新欄位 `resolved_secrets`。WASM 收到優先用它，沒有
// 才 fallback 舊 KV + crypto_decrypt（那個 fallback 即 T7 雙讀）。
//
// 嚴格邊界（rule 02 §2.2）：本檔只做「查 D1 ref → secret_get 取值 → 當字串塞 payload」。
// **不解密、不展開模板、不組 JWT**——secret_get 的實作（env[ref]）在 wasi-shim host function
// 內，解密/注入邏輯仍全在 WASM 零件。

/** D1 credentials 目錄一列（只取本檔需要的欄位）。 */
interface CredentialRefRow {
  name: string;
  secret_ref: string;
}

/**
 * 對一組 credential name，從新家（cypher per-script secrets）取明文。
 *
 * 流程：查 D1 `credentials`（api_key + name）拿 `secret_ref` → 用 `secret_get(ref)`
 * （host function，實作 = env[ref]）取值。
 *
 * ⚠️ 只把「D1 有 ref 且 secret_get 真的取到值」的 name 放進回傳 map。查不到 ref、
 * 或 secret_get 回 null（新家還沒這把值）→ **該 name 缺席**（不是放空字串！），
 * 讓 WASM 對這把 key 走 fallback 舊 KV 路徑（T7 雙讀）。放空字串會讓 WASM 誤判命中用空值。
 *
 * 取到值的 name 順手更新 D1 `last_used_at`（§2.5 治理面 last_used）。
 *
 * D1 未建表 / migration 未跑 / CREDENTIALS_DB 未綁 → 回空 map（整組走 fallback），
 * 不 throw——遷移過渡期（雙讀）本就允許「新家還沒資料」。
 */
export async function resolveSecretsFromNewHome(
  env: Bindings,
  apiKey: string,
  names: string[],
): Promise<Record<string, string>> {
  const resolved: Record<string, string> = {};
  if (names.length === 0) return resolved;

  const db = env.CREDENTIALS_DB;
  if (!db) return resolved; // 未綁 D1 → 整組走 fallback

  // 1. 查 D1 拿每個 name 的 secret_ref
  let rows: CredentialRefRow[];
  try {
    const placeholders = names.map(() => '?').join(', ');
    const result = await db
      .prepare(
        `SELECT name, secret_ref FROM credentials
         WHERE api_key = ? AND name IN (${placeholders})`,
      )
      .bind(apiKey, ...names)
      .all<CredentialRefRow>();
    rows = result.results ?? [];
  } catch {
    // D1 未建表 / query 失敗 → 過渡期整組走 fallback（雙讀），不假綠
    return resolved;
  }
  if (rows.length === 0) return resolved;

  // 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 row of rows) {
    const value = await secretGet(row.secret_ref);
    // null（新家沒這把值 / 非 CRED_ 前綴被拒）→ 不放進 map，讓 WASM fallback 舊 KV
    if (value === null) continue;
    resolved[row.name] = value;
    resolvedNames.push(row.name);
  }

  // 3. 順手更新 last_used_at（只更新真的從新家取到值的 name）
  if (resolvedNames.length > 0) {
    try {
      const now = Math.floor(Date.now() / 1000);
      const placeholders = resolvedNames.map(() => '?').join(', ');
      await db
        .prepare(
          `UPDATE credentials SET last_used_at = ?
           WHERE api_key = ? AND name IN (${placeholders})`,
        )
        .bind(now, apiKey, ...resolvedNames)
        .run();
    } catch {
      // last_used 更新失敗不影響注入主流程（治理面欄位，非關鍵路徑）
    }
  }

  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);

  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>;
}
