/**
 * Credentials API — 多租戶 credential 管理
 *
 * 現行做法（D19「擁有目錄，不擁有內容物」；規範見 .claude/rules/01-tech-stack.md
 * 「Credential 儲存規範」。arcrun 不自管任何加密金鑰）：
 *
 *   寫入（POST 建立 / PUT 覆寫）：
 *     1. 密文值 PUT 進 CF Workers per-script Secrets（掛在本 worker 上，管理 API 唯寫，
 *        arcrun 自己也讀不回值——D19「不持有內容物」）。
 *     2. 目錄（api_key/name/service/sensitivity/secret_ref/created_at/last_used_at，
 *        **不含密文**）走 KBDB HTTP API 寫，不再直連任何 D1。
 *   不再寫 KV / 不再寫明文密文到 D1。
 *
 * 傳輸格式：client **不做** AES-GCM 加密，明文值經 TLS 送到 cypher，cypher 短暫在記憶體
 * 經手明文（不落地、不持久、不持金鑰）後直接 PUT 進 Workers Secrets。（此為 2026-07-03
 * 定案並已落地的做法，取代更早的 `{name, encrypted, iv}` 格式；rule 01 已同步。）
 *
 * D38 圍牆修復（總管交辦，2026-08-07；leo「任何東西禁止用 SQL 語句存取資料，一律 API」）：
 * 目錄舊家是 KBDB 裡多開的一張獨立 credentials 表（0002_credentials.sql，違規），現改走
 * KBDB 三張核心表——entries 表一列（entry_type='credential'，page_name=name 當冪等鍵，
 * owner_id=api_key 隔離租戶，其餘欄位打包進 metadata_json），template 定義見
 * kbdb/migrations/0005_credential_template.sql，舊表資料遷移＋拆表見 0006。連法比照既有
 * execution-logger.ts / portal.ts 慣例：kbdbBase(env) 組 base+headers，直接 fetch KBDB
 * HTTP API，不經自己的 /kbdb/* proxy route（那支是給 CLI 用的，server 端直連 base 更省一跳）。
 *
 * 效能（D38 評估要求「帶數字」，見 system-dev/wiki/decisions-summary.md D38 段）：
 * 熱路徑（auth-dispatcher.ts resolveSecretsFromNewHome，每次 workflow 執行都會查一次）原本
 * 直連 D1、零快取；改走 HTTP 後若一樣「每次查一次」延遲只會變差（多一趟公網往返）。這份
 * name→secret_ref 映射「幾乎不變」（D38 評估原話），故本檔加一個租戶級記憶體快取
 * （dirCache，per-isolate，TTL 60 秒），寫入（POST/PUT/DELETE）時主動失效，讓熱路徑多數
 * 命中零網路呼叫。見下方 getCredentialDirectory / invalidateCredentialCache。
 *
 * 治理端點：
 *   - `GET /credentials`：改讀 KBDB entries（與 `/credentials/catalog` 共用同一份查詢，同時
 *     保留 `/catalog` 別名，Console 既有呼叫不受影響）。
 *   - `DELETE /credentials/:name`：先查 KBDB 拿 secret_ref → 有則刪 Workers Secret + entries
 *     row；沒有（credential 從未回填過，只存在舊 KV）→ fallback 刪舊 KV key，避免刪不掉的
 *     孤兒資料。
 */

import { Hono } from 'hono';
import type { Bindings } from '../types';
import { sha256Prefix } from '../lib/hash';
import { kbdbBase } from './kbdb-proxy';

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

/** 本 worker 的 script name（wrangler.toml `name`），官方與 self-hosted 都用同一個名字，
 *  只有帳號（CF_ACCOUNT_ID）不同——CF Workers Scripts secrets API 是 accountId+scriptName 定位。*/
const CYPHER_SCRIPT_NAME = 'arcrun-cypher-executor';

/**
 * secret_ref 命名規則（以 CRED_ 前綴隔離命名空間）：
 *   CRED_<NAME 大寫>_<sha256(api_key) 前 8 碼大寫>
 * 加 api_key 的 hash 是為了避免跨租戶同名 credential（如兩個用戶都存 telegram_bot_token）
 * 撞名覆蓋彼此的 Workers Secret（secret 是掛在同一個 worker 上、全域命名空間，沒有租戶
 * 隔離機制，必須自己用命名衍生隔離）。name 先前已被 validateName() 限制為 \w+，
 * 大寫後仍是合法的 env var 名（CF secret name 只接受 [A-Za-z0-9_]）。
 */
