/**
 * /recipes — API recipe CRUD
 *
 * recipe 是「http_request + 參數模板」的具名封裝。
 * 不需要 deploy Worker，執行時由 cypher-executor 直接 fetch。
 *
 * KV 結構：
 *   recipe:{canonical_id}  → RecipeDefinition JSON
 *   idx:{rec_hash}         → canonical_id  （反查索引）
 *
 * 引用方式（workflow config）：
 *   component: "rec_f7e2a1b3"  → 永久穩定，不受改名影響
 *   component: "slack"         → 向前兼容，直接用 canonical_id 查
 */

import { Hono } from 'hono';
import type { Bindings } from '../types';
import { deriveRecipeHash } from '../lib/hash';
import type { ResponseMap } from '../lib/recipe-payload';

export const recipesRouter = new Hono<{ Bindings: Bindings }>();

export interface RecipeDefinition {
  // UUID 身份模型（kbdb-base §7.5.5）：每個 recipe 一誕生領 uuid = 唯一身份。
  // canonical_id / author / 公私 都是屬性，不是身份。身份(uuid) 與歸屬(author) 分離。
  // 舊 recipe 無 uuid → resolveRecipe / migration 兼容（migration 增量補 uuid，不刪舊 key）。
  uuid?: string;            // 唯一身份；舊資料可能缺，讀取時容忍
  author?: string;          // 該 uuid 投稿者（誰投誰負責那版市場數據）；'system' = init-seed 種子
  derived_from?: string;    // 可選溯源：fork 自哪個 uuid（Leo 改 John 版時記 John 的 uuid）
  canonical_id: string;
  hash_id: string;          // rec_xxxxxxxx
  display_name?: string;
  description?: string;
  endpoint: string;
  method?: string;          // GET | POST | PUT | PATCH | DELETE，預設 POST
  headers?: Record<string, string>;
  body?: Record<string, unknown>;
  /**
   * ③ payload 層（SDD workflow-discovery 3.12）：帶 body 的 API 把 payload 收回 recipe，
   * 不必寫進 workflow code。與 `body` 的差別＝支援巢狀 {{var}} 與 dot path、
   * 單一引用保留原型別。兩者並存時 body_template 優先（新欄位贏，舊 recipe 不受影響）。
   */
  body_template?: Record<string, unknown>;
  /**
   * ③ 回應正規化層：各家 API 回應形狀不同（Gemini／Claude／Workers AI），
   * 取值路徑・思考型模型旗標・淨化規則**隨 recipe 走** ⇒ 換源＝換 recipe，不必改 workflow。
   * 未設＝原樣回傳（既有 recipe 行為零變化）。
   */
  response_map?: ResponseMap;
  /**
   * 認證型別。未設＝沿用既有 auth_service 判斷（向後相容）。
   * `binding`＝**免金鑰**，用平台內建能力（env.AI／VECTORIZE／BROWSER／QUEUE），
   * 不是為 Workers AI 開特例——Cloudflare 這一整類都被舊抽象（只認 HTTP+金鑰）排除在外。
   */
  auth?: 'static_key' | 'service_account' | 'oauth2' | 'binding';
  /** auth='binding' 時指定用哪個 binding（例 'AI'／'VECTORIZE'）。 */
  binding_name?: string;
  /**
   * 此 recipe 要用哪個 auth recipe（auth_recipe:{auth_service}）。
   * 讓多個 recipe 共用同一把 auth（例：kbdb_get / kbdb_create_block 都設 "kbdb"）。
   * 未設時 auth-dispatcher fallback 到把 canonical_id 當 service name（向後相容）。
   */
  auth_service?: string;
  credentials_required?: Array<{
    key: string;
    inject_as: string;
  }>;
  // 暴露 consent 閘已移除（leo 2026-06-29 拍板，Arcrun#13）：arcrun 是給 AI 用的系統，
  // 不再對 push/暴露要求人類確認。此欄位保留只為向後相容舊 KV record（讀到不報錯，不再寫入/檢查）。
  exposure_consent?: unknown;
  created_at: number;
  updated_at: number;
}

