/**
 * 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. D1 `credentials` 表只寫「目錄」（api_key/name/service/sensitivity/secret_ref/
 *        created_at），**不含密文**。
 *   不再寫 KV / 不再寫明文密文到 D1。
 *
 * 傳輸格式：client **不做** AES-GCM 加密，明文值經 TLS 送到 cypher，cypher 短暫在記憶體
 * 經手明文（不落地、不持久、不持金鑰）後直接 PUT 進 Workers Secrets。（此為 2026-07-03
 * 定案並已落地的做法，取代更早的 `{name, encrypted, iv}` 格式；rule 01 已同步。）
 *
 * 治理端點：
 *   - `GET /credentials`：改讀 D1（與 `/credentials/catalog` 共用同一份 query，同時保留
 *     `/catalog` 別名，Console 既有呼叫不受影響）。
 *   - `DELETE /credentials/:name`：先查 D1 拿 secret_ref → 有則刪 Workers Secret + D1 row；
 *     沒有（credential 從未回填過，只存在舊 KV）→ fallback 刪舊 KV key，避免刪不掉的孤兒資料。
 */

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

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 upsertCredentialRow(env.CREDENTIALS_DB, 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 對齊）。
 */
async function putWorkerSecret(env: Bindings, secretRef: string, value: string): Promise<void> {
  if (!env.CF_SECRETS_API_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 ${env.CF_SECRETS_API_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（本來就不存在）視為成功（冪等刪除，呼叫端可能已被清過）。
 */
async function deleteWorkerSecret(env: Bindings, secretRef: string): Promise<void> {
  if (!env.CF_SECRETS_API_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 ${env.CF_SECRETS_API_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}`);
  }
}

/**
 * D1 upsert credential 目錄 row（不含密文）。
 * created_at 只在首次建立時寫入；覆寫（PUT/重複 POST）保留原 created_at，只更新
 * service/sensitivity/secret_ref（secret_ref 是純函式衍生自 api_key+name，理論上覆寫時
 * 值不會變，這裡仍寫入以求同一份 SQL 同時支援「首次建立」與「覆寫」兩種呼叫路徑）。
 */
async function upsertCredentialRow(
  db: D1Database,
  apiKey: string,
  name: string,
  service: string | null,
  sensitivity: 'standard' | 'high',
  secretRef: string,
): Promise<void> {
  const now = Math.floor(Date.now() / 1000);
  await db
    .prepare(
      `INSERT INTO credentials (api_key, name, service, sensitivity, secret_ref, created_at, last_used_at)
       VALUES (?, ?, ?, ?, ?, ?, NULL)
       ON CONFLICT(api_key, name) DO UPDATE SET
         service = excluded.service,
         sensitivity = excluded.sensitivity,
         secret_ref = excluded.secret_ref`,
    )
    .bind(apiKey, name, service, sensitivity, secretRef, now)
    .run();
}

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

/** D1 目錄 list（不含 secret_ref、不含值）——`GET /credentials` 與 `/credentials/catalog` 共用。 */
async function listCredentialRows(db: D1Database, apiKey: string): Promise<CredentialRow[]> {
  const rows = await db
    .prepare(
      `SELECT name, service, sensitivity, created_at, last_used_at
       FROM credentials WHERE api_key = ? ORDER BY created_at DESC`,
    )
    .bind(apiKey)
    .all<CredentialRow>();
  return rows.results ?? [];
}

/** 查單一 credential 的 secret_ref（治理端點刪除用；不對外回傳 secret_ref 本身，只內部使用）。 */
async function findSecretRef(db: D1Database, apiKey: string, name: string): Promise<string | null> {
  const row = await db
    .prepare(`SELECT secret_ref FROM credentials WHERE api_key = ? AND name = ?`)
    .bind(apiKey, name)
    .first<{ secret_ref: string }>();
  return row?.secret_ref ?? 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. D1 目錄（不含密文）
  await upsertCredentialRow(env.CREDENTIALS_DB, apiKey, name, service ?? null, sensitivity, secretRef);

  return { secretRef, sensitivity };
}

// POST /credentials — 建立/覆寫 credential（新家：Workers Secrets + D1 目錄）
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 secretRef = await findSecretRef(c.env.CREDENTIALS_DB, apiKey, name);
    if (secretRef) {
      await deleteWorkerSecret(c.env, secretRef);
      await c.env.CREDENTIALS_DB
        .prepare(`DELETE FROM credentials WHERE api_key = ? AND name = ?`)
        .bind(apiKey, name)
        .run();
      return c.json({ success: true, name, source: 'workers-secrets' });
    }
    // D1 沒有 row：這個 credential 可能從未回填過（只存在舊 KV），fallback 刪舊路徑，
    // 避免「GET 改讀 D1 看不到、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 — D1 目錄唯讀 list（Mira Console 完整版，Arcrun#3 console 系）。
// 與 GET /credentials（下方，T9 起改讀同一份 D1 查詢）是同一份資料的兩個路徑；
// /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.CREDENTIALS_DB, apiKey);
    return c.json({ success: true, credentials: rows, total: rows.length });
  } catch (e) {
    // 誠實回報：D1 未建表 / migration 未跑（不假綠回空陣列裝沒事）
    return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 502);
  }
});

// GET /credentials — 列出 credential 目錄（T9：改讀 D1，只回 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.CREDENTIALS_DB, 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);
  }
});