async function deriveSecretRef(apiKey: string, name: string): Promise<string> {
  const hash8 = await sha256Prefix(apiKey);
  return `CRED_${name.toUpperCase()}_${hash8.toUpperCase()}`;
}

/**
 * 存一筆 credential 進「新家」（CF Workers Secrets 明文 + D1 目錄）。
 *
 * 給 OAuth callback 這類非 /credentials 端點的內部呼叫者用（存 provider token 供
 * auth primitive 零件取用）。與 `POST /credentials` 共用同一條寫入路徑，
 * 確保只有一套儲存。
 */
export async function storeCredential(
  env: Bindings,
  apiKey: string,
  name: string,
  value: string,
  service: string | null,
): Promise<void> {
  const secretRef = await deriveSecretRef(apiKey, name);
  await putWorkerSecret(env, secretRef, value);
  await upsertCredentialEntry(env, apiKey, name, service, 'standard', secretRef);
}

function validateName(name: unknown): name is string {
  return typeof name === 'string' && /^\w+$/.test(name);
}

function validSensitivity(s: unknown): s is 'standard' | 'high' {
  return s === 'standard' || s === 'high';
}

/**
 * 呼叫 CF Workers Scripts secrets 管理 API，把明文值存進本 worker 的 per-script secret。
 * 唯寫：這支 API 不回傳任何既有 secret 的值，只能 create/update/delete/list 名字（D19 對齊）。
 *
 * `tokenOverride`（2026-08-14，arcrun-rag#99：全新帳號卡在註冊，`writable:false`）：
 * 本 worker 自己的 `env.CF_SECRETS_API_TOKEN` 從安裝那天起就沒被種過（07-29 已知缺口，記在
 * pending-changes.md「credential 走 n8n 模式」——當時只降級某個功能；D61 認證分離之後升級成
 * 「連第一個帳號都建不起來」的硬斷點，因為 `/console/setup`／`/portal/admin/bootstrap`
 * 現在都走這條寫入路徑）。安裝精靈裝機當下手上有一把**自己還有效**的 OAuth token
 * （`workers-scripts.write` scope，跟部署零件、種 credential 用的是同一把——見
 * `installer/oauth-prototype/worker.js` 的 `putWorkerSecretDirect`/`seedCredential`，
 * 是同一個「安裝器代寫」精神，D36 第1步）。讓呼叫端把這把 token **隨請求帶入、不落地**，
 * 補的正是「cypher 自己永遠拿不到長效寫入憑證」這個結構性缺口，不是新開一條路。
 * 優先權：呼叫端提供 > worker 自身 env。
 */
export async function putWorkerSecret(env: Bindings, secretRef: string, value: string, tokenOverride?: string): Promise<void> {
  const token = tokenOverride || env.CF_SECRETS_API_TOKEN;
  if (!token || !env.CF_ACCOUNT_ID) {
    throw new Error(
      '此 worker 缺 CF_SECRETS_API_TOKEN / CF_ACCOUNT_ID 設定，寫入路徑未就緒（見 ' +
      'credential-store-migration.md T3：acr init/update 應確保這兩項就緒）',
    );
  }
  const url = `https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/workers/scripts/${CYPHER_SCRIPT_NAME}/secrets`;
  const res = await fetch(url, {
    method: 'PUT',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ name: secretRef, text: value, type: 'secret_text' }),
  });
  const body = (await res.json().catch(() => null)) as
    | { success?: boolean; errors?: Array<{ message?: string }> }
    | null;
  if (!res.ok || !body?.success) {
    const detail = body?.errors?.map(e => e.message).filter(Boolean).join('; ') || `HTTP ${res.status}`;
    throw new Error(`CF Workers Secrets 寫入失敗：${detail}`);
  }
}

/**
 * 呼叫 CF Workers Scripts secrets 管理 API 刪除一個 per-script secret（T9 治理端點用）。
 * 404（本來就不存在）視為成功（冪等刪除，呼叫端可能已被清過）。
 *
 * `tokenOverride`：與 `putWorkerSecret` 同一組理由（見該函式註解）。auth store 分片重切時
 * 會刪多出來的舊分片，這條路徑也要能吃到安裝精靈臨時遞來的 token。
 */