// ── UUID 身份模型 KV key（kbdb-base §7.5.5）────────────────────────────────────
//   recipe:{uuid}                  → recipe 本體（唯一身份）
//   idx:canonical:{canonical_id}   → JSON array of uuid（同 canonical 多作者版本並存，公庫用）
//   idx:installed:{canonical_id}   → 單一 uuid（本部署執行時用哪個版本；pull/submit 時定）
//   idx:{hash_id}                  → canonical_id（既有 rec_hash 反查，保留）
// 舊資料 recipe:{canonical_id} 不刪，resolveRecipe fallback 讀得到（migration 增量補，不破現況）。
const kIdxCanonical = (canonicalId: string) => `idx:canonical:${canonicalId}`;
const kIdxInstalled = (canonicalId: string) => `idx:installed:${canonicalId}`;

/**
 * 寫一份 recipe（UUID 身份模型）：給定 recipe 已含 uuid → 寫 recipe:{uuid}、
 * 把 uuid 併進 idx:canonical:{canonical_id} 清單、設為本部署 installed（執行時用此版本）、
 * 維護 idx:{hash_id} 反查。private(POST /recipes) 與 public(submit-p) 共用此寫入。
 */
export async function installRecipeRecord(kv: KVNamespace, recipe: RecipeDefinition): Promise<void> {
  const uuid = recipe.uuid!;
  const { canonical_id, hash_id } = recipe;

  const listRaw = await kv.get(kIdxCanonical(canonical_id));
  const uuids: string[] = listRaw ? JSON.parse(listRaw) : [];
  if (!uuids.includes(uuid)) uuids.push(uuid);

  await Promise.all([
    kv.put(`recipe:${uuid}`, JSON.stringify(recipe)),
    kv.put(kIdxCanonical(canonical_id), JSON.stringify(uuids)),
    kv.put(kIdxInstalled(canonical_id), uuid),
    kv.put(`idx:${hash_id}`, canonical_id),
  ]);
}

// POST /recipes — 新增或更新 recipe
recipesRouter.post('/recipes', async (c) => {
  let body: Partial<RecipeDefinition>;
  try {
    body = await c.req.json();
  } catch {
    return c.json({ success: false, error: 'request body 必須為 JSON' }, 400);
  }

  const canonicalId = (body.canonical_id ?? '').trim().toLowerCase();
  if (!canonicalId) return c.json({ success: false, error: 'canonical_id 必填' }, 400);
  if (!body.endpoint) return c.json({ success: false, error: 'endpoint 必填' }, 400);

  const hashId = await deriveRecipeHash(canonicalId);
  const now = Date.now();

  // 私庫（POST /recipes）= 自己地盤，同 canonical 就地更新自己安裝的那份（沿用既有 uuid）。
  // 既有 installed → 沿用其 uuid + created_at；無 → 新領 uuid（首次裝這個 canonical）。
  // 讀取順序：先 UUID 模型（installed→uuid），fallback 舊 key（migration 前的種子）。
  const existing = await resolveRecipe(canonicalId, c.env.RECIPES);

  // 暴露 consent 閘已移除（leo 2026-06-29，Arcrun#13）：直接 push，不攔。

  const recipe: RecipeDefinition = {
    uuid: existing?.uuid ?? crypto.randomUUID(),
    author: body.author ?? existing?.author ?? 'local',
    derived_from: body.derived_from ?? existing?.derived_from,
    canonical_id: canonicalId,
    hash_id: hashId,
    display_name: body.display_name,
    description: body.description,
    endpoint: body.endpoint,
    method: (body.method ?? 'POST').toUpperCase(),
    headers: body.headers,
    body: body.body,
    // ③ payload/回應/binding 三層（3.12）：全選填，沒給就是 undefined＝既有行為
    body_template: body.body_template,
    response_map: body.response_map,
    auth: body.auth,
    binding_name: body.binding_name,
    auth_service: body.auth_service,
    credentials_required: body.credentials_required,
    created_at: existing?.created_at ?? now,
    updated_at: now,
  };

  await installRecipeRecord(c.env.RECIPES, recipe);
  return c.json({ success: true, recipe });
});

