// credential-legacy-migration.ts — 「新讀取端上線、舊資料還沒搬完」的自癒補丁
// （D38 圍牆修復收尾，總管交辦，2026-08-08）。
//
// ── 為什麼這支檔案存在 ────────────────────────────────────────────────────
// 7ba7855（D38 圍牆修復）把 credential 目錄的讀寫端從舊表 `credentials`（0002，違規多開
// 的第四張表）改成走 entries 表（entry_type='credential'）。0006_drop_credentials_table.sql
// 寫了「把舊表資料搬進 entries 後讓舊表退場」的一次性 migration，但這支 migration **要有人
// 手動觸發部署才會跑**——2026-08-07 youlin 測試實例的事故就是「code 部署了、migration 沒
// 跑」造成 20/20 workflow 全部找不到 credential。
//
// leo 追加的硬要求（2026-08-08）：credential 資料住在**用戶自己的 Cloudflare 帳號**，
// 換讀取路徑＝每個既有實例的資料都要跟著搬，但**用戶不准做任何手動步驟**——不能要求他
// 跑指令、改設定、重裝。搬遷必須內建在「用戶本來就會走的路」裡（因此天然無感）。
//
// ── 解法：把「搬」變成「讀」的副作用，而不是獨立一步 ─────────────────────
// KBDB worker（本檔）是 D38 唯一允許碰 SQL 的地方（牆內）。這裡在**每次查詢某租戶的
// credential 目錄之前**，先確認舊表資料是否已經搬進 entries——沒有就搬（scoped 到這個
// owner_id，NOT EXISTS 防重複），有就是零成本的一次 sqlite_master 檢查。
//
// 呼叫時機只有一個：cypher-executor 的 credentials.ts 熱路徑（getCredentialDirectory /
// findCredentialEntry）本來就會在**每次 workflow 執行**打一次 GET /entries?entry_type=
// credential&owner_id=X（60 秒快取未命中時）。只要 KBDB worker 部署了本檔的邏輯，
// 下一次任何人跑 workflow，那個租戶的資料就自動搬好了——**不需要用戶多做任何事**，
// 也不需要「更新流程」額外呼叫一支新端點：更新 KBDB worker 本身就是唯一需要發生的事，
// 之後的搬遷由使用行為自然觸發。
//
// ── 三個安全性質（都經得起故意製造壞狀態來驗證，見 tests/credential-legacy-migration.test.ts）──
//   1. 冪等：NOT EXISTS 防止同一筆搬兩次；同一個 owner 呼叫 N 次只搬一次。
//   2. 對「已經搬過」與「還沒搬」的實例都正確：已搬過 → legacyTableExists 一旦舊表被真的
//      清空退場（未來清理步驟）就直接短路回 false，query 零成本；還沒搬 → 這次呼叫就地補齊。
//   3. 不砍表：本檔刻意不執行「讓舊表退場」那句 SQL——多個實例的搬遷時間點不同，
//      表還留著才能讓「還沒搬的」與「已經搬的」實例同時安全運作（leo 08-08：
//      「他們會同時存在一段時間」）。退場是之後所有租戶都確認搬完才做的獨立清理步驟。

/** 舊表是否還存在（sqlite_master 查詢，索引命中、幾乎零成本）。
 *  一旦舊表被清理步驟真的清空退場，這裡會回 false，後續呼叫直接短路，不再嘗試搬遷。 */
async function legacyCredentialsTableExists(db: D1Database): Promise<boolean> {
  const row = await db
    .prepare(`SELECT 1 AS x FROM sqlite_master WHERE type = 'table' AND name = 'credentials'`)
    .first<{ x: number }>();
  return row !== null;
}

/**
 * 把某個租戶（owner_id=api_key）在舊 `credentials` 表裡、entries 還沒有對應列的 row
 * 搬進 entries（entry_type='credential'）。scoped 到單一 owner，故查詢便宜，可安全地在
 * 熱路徑（每次 workflow 執行）前呼叫。
 *
 * 欄位對應與 0006_drop_credentials_table.sql 逐字一致（page_name=name 冪等鍵，
 * metadata_json 打包 service/sensitivity/secret_ref/last_used_at）。
 *
 * @returns 實際搬移的筆數（0 = 這個 owner 沒有待搬資料，含「舊表本來就不存在」與
 *          「已經搬過」兩種情況——呼叫端不需要分辨，行為一致）。
 */
export async function migrateLegacyCredentialsForOwner(db: D1Database, ownerId: string): Promise<number> {
  if (!ownerId) return 0; // 沒有 owner_id 的查詢（極少見）不觸發：搬遷是 per-tenant 動作，範圍不明確就不做
  if (!(await legacyCredentialsTableExists(db))) return 0; // 舊表不存在（從未有 / 已清理）→ 零成本短路

  const before = await db
    .prepare(`SELECT COUNT(*) AS n FROM entries WHERE entry_type = 'credential' AND owner_id = ?1`)
    .bind(ownerId)
    .first<{ n: number }>();

  await db
    .prepare(
      `INSERT INTO entries (id, entry_type, owner_id, page_name, metadata_json, created_at, updated_at)
       SELECT
         'e_cred_' || lower(hex(randomblob(8))),
         'credential',
         c.api_key,
         c.name,
         json_object('service', c.service, 'sensitivity', c.sensitivity, 'secret_ref', c.secret_ref, 'last_used_at', c.last_used_at),
         c.created_at,
         unixepoch()
       FROM credentials c
       WHERE c.api_key = ?1
         AND NOT EXISTS (
           SELECT 1 FROM entries e
           WHERE e.entry_type = 'credential' AND e.owner_id = c.api_key AND e.page_name = c.name
         )`,
    )
    .bind(ownerId)
    .run();

  const after = await db
    .prepare(`SELECT COUNT(*) AS n FROM entries WHERE entry_type = 'credential' AND owner_id = ?1`)
    .bind(ownerId)
    .first<{ n: number }>();

  return (after?.n ?? 0) - (before?.n ?? 0);
}