export async function deleteWorkerSecret(env: Bindings, secretRef: string, tokenOverride?: string): Promise<void> {
  const token = tokenOverride || env.CF_SECRETS_API_TOKEN;
  if (!token || !env.CF_ACCOUNT_ID) {
    throw new Error('此 worker 缺 CF_SECRETS_API_TOKEN / CF_ACCOUNT_ID 設定，刪除路徑未就緒');
  }
  const url = `https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/workers/scripts/${CYPHER_SCRIPT_NAME}/secrets/${secretRef}`;
  const res = await fetch(url, {
    method: 'DELETE',
    headers: { Authorization: `Bearer ${token}` },
  });
  if (res.status === 404) return;
  const body = (await res.json().catch(() => null)) as
    | { success?: boolean; errors?: Array<{ message?: string }> }
    | null;
  if (!res.ok || !body?.success) {
    const detail = body?.errors?.map(e => e.message).filter(Boolean).join('; ') || `HTTP ${res.status}`;
    throw new Error(`CF Workers Secrets 刪除失敗：${detail}`);
  }
}

// ── KBDB 目錄存取（D38：零 SQL，一律走 entries HTTP API）──────────────────────────

const CREDENTIAL_ENTRY_TYPE = 'credential';

/** entries 表回來的一列（本檔只取用得到的欄位，避免耦合 KBDB 內部型別）。 */
interface KbdbEntryRow {
  id: string;
  page_name: string | null;
  owner_id: string | null;
  metadata_json: string | null;
  created_at: number;
}

interface CredentialMeta {
  service: string | null;
  sensitivity: 'standard' | 'high';
  secret_ref: string;
  last_used_at: number | null;
}

/** name → secret_ref 對照（給熱路徑用；獨立型別別名，避免函式簽章直接內嵌逗號分隔泛型）。 */
type CredentialRefMap = Record<string, string>;

function parseMeta(row: KbdbEntryRow): CredentialMeta {
  try {
    const m = row.metadata_json ? (JSON.parse(row.metadata_json) as Record<string, unknown>) : {};
    return {
      service: typeof m.service === 'string' ? m.service : null,
      sensitivity: m.sensitivity === 'high' ? 'high' : 'standard',
      secret_ref: typeof m.secret_ref === 'string' ? m.secret_ref : '',
      last_used_at: typeof m.last_used_at === 'number' ? m.last_used_at : null,
    };
  } catch {
    // 壞資料誠實視為空目錄列，不讓損毀的 metadata_json 炸整條路徑
    return { service: null, sensitivity: 'standard', secret_ref: '', last_used_at: null };
  }
}

/** 對 KBDB base 發 request（server 端直連，不經 /kbdb/* proxy——那支是給 CLI 用的）。 */
async function kbdbCredFetch(env: Bindings, path: string, init?: RequestInit): Promise<Response> {
  const { base, headers } = kbdbBase(env);
  return fetch(`${base}${path}`, {
    ...init,
    headers: { ...headers, ...(init?.headers as Record<string, string> | undefined) },
  });
}

// ── 熱路徑快取（D38 效能要求：這份映射幾乎不變，帶快取才不會比舊版 D1 直查慢）─────────
//
// per-isolate 記憶體快取，key=apiKey，TTL 60 秒。auth-dispatcher.ts 的
// resolveSecretsFromNewHome() 每次 workflow 執行都會呼叫，命中快取＝零網路呼叫；
// 未命中才打一次 KBDB（一次列出該租戶全部 credential，通常個位數到十位數筆，遠比逐名查便宜）。
// 寫入路徑（upsert/delete）主動 invalidate，保證「剛存的 credential 立刻查得到」不受 TTL 拖延。
// 快取容器用 plain object——apiKey 皆為服務端衍生字串，非使用者可控鍵名。
interface CachedDirRow {
  id: string;
  name: string;
  secret_ref: string;
  service: string | null;
  sensitivity: 'standard' | 'high';
  last_used_at: number | null;
}
interface CachedDir {
  rows: CachedDirRow[];
  fetchedAt: number;
}
const DIR_CACHE_TTL_MS = 60_000;
const dirCache: Record<string, CachedDir> = {};

/** 寫入（建立/覆寫/刪除）後呼叫，讓下次熱路徑查詢重新打一次 KBDB（不吃到過期快取）。 */
export function invalidateCredentialCache(apiKey: string): void {
  delete dirCache[apiKey];
}