// POST /recipes/submit — 公共庫投稿（submit-p）。kbdb-base SDD §7.2/§7.3。
//
// 兩套部署模型：self-hosted cypher = 私庫（直接 POST /recipes 寫自己 KV）；
// 官方 cypher = 公共庫，外部投稿者把修好的 recipe 送來這個端點。
//
// app-store / UUID 模型（§7.5.5）：submit-p = **新增一個作者版本（領新 uuid）**，
// 不覆蓋同 canonical_id。同 canonical 多作者並存（Leo 版、John 版各自 uuid + 市場數據）。
// 公共庫 = 暴露面 → 強制 exposure_consent（mindset §6：暴露需人類明示同意）。
// 投稿者帶的 stat 只當「存證」（誰在何時投了什麼、聲稱打通幾次），寫進 KBDB 一筆
// recipe_submission entry，**不**併進 recipe-stat 真實計數（避免自報數污染市場數據，§7.3）。
// 市場信任靠真實使用累積（5.1），不拿投稿者自報數當門檻 → 不造債。
recipesRouter.post('/recipes/submit', async (c) => {
  let body: Partial<RecipeDefinition> & {
    stat?: { success_count?: number; failure_count?: number };
    submitter?: string;
  };
  try {
    body = await c.req.json();
  } catch {
    return c.json({ success: false, error: 'request body 必須為 JSON' }, 400);
  }

  const canonicalId = (body.canonical_id ?? '').trim().toLowerCase();
  if (!canonicalId) return c.json({ success: false, error: 'canonical_id 必填' }, 400);
  if (!body.endpoint) return c.json({ success: false, error: 'endpoint 必填' }, 400);

  const hashId = await deriveRecipeHash(canonicalId);
  const now = Date.now();

  // 暴露 consent 閘已移除（leo 2026-06-29，Arcrun#13）：公共庫投稿不再需要人類確認。

  // app-store 模型：**領新 uuid = 新增作者版本**，不覆蓋既有 canonical（§7.5.5）。
  const recipe: RecipeDefinition = {
    uuid: crypto.randomUUID(),
    author: body.author ?? body.submitter ?? 'anonymous',
    derived_from: body.derived_from,
    canonical_id: canonicalId,
    hash_id: hashId,
    display_name: body.display_name,
    description: body.description,
    endpoint: body.endpoint,
    method: (body.method ?? 'POST').toUpperCase(),
    headers: body.headers,
    body: body.body,
    auth_service: body.auth_service,
    credentials_required: body.credentials_required,
    created_at: now,
    updated_at: now,
  };

  // 新增作者版本：寫 recipe:{uuid} + 併進 idx:canonical 清單（同 canonical 多版本並存）。
  // installed 也指向這個新版本（官方部署投稿後預設用最新；market 選擇由 §7.5.5 端點處理）。
  await installRecipeRecord(c.env.RECIPES, recipe);

  // stat 存證：寫一筆 recipe_submission entry 進 KBDB（不當門檻，當法律歸責軌跡）。
  // fire-and-forget：存證失敗不擋投稿成功。
  const kbdbBase = (c.env.KBDB_BASE_URL ?? 'https://kbdb.finally.click').replace(/\/$/, '');
  const evidence = {
    content: canonicalId,
    entry_type: 'recipe_submission',
    metadata_json: JSON.stringify({
      uuid: recipe.uuid,
      canonical_id: canonicalId,
      author: recipe.author,
      submitter: body.submitter ?? 'unknown',
      claimed_stat: body.stat ?? null,
      submitted_at: now,
    }),
  };
  const kbdbHeaders: Record<string, string> = { 'Content-Type': 'application/json' };
  if (c.env.KBDB_INTERNAL_TOKEN) kbdbHeaders['Authorization'] = `Bearer ${c.env.KBDB_INTERNAL_TOKEN}`;
  c.executionCtx.waitUntil(
    fetch(`${kbdbBase}/entries`, {
      method: 'POST',
      headers: kbdbHeaders,
      body: JSON.stringify(evidence),
    }).catch(() => undefined),
  );

  return c.json({ success: true, recipe, evidence_recorded: true });
});