/** 拉某租戶全部 credential 目錄列（快取層，60 秒 TTL）。給熱路徑（auth-dispatcher）與治理端點共用。 */
async function getCredentialDirectory(env: Bindings, apiKey: string): Promise<CachedDirRow[]> {
  const now = Date.now();
  const cached = dirCache[apiKey];
  if (cached && now - cached.fetchedAt < DIR_CACHE_TTL_MS) return cached.rows;

  const qs = new URLSearchParams({ owner_id: apiKey, entry_type: CREDENTIAL_ENTRY_TYPE, limit: '200' });
  const res = await kbdbCredFetch(env, `/entries?${qs.toString()}`);
  if (!res.ok) {
    // KBDB 不可達 / 回錯：誠實回空（呼叫端各自決定 fallback，不快取失敗結果避免卡住恢復）
    return [];
  }
  const body = (await res.json().catch(() => null)) as { entries?: KbdbEntryRow[] } | null;
  const rows: CachedDirRow[] = (body?.entries ?? [])
    .filter((e): e is KbdbEntryRow & { page_name: string } => !!e.page_name)
    .map((e) => {
      const meta = parseMeta(e);
      return {
        id: e.id,
        name: e.page_name,
        secret_ref: meta.secret_ref,
        service: meta.service,
        sensitivity: meta.sensitivity,
        last_used_at: meta.last_used_at,
      };
    });
  dirCache[apiKey] = { rows, fetchedAt: now };
  return rows;
}

/**
 * 給熱路徑（auth-dispatcher.ts）用：回這個租戶所有 credential 的 name→secret_ref 對照。
 * 快取命中＝零網路呼叫；未命中打一次 KBDB list（見 getCredentialDirectory）。
 */
export async function getCredentialSecretRefs(env: Bindings, apiKey: string): Promise<CredentialRefMap> {
  const rows = await getCredentialDirectory(env, apiKey);
  const out: CredentialRefMap = {};
  for (const r of rows) {
    if (r.secret_ref) out[r.name] = r.secret_ref;
  }
  return out;
}

/**
 * 治理面 last_used_at 更新（非關鍵路徑，best-effort，不阻塞呼叫端）。
 * 直接用快取裡已知的 id/其餘欄位組 PATCH，不額外多打一次查詢。找不到快取（代表這個租戶
 * 本次請求根本沒查到目錄，不太可能發生——resolveSecretsFromNewHome 只在有 secret_ref 命中時
 * 才會呼叫本函式）就跳過，不為了治理欄位額外多打一輪 KBDB。
 * 呼叫端刻意不 await 本函式的內部 fetch（fire-and-forget，見 auth-dispatcher.ts），失敗吞掉。
 */
export function touchLastUsed(env: Bindings, apiKey: string, names: string[]): void {
  const cached = dirCache[apiKey];
  if (!cached || names.length === 0) return;
  const now = Math.floor(Date.now() / 1000);
  for (const r of cached.rows) {
    if (!names.includes(r.name)) continue;
    const meta: CredentialMeta = {
      service: r.service, sensitivity: r.sensitivity, secret_ref: r.secret_ref, last_used_at: now,
    };
    kbdbCredFetch(env, `/entries/${encodeURIComponent(r.id)}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ metadata_json: JSON.stringify(meta) }),
    }).catch(() => { /* 治理面欄位，非關鍵路徑，失敗不影響任何主流程 */ });
    r.last_used_at = now; // 快取內同步更新，避免同一 TTL 視窗內下一次讀到舊值
  }
}

/** 找某租戶某 credential 的 entry（page_name=name 精確比對，entry_type=credential 隔離）。 */
async function findCredentialEntry(env: Bindings, apiKey: string, name: string): Promise<KbdbEntryRow | null> {
  const qs = new URLSearchParams({
    owner_id: apiKey, entry_type: CREDENTIAL_ENTRY_TYPE, page_name: name, limit: '1',
  });
  const res = await kbdbCredFetch(env, `/entries?${qs.toString()}`);
  if (!res.ok) throw new Error(`KBDB /entries 查詢失敗：HTTP ${res.status}`);
  const body = (await res.json().catch(() => null)) as { entries?: KbdbEntryRow[] } | null;
  return body?.entries?.[0] ?? null;
}

/**
 * upsert credential 目錄列（不含密文）。
 * created_at 只在首次建立時寫入（entries 表自帶 created_at，PATCH 不會動它）；
 * last_used_at 覆寫時保留原值——secret_ref 是純函式衍生自 api_key+name，理論上覆寫時值不會
 * 變，這裡仍走同一條寫入路徑以求同時支援「首次建立」與「覆寫」兩種呼叫路徑（比照舊 D1 版本）。
 */
async function upsertCredentialEntry(
  env: Bindings,
  apiKey: string,
  name: string,
  service: string | null,
  sensitivity: 'standard' | 'high',
  secretRef: string,
): Promise<void> {
  const existing = await findCredentialEntry(env, apiKey, name);
  const meta: CredentialMeta = {
    service, sensitivity, secret_ref: secretRef,
    last_used_at: existing ? parseMeta(existing).last_used_at : null,
  };
  if (existing) {
    const res = await kbdbCredFetch(env, `/entries/${encodeURIComponent(existing.id)}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ metadata_json: JSON.stringify(meta) }),
    });
    if (!res.ok) throw new Error(`credential 目錄更新失敗：HTTP ${res.status}`);
  } else {
    const res = await kbdbCredFetch(env, `/entries`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        entry_type: CREDENTIAL_ENTRY_TYPE, owner_id: apiKey, page_name: name,
        metadata_json: JSON.stringify(meta),
      }),
    });
    if (!res.ok) throw new Error(`credential 目錄建立失敗：HTTP ${res.status}`);
  }
  invalidateCredentialCache(apiKey);
}

interface CredentialRow {
  name: string;
  service: string | null;
  sensitivity: string;
  created_at: number;
  last_used_at: number | null;
}

/** KBDB 目錄 list（不含 secret_ref、不含值）——`GET /credentials` 與 `/credentials/catalog` 共用。 */
async function listCredentialRows(env: Bindings, apiKey: string): Promise<CredentialRow[]> {
  const qs = new URLSearchParams({ owner_id: apiKey, entry_type: CREDENTIAL_ENTRY_TYPE, limit: '200' });
  const res = await kbdbCredFetch(env, `/entries?${qs.toString()}`);
  if (!res.ok) throw new Error(`credential 目錄查詢失敗：HTTP ${res.status}`);
  const body = (await res.json().catch(() => null)) as { entries?: KbdbEntryRow[] } | null;
  const rows = (body?.entries ?? [])
    .filter((e): e is KbdbEntryRow & { page_name: string } => !!e.page_name)
    .map((e) => {
      const meta = parseMeta(e);
      return { name: e.page_name, service: meta.service, sensitivity: meta.sensitivity, created_at: e.created_at, last_used_at: meta.last_used_at };
    });
  // entries API 已用 created_at DESC 排序，這裡不重排（保持與舊版 D1 query 相同排序語意）
  return rows;
}

/** 給 `GET /portal/admin/ai` 之類「只要知道有沒有存過、不要值」的呼叫端用。 */
export async function hasCredential(env: Bindings, apiKey: string, name: string): Promise<boolean> {
  const entry = await findCredentialEntry(env, apiKey, name);
  return entry !== null;
}

interface CredentialWriteBody {
  name?: string;
  value?: string;
  service?: string;
  sensitivity?: string;
}

/** POST 建立 / PUT 覆寫共用的寫入邏輯。回傳 { secretRef } 供 route handler 組回應。 */
async function writeCredential(
  env: Bindings,
  apiKey: string,
  name: string,
  value: string,
  service: string | undefined,
  sensitivityRaw: string | undefined,
): Promise<{ secretRef: string; sensitivity: 'standard' | 'high' }> {
  const sensitivity = validSensitivity(sensitivityRaw) ? sensitivityRaw : 'standard';
  const secretRef = await deriveSecretRef(apiKey, name);

  // 1. 密文值進 Workers Secrets（唯寫，arcrun 自己也讀不回）
  await putWorkerSecret(env, secretRef, value);

  // 2. KBDB 目錄（不含密文）
  await upsertCredentialEntry(env, apiKey, name, service ?? null, sensitivity, secretRef);

  return { secretRef, sensitivity };
}

// POST /credentials — 建立/覆寫 credential（新家：Workers Secrets + KBDB entries 目錄）
credentialsRouter.post('/credentials', async (c) => {
  const apiKey = c.req.header('X-Arcrun-API-Key');
  if (!apiKey) {
    return c.json({ error: '缺少 X-Arcrun-API-Key header' }, 401);
  }

  const body = (await c.req.json().catch(() => null)) as CredentialWriteBody | null;
  if (!validateName(body?.name)) {
    return c.json({ error: 'name 必填，只能包含英文字母、數字和底線' }, 400);
  }
  if (!body?.value || typeof body.value !== 'string') {
    return c.json({ error: 'value 必填（credential 明文值，經 TLS 傳輸）' }, 400);
  }

  try {
    const { secretRef, sensitivity } = await writeCredential(
      c.env, apiKey, body.name, body.value, body.service, body.sensitivity,
    );
    return c.json({ success: true, name: body.name, service: body.service ?? null, sensitivity, secret_ref: secretRef });
  } catch (e) {
    // 誠實回報：寫入失敗（缺 token 設定 / CF API 錯誤）不假綠（mindset §7）
    return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 502);
  }
});