// POST /recipes/migrate-uuid — 一次性 migration：把 migration 前的舊 key recipe:{canonical_id}
// （無 uuid）轉成 UUID 身份模型（§7.5.5）。增量寫、**不刪舊 key**（失敗也不破現況；resolveRecipe
// 本就 fallback 舊 key）。冪等：已有 uuid 的跳過。重跑安全。
recipesRouter.post('/recipes/migrate-uuid', async (c) => {
  const list = await c.env.RECIPES.list({ prefix: 'recipe:' });
  let migrated = 0, skipped = 0;
  const errors: string[] = [];
  for (const k of list.keys) {
    try {
      const rec = await c.env.RECIPES.get(k.name, 'json') as RecipeDefinition | null;
      if (!rec || !rec.canonical_id) { skipped++; continue; }
      if (rec.uuid) { skipped++; continue; }          // 已是新模型
      const migrated_recipe: RecipeDefinition = {
        ...rec,
        uuid: crypto.randomUUID(),
        author: rec.author ?? 'system',                // 舊種子歸 system
      };
      await installRecipeRecord(c.env.RECIPES, migrated_recipe);
      migrated++;
    } catch (e) {
      errors.push(`${k.name}: ${e instanceof Error ? e.message : String(e)}`);
    }
  }
  return c.json({ success: errors.length === 0, migrated, skipped, errors });
});

// GET /recipes/:id — 讀取 recipe（支援 canonical_id / rec_hash / uuid）
recipesRouter.get('/recipes/:id', async (c) => {
  const id = c.req.param('id');
  const recipe = await resolveRecipe(id, c.env.RECIPES);
  if (!recipe) return c.json({ success: false, error: `找不到 recipe: ${id}` }, 404);
  return c.json({ success: true, recipe });
});

// GET /recipes — 列出所有 recipe（本部署 KV 全部版本，含多作者）。
// prefix recipe: 同時命中 recipe:{uuid}（新）與 recipe:{canonical_id}（migration 前舊 key）。
// 去重：同 canonical_id 若已有帶 uuid 的版本，捨棄無 uuid 的舊 key 重複項。
recipesRouter.get('/recipes', async (c) => {
  const list = await c.env.RECIPES.list({ prefix: 'recipe:' });
  const all = (await Promise.all(
    list.keys.map(k => c.env.RECIPES.get(k.name, 'json') as Promise<RecipeDefinition | null>)
  )).filter(Boolean) as RecipeDefinition[];

  // canonical → 是否已有帶 uuid 的版本
  const hasUuidVersion = new Set(all.filter(r => r.uuid).map(r => r.canonical_id));
  const recipes = all.filter(r => r.uuid || !hasUuidVersion.has(r.canonical_id));
  return c.json({ success: true, recipes, count: recipes.length });
});

// ── 公庫只讀端點（kbdb-base §7.5.4，公→私 pull + 瀏覽的後端基礎）──────────────────
//   官方 cypher 開公開只讀（無需 api_key，公庫本就公共）。語意 = 「這是公庫，給 self-hosted pull/瀏覽」，
//   含作者維度 + 市場星數（與內部 /recipes 分開命名，公庫的多作者/排序不污染內部）。

/** 從 KBDB 抓 recipe 市場星數（5.1 記的 success/failure）。失敗回 null（端點仍可用，星數缺省）。*/
async function fetchMarketStat(
  env: Bindings,
  canonicalId: string,
): Promise<{ success_count: number; failure_count: number } | null> {
  try {
    const base = (env.KBDB_BASE_URL ?? 'https://kbdb.finally.click').replace(/\/$/, '');
    const headers: Record<string, string> = {};
    if (env.KBDB_INTERNAL_TOKEN) headers['Authorization'] = `Bearer ${env.KBDB_INTERNAL_TOKEN}`;
    const res = await fetch(`${base}/recipe-stats/${encodeURIComponent(canonicalId)}`, { headers });
    if (!res.ok) return null;
    const json = await res.json() as { stat?: { success_count?: number; failure_count?: number } };
    if (!json.stat) return null;
    return {
      success_count: json.stat.success_count ?? 0,
      failure_count: json.stat.failure_count ?? 0,
    };
  } catch {
    return null;
  }
}

// 收集本部署 KV 全部 recipe（去重，與 GET /recipes 同邏輯），給公庫端點共用。
async function listAllRecipes(kv: KVNamespace): Promise<RecipeDefinition[]> {
  const list = await kv.list({ prefix: 'recipe:' });
  const all = (await Promise.all(
    list.keys.map(k => kv.get(k.name, 'json') as Promise<RecipeDefinition | null>),
  )).filter(Boolean) as RecipeDefinition[];
  const hasUuid = new Set(all.filter(r => r.uuid).map(r => r.canonical_id));
  return all.filter(r => r.uuid || !hasUuid.has(r.canonical_id));
}