// PUT /credentials/:name — 整筆覆寫（credential-store-migration §3：只能 replace，不能 edit 局部）
credentialsRouter.put('/credentials/:name', async (c) => {
  const apiKey = c.req.header('X-Arcrun-API-Key');
  if (!apiKey) {
    return c.json({ error: '缺少 X-Arcrun-API-Key header' }, 401);
  }

  const name = c.req.param('name');
  if (!validateName(name)) {
    return c.json({ error: 'name 只能包含英文字母、數字和底線' }, 400);
  }

  const body = (await c.req.json().catch(() => null)) as CredentialWriteBody | null;
  if (!body?.value || typeof body.value !== 'string') {
    return c.json({ error: 'value 必填（credential 明文值，經 TLS 傳輸）' }, 400);
  }

  try {
    const { secretRef, sensitivity } = await writeCredential(
      c.env, apiKey, name, body.value, body.service, body.sensitivity,
    );
    return c.json({ success: true, name, service: body.service ?? null, sensitivity, secret_ref: secretRef });
  } catch (e) {
    return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 502);
  }
});

// DELETE /credentials/:name — 刪除 credential（T9：新家優先，舊 KV 為回退）
// D19 對齊：能刪的只有「目錄 row + Workers Secret 這個密文本體」，本端點從頭到尾不讀值。
credentialsRouter.delete('/credentials/:name', async (c) => {
  const apiKey = c.req.header('X-Arcrun-API-Key');
  if (!apiKey) {
    return c.json({ error: '缺少 X-Arcrun-API-Key header' }, 401);
  }

  const name = c.req.param('name');
  try {
    const entry = await findCredentialEntry(c.env, apiKey, name);
    if (entry) {
      const meta = parseMeta(entry);
      if (meta.secret_ref) await deleteWorkerSecret(c.env, meta.secret_ref);
      const res = await kbdbCredFetch(c.env, `/entries/${encodeURIComponent(entry.id)}`, { method: 'DELETE' });
      if (!res.ok) throw new Error(`credential 目錄刪除失敗：HTTP ${res.status}`);
      invalidateCredentialCache(apiKey);
      return c.json({ success: true, name, source: 'workers-secrets' });
    }
    // KBDB 沒有這筆 entry：這個 credential 可能從未回填過（只存在舊 KV），fallback 刪舊路徑，
    // 避免「GET 改讀新家看不到、DELETE 卻刪不掉」的孤兒資料。
    await c.env.CREDENTIALS_KV.delete(`${apiKey}:cred:${name}`);
    return c.json({ success: true, name, source: 'legacy-kv' });
  } catch (e) {
    return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 502);
  }
});

// GET /credentials/catalog — 目錄唯讀 list（Mira Console 完整版，Arcrun#3 console 系）。
// 與 GET /credentials（下方，改讀同一份 KBDB 查詢）是同一份資料的兩個路徑；
// /catalog 保留給既有 Console 呼叫，避免破壞既有前端整合。
credentialsRouter.get('/credentials/catalog', async (c) => {
  const apiKey = c.req.header('X-Arcrun-API-Key');
  if (!apiKey) {
    return c.json({ error: '缺少 X-Arcrun-API-Key header' }, 401);
  }
  try {
    const rows = await listCredentialRows(c.env, apiKey);
    return c.json({ success: true, credentials: rows, total: rows.length });
  } catch (e) {
    // 誠實回報：KBDB 不可達 / 回錯（不假綠回空陣列裝沒事）
    return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 502);
  }
});

// GET /credentials — 列出 credential 目錄（改讀 KBDB，只回 metadata，絕不含值/secret_ref）
credentialsRouter.get('/credentials', async (c) => {
  const apiKey = c.req.header('X-Arcrun-API-Key');
  if (!apiKey) {
    return c.json({ error: '缺少 X-Arcrun-API-Key header' }, 401);
  }
  try {
    const rows = await listCredentialRows(c.env, apiKey);
    return c.json({ success: true, credentials: rows, total: rows.length });
  } catch (e) {
    return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 502);
  }
});