// GET /public-recipes?q=&limit=&offset= — 搜尋/列出公庫 recipe。
// 同 canonical_id 回多筆（多作者），各附市場星數，供 CC 依數據選（§7.5.5）。
// 落空（q 無命中）→ 回 found:false + 創作引導（§7.5.6），不回空陣列乾等。
recipesRouter.get('/public-recipes', async (c) => {
  const q = (c.req.query('q') ?? '').trim().toLowerCase();
  const limit = Math.min(Number(c.req.query('limit') ?? 50), 200);
  const offset = Number(c.req.query('offset') ?? 0);

  const all = await listAllRecipes(c.env.RECIPES);
  const matched = q
    ? all.filter(r =>
        r.canonical_id.toLowerCase().includes(q) ||
        (r.display_name ?? '').toLowerCase().includes(q) ||
        (r.description ?? '').toLowerCase().includes(q))
    : all;

  if (q && matched.length === 0) {
    // 落空 = 創作入口（§7.5.6）：讓 CC 知道「公庫沒有，可自己做一個成為作者」。
    return c.json({
      found: false,
      query: q,
      hint: `公庫無符合「${q}」的 recipe。可自行建立並 submit-p 投稿成為作者（app-store 模型）。`,
    });
  }

  const page = matched.slice(offset, offset + limit);
  const withStats = await Promise.all(
    page.map(async r => ({
      uuid: r.uuid,
      canonical_id: r.canonical_id,
      author: r.author,
      display_name: r.display_name,
      description: r.description,
      market_stat: await fetchMarketStat(c.env, r.uuid ?? r.canonical_id), // §7.5.h per-uuid
    })),
  );
  return c.json({ found: true, recipes: withStats, count: matched.length });
});

// GET /public-recipes/:canonical_id?author= — 取單一 recipe 全文（pull 用）。
// 不指定 author → 回市場最佳版本（success_count 最高）。落空 → found:false 創作引導（§7.5.6）。
recipesRouter.get('/public-recipes/:canonical_id', async (c) => {
  const canonicalId = c.req.param('canonical_id').trim().toLowerCase();
  const author = c.req.query('author');

  const all = await listAllRecipes(c.env.RECIPES);
  let versions = all.filter(r => r.canonical_id === canonicalId);
  if (author) versions = versions.filter(r => r.author === author);

  if (versions.length === 0) {
    return c.json({
      found: false,
      canonical_id: canonicalId,
      hint: `公庫無 recipe「${canonicalId}」${author ? `（author=${author}）` : ''}。可自行建立並 submit-p 投稿成為作者（app-store 模型）。`,
    });
  }

  // 多作者 → 選市場最佳（success_count 最高；無 stat 視為 0）。
  // §7.5.h：星數 per-uuid（5.1 記 uuid）→ 能真正區分 Leo 版/John 版。舊資料無 uuid fallback canonical_id。
  let best = versions[0];
  let bestStat: { success_count: number; failure_count: number } | null = null;
  let bestScore = -1;
  for (const v of versions) {
    const stat = await fetchMarketStat(c.env, v.uuid ?? v.canonical_id);
    const score = stat?.success_count ?? 0;
    if (score > bestScore) { bestScore = score; best = v; bestStat = stat; }
  }
  return c.json({ found: true, recipe: best, market_stat: bestStat });
});

// DELETE /recipes/:id — 刪除（依 UUID 模型清掉 recipe:{uuid} + installed + canonical 清單裡的該 uuid + 舊 key）
recipesRouter.delete('/recipes/:id', async (c) => {
  const id = c.req.param('id');
  const recipe = await resolveRecipe(id, c.env.RECIPES);
  if (!recipe) return c.json({ success: false, error: `找不到 recipe: ${id}` }, 404);

  const canonicalId = recipe.canonical_id;
  const ops: Promise<unknown>[] = [
    c.env.RECIPES.delete(`idx:${recipe.hash_id}`),
    c.env.RECIPES.delete(`recipe:${canonicalId}`),      // 舊 key（若存在）
  ];
  if (recipe.uuid) {
    ops.push(c.env.RECIPES.delete(`recipe:${recipe.uuid}`));
    // 從 canonical 清單移除此 uuid；若清單空了連 installed 一起清
    const listRaw = await c.env.RECIPES.get(kIdxCanonical(canonicalId));
    const uuids: string[] = listRaw ? JSON.parse(listRaw) : [];
    const left = uuids.filter(u => u !== recipe.uuid);
    if (left.length > 0) {
      ops.push(c.env.RECIPES.put(kIdxCanonical(canonicalId), JSON.stringify(left)));
      // installed 若指向被刪的 uuid → 改指剩下第一個
      const installed = await c.env.RECIPES.get(kIdxInstalled(canonicalId));
      if (installed === recipe.uuid) ops.push(c.env.RECIPES.put(kIdxInstalled(canonicalId), left[0]));
    } else {
      ops.push(c.env.RECIPES.delete(kIdxCanonical(canonicalId)));
      ops.push(c.env.RECIPES.delete(kIdxInstalled(canonicalId)));
    }
  }
  await Promise.all(ops);
  return c.json({ success: true, deleted: recipe.uuid ?? canonicalId });
});

/**
 * 用 canonical_id / rec_hash / uuid 查 recipe（執行時的解析入口）。
 * UUID 身份模型（§7.5.5）+ 向後相容（migration 前的舊 key）：
 *   1. id 是 uuid（recipe:{uuid} 直接存在）→ 直接回。
 *   2. rec_xxxxxxxx → idx:{hash} 反查 canonical_id → 再走 canonical 解析。
 *   3. canonical_id → 先查 idx:installed:{canonical_id}（本部署安裝的唯一版本）→ recipe:{uuid}；
 *      查不到 fallback 舊 key recipe:{canonical_id}（種子 / migration 前資料）。
 * 執行鏈路（component-loader/auth-dispatcher）都經此 → 不破執行。
 */
export async function resolveRecipe(
  id: string,
  kv: KVNamespace,
): Promise<RecipeDefinition | null> {
  // 1. 直接 uuid（pull / market 指定版本時用）
  const direct = await kv.get(`recipe:${id}`, 'json') as RecipeDefinition | null;
  if (direct && direct.uuid) return direct;
  // direct 命中但無 uuid = 舊 key recipe:{canonical_id}（migration 前）→ 仍可用，但繼續嘗試 installed 拿新版
  // （installed 優先：migration 後新版在 recipe:{uuid}，舊 key 為 fallback）

  // 2. rec_hash 反查 canonical_id
  let canonicalId = id;
  if (id.startsWith('rec_')) {
    const looked = await kv.get(`idx:${id}`);
    if (!looked) return direct; // hash 查不到，回 step1 結果（通常 null）
    canonicalId = looked;
  }

  // 3. canonical → installed uuid → recipe:{uuid}；fallback 舊 key
  const installedUuid = await kv.get(kIdxInstalled(canonicalId));
  if (installedUuid) {
    const byUuid = await kv.get(`recipe:${installedUuid}`, 'json') as RecipeDefinition | null;
    if (byUuid) return byUuid;
  }
  // fallback：舊 key recipe:{canonical_id}（direct 若正是它，已在手上）
  return direct ?? (await kv.get(`recipe:${canonicalId}`, 'json'));
}

// ── Auth Recipe ────────────────────────────────────────────────────────────────

export type AuthPrimitive = 'static_key' | 'oauth2' | 'service_account' | 'mtls';

export interface SecretRequirement {
  key: string;          // CREDENTIALS_KV 的名稱（e.g. "notion_token"）
  label: string;        // CLI/UI 顯示（e.g. "Internal Integration Token"）
  type?: 'string' | 'json_blob';  // default: string
  help?: string;
  help_url?: string;
  optional?: boolean;
}

export interface AuthInjectSpec {
  header?: Record<string, string>;  // e.g. { Authorization: "Bearer {{secret.token}}" }
  query?: Record<string, string>;
  body?: Record<string, string>;
  // path：注入 endpoint URL path 的 secret（auth-recipe.md §六，2026-05-29 加）。
  // 解 telegram 類「token 在 URL path」（/bot{token}/）—— header/query/body 都不適用。
  // key = 模板變數名（recipe endpoint 用 {{auth.K}} 引用），value = {{secret.X}} 模板。
  // auth_static_key WASM 解密後輸出為 auth_path → auth-dispatcher 帶進 _auth_path → makeRecipeRunner interpolate。
  path?: Record<string, string>;
}

export interface AuthRecipeDefinition {
  kind: 'auth_recipe';
  service: string;              // canonical_id，e.g. "notion"
  version: number;
  primitive: AuthPrimitive;
  base_url: string;
  display_name?: string;
  description?: string;

  // service_account 專用
  service_account_kind?: 'google_jwt';
  token_exchange?: {
    endpoint: string;
    scopes: string[];
  };

  required_secrets: SecretRequirement[];
  inject: AuthInjectSpec;

  created_at: number;
  updated_at: number;
}

/** 查 auth recipe（KV key: auth_recipe:{service}）*/
export async function resolveAuthRecipe(
  service: string,
  kv: KVNamespace,
): Promise<AuthRecipeDefinition | null> {
  return kv.get(`auth_recipe:${service}`, 'json');
}

// POST /auth-recipes — 新增或更新 auth recipe
recipesRouter.post('/auth-recipes', async (c) => {
  let body: Partial<AuthRecipeDefinition>;
  try {
    body = await c.req.json();
  } catch {
    return c.json({ success: false, error: 'request body 必須為 JSON' }, 400);
  }

  const service = (body.service ?? '').trim().toLowerCase();
  if (!service) return c.json({ success: false, error: 'service 必填' }, 400);
  if (!body.primitive) return c.json({ success: false, error: 'primitive 必填' }, 400);
  if (!body.base_url) return c.json({ success: false, error: 'base_url 必填' }, 400);
  if (!body.required_secrets?.length) return c.json({ success: false, error: 'required_secrets 必填' }, 400);
  if (!body.inject) return c.json({ success: false, error: 'inject 必填' }, 400);

  // help_url 一律必填（不分冷熱門服務）：每個 secret 都要留官方文件連結，
  // 讓 AI/使用者不必猜該去哪設定、也不會搜到錯誤來源（壓測 §recipe 設定說明）。
  const missingHelp = body.required_secrets.filter(s => !s.help_url || !/^https?:\/\//.test(s.help_url));
  if (missingHelp.length > 0) {
    return c.json({
      success: false,
      error: `每個 required_secret 必須有 help_url（官方文件連結，http(s)://）。缺：${missingHelp.map(s => s.key).join(', ')}`,
      requires: 'help_url',
    }, 400);
  }

  const now = Date.now();
  const existing = await c.env.RECIPES.get(`auth_recipe:${service}`, 'json') as AuthRecipeDefinition | null;

  const recipe: AuthRecipeDefinition = {
    kind: 'auth_recipe',
    service,
    version: body.version ?? 1,
    primitive: body.primitive,
    base_url: body.base_url,
    display_name: body.display_name,
    description: body.description,
    service_account_kind: body.service_account_kind,
    token_exchange: body.token_exchange,
    required_secrets: body.required_secrets,
    inject: body.inject,
    created_at: existing?.created_at ?? now,
    updated_at: now,
  };

  await c.env.RECIPES.put(`auth_recipe:${service}`, JSON.stringify(recipe));
  return c.json({ success: true, recipe });
});

// GET /auth-recipes — 列出所有 auth recipe
recipesRouter.get('/auth-recipes', async (c) => {
  const list = await c.env.RECIPES.list({ prefix: 'auth_recipe:' });
  const recipes = await Promise.all(
    list.keys.map(k => c.env.RECIPES.get(k.name, 'json'))
  );
  return c.json({ success: true, recipes: recipes.filter(Boolean), count: recipes.length });
});

// GET /auth-recipes/:service — 讀取單一 auth recipe
recipesRouter.get('/auth-recipes/:service', async (c) => {
  const service = c.req.param('service');
  const recipe = await resolveAuthRecipe(service, c.env.RECIPES);
  if (!recipe) return c.json({ success: false, error: `找不到 auth recipe: ${service}` }, 404);
  return c.json({ success: true, recipe });
});

// DELETE /auth-recipes/:service — 刪除 auth recipe
recipesRouter.delete('/auth-recipes/:service', async (c) => {
  const service = c.req.param('service');
  const recipe = await resolveAuthRecipe(service, c.env.RECIPES);
  if (!recipe) return c.json({ success: false, error: `找不到 auth recipe: ${service}` }, 404);
  await c.env.RECIPES.delete(`auth_recipe:${service}`);
  return c.json({ success: true, deleted: service });
});
