// Template + Record CRUD. A "record" = multiple entries composed via a template's slots.
// Base, D1 only. (Ported clean from KBDB; no vectorize/triplet imports.)
import type { Template } from '../types';
import { createEntry } from './entry-crud';

function uid(prefix: string): string {
  return `${prefix}_${crypto.randomUUID()}`;
}

// ---- Templates ----

export interface CreateTemplateInput {
  name: string;
  description?: string | null;
  slots: string[];
  created_by?: string | null;
  id?: string;
}

export async function createTemplate(db: D1Database, input: CreateTemplateInput): Promise<Template> {
  const id = input.id ?? uid('tpl');
  await db
    .prepare(`INSERT INTO templates (id, name, description, slots_json, created_by) VALUES (?, ?, ?, ?, ?)`)
    .bind(id, input.name, input.description ?? null, JSON.stringify(input.slots), input.created_by ?? null)
    .run();
  const row = await getTemplate(db, id);
  if (!row) throw new Error('createTemplate: row not found after insert');
  return row;
}

export async function getTemplate(db: D1Database, idOrName: string): Promise<Template | null> {
  const row = await db
    .prepare('SELECT * FROM templates WHERE id = ? OR name = ? LIMIT 1')
    .bind(idOrName, idOrName)
    .first<Template>();
  return row ?? null;
}

export async function listTemplates(db: D1Database): Promise<Template[]> {
  const res = await db.prepare('SELECT * FROM templates ORDER BY created_at DESC').all<Template>();
  return res.results ?? [];
}

export async function updateTemplate(db: D1Database, id: string, patch: { description?: string | null; slots?: string[] }): Promise<Template | null> {
  const cols: string[] = [];
  const params: unknown[] = [];
  if (patch.description !== undefined) { cols.push('description = ?'); params.push(patch.description); }
  if (patch.slots !== undefined) { cols.push('slots_json = ?'); params.push(JSON.stringify(patch.slots)); }
  if (cols.length === 0) return getTemplate(db, id);
  cols.push('updated_at = unixepoch()');
  await db.prepare(`UPDATE templates SET ${cols.join(', ')} WHERE id = ?`).bind(...params, id).run();
  return getTemplate(db, id);
}

// ---- Records (entry_values composed by template) ----

export interface CreateRecordInput {
  template: string;          // template id or name
  values?: Record<string, string>; // slot_name -> content（**新建**一筆 entry 當這個 slot 的值）
  /**
   * slot_name -> **既有** entry 的 id：把水池（entries）裡那條既有 entry 直接掛到這個 slot 上，
   * 不新建、不複製（Arcrun#128）。
   *
   * 🔴 為什麼要有這條路（不是優化，是「外鍵」本來就該有的樣子）：
   *   leo 2026-08-15 的心智模型——「blocks 是一個大水池，template/slots 組成虛擬表和 fields，
   *   最後都指向水池的一條 entry⋯⋯連到三元組就是外鍵」。而 `entry_values` 的約束
   *   （`migrations/0001_base.sql`）只有 `UNIQUE(record_id, slot_name)`，
   *   **`entry_id` 上沒有任何 unique** ⇒ 一條 entry 本來就能被無限多筆 record 的無限多個
   *   slot 參照，**儲存層早就是外鍵語意**。壞的只有寫入路徑：本函式舊版對每個 slot 值
   *   **無條件 createEntry** ⇒ 每設一次外鍵就把被參照的資料複製一份。
   *   那不是外鍵，那是複製。
   *
   *   後果不只是多佔列數：兩份從此**各自漂移**（改一邊，另一邊還是舊的），
   *   且資料量隨「有幾個 App 參照它」線性膨脹 ⇒ 遲早要有人來清、去重、修對不上的兩份
   *   ⇒ 直接違背這個模型的產品承諾「加一個 App 只要建一份 template，不必遷移、不需要工程師」
   *   （`InkStoneCo/system-dev/docs/4-guides/llm-wiki-schema.md`）。
   *
   * **與 values 並存**：給字串 → 照舊新建（舊呼叫端一個字都不用改）；給 id → 參照既有。
   */
  entry_ids?: Record<string, string>;
  owner_id?: string | null;
  record_id?: string;
}

/** 被參照 entry 的 id -> 它現在的 content（回傳值要帶真內容，不是空殼）。 */
type ReferencedContent = Map<string, string | null>;

/**
 * 讀出被參照的既有 entry，並在**寫入任何一列之前**把該擋的擋掉。
 *
 * 為什麼要先讀（不是多此一舉，靠 FK 報錯不夠）：
 *  1. **id 不存在**要給看得懂的錯（FK 違反在 D1 只回一句 SQLITE_CONSTRAINT，
 *     呼叫端不知道是哪個 slot、哪個 id）；
 *  2. **跨租戶必須擋**——這條新路徑讓呼叫端可以自己指定 entry_id，若不檢查歸屬，
 *     任何人都能把別人的 entry 掛進自己的 record，再從 `GET /records/:id` 讀回它的內容
 *     ⇒ 等於開一扇繞過租戶邊界的門（同 `.claude/rules/02-forbidden.md` §6.1 的精神：
 *     資料面的歸屬要與寫入端同源）；
 *  3. 回傳值要帶被參照 entry 的**現有內容**（呼叫端拿到的 values 才是那條真的 entry）。
 *
 * 所有檢查都在第一筆 INSERT 之前跑完 ⇒ 失敗就是「一列都沒寫」，不留半筆殘骸
 * （base 沒有交易可用，這是這裡唯一保證得了的原子性形式）。
 *
 * 批次以 90 個 id 一組：D1 綁定參數上限 100，沿用 searchByTemplate 既有慣例。
 */
async function loadReferencedEntries(
  db: D1Database,
  entryIds: Record<string, string>,
  recordOwnerId: string | null,
): Promise<ReferencedContent> {
  const ids = [...new Set(Object.values(entryIds))];
  if (ids.length === 0) return new Map();

  const rows: { id: string; content: string | null; owner_id: string | null }[] = [];
  for (let i = 0; i < ids.length; i += 90) {
    const chunk = ids.slice(i, i + 90);
    const res = await db
      .prepare(`SELECT id, content, owner_id FROM entries WHERE id IN (${chunk.map(() => '?').join(',')})`)
      .bind(...chunk)
      .all<{ id: string; content: string | null; owner_id: string | null }>();
    rows.push(...(res.results ?? []));
  }

  const found = new Map(rows.map((r) => [r.id, r]));
  const missing = ids.filter((id) => !found.has(id));
  if (missing.length > 0) throw new Error(`entry not found: ${missing.join(', ')}`);

  // 歸屬不同 → 擋。owner_id 為 null 的 entry 視為無主／共用（既有資料多半如此），放行。
  if (recordOwnerId != null) {
    const foreign = rows.filter((r) => r.owner_id != null && r.owner_id !== recordOwnerId);
    if (foreign.length > 0) {
      throw new Error(
        `entry owner mismatch: ${foreign.map((r) => `${r.id}(${r.owner_id})`).join(', ')} != ${recordOwnerId}`,
      );
    }
  }

  return new Map(rows.map((r) => [r.id, r.content]));
}

export interface RecordResult {
  record_id: string;
  template_id: string;
  values: Record<string, string>;
  /**
   * record 的歸屬（＝其底層 slot entries 的 owner_id，createRecord 寫入時同一值）。
   * 2026-08-12 補：`GET /records/:id` 原本不回這欄，所以**呼叫端無從判斷這筆是不是自己的**
   * ——按 id 直讀等於沒有租戶邊界。要讓 cypher 的 portal 資料面（授權的人/AI 走的那條）
   * 能對單筆做「不是我的就回 404」，歸屬必須跟著資料一起回來。無歸屬的舊資料 → null。
   */
  owner_id: string | null;
}

export async function createRecord(db: D1Database, input: CreateRecordInput): Promise<RecordResult> {
  const tpl = await getTemplate(db, input.template);
  if (!tpl) throw new Error(`template not found: ${input.template}`);
  const slots: string[] = JSON.parse(tpl.slots_json);
  const recordId = input.record_id ?? uid('rec');
  const values = input.values ?? {};
  const entryIds = input.entry_ids ?? {};
  const refSlots = Object.keys(entryIds);

  // 同一個 slot 不准同時給字串又給 id：兩者的意思相反（複製一份 vs 指向既有），
  // 猜哪一個都可能默默寫錯一份資料 ⇒ 當場報錯，不猜。
  const both = refSlots.filter((s) => s in values);
  if (both.length > 0) throw new Error(`slot given both value and entry_id: ${both.join(', ')}`);

  // entry_ids 指到 template 沒有的 slot → 報錯（**不學 values 那條「靜默略過」**）。
  // 理由：外鍵設了卻無聲消失是最難查的失敗——呼叫端會以為關聯建好了，
  // 而 `template=` 查詢永遠撈不到它（查詢走 entry_values）。讓它當場講話。
  const unknown = refSlots.filter((s) => !slots.includes(s));
  if (unknown.length > 0) throw new Error(`slot not in template: ${unknown.join(', ')}`);

  // 全部檢查（存在／歸屬）先跑完再寫，失敗＝一列都沒寫。
  const referenced = await loadReferencedEntries(db, entryIds, input.owner_id ?? null);

  for (const slot of slots) {
    // 參照既有 entry：只插一列關聯，**不碰 entries**（這就是外鍵）。
    if (slot in entryIds) {
      await db
        .prepare(`INSERT INTO entry_values (id, record_id, template_id, slot_name, entry_id) VALUES (?, ?, ?, ?, ?)`)
        .bind(uid('ev'), recordId, tpl.id, slot, entryIds[slot])
        .run();
      continue;
    }
    if (!(slot in values)) continue;
    const entry = await createEntry(db, {
      content: values[slot],
      entry_type: 'value',
      owner_id: input.owner_id ?? null,
    });
    await db
      .prepare(`INSERT INTO entry_values (id, record_id, template_id, slot_name, entry_id) VALUES (?, ?, ?, ?, ?)`)
      .bind(uid('ev'), recordId, tpl.id, slot, entry.id)
      .run();
  }

  // 回傳值：舊路徑照舊原樣回 input.values（一字不變），參照來的 slot 補上那條既有 entry 的現有內容。
  const out: Record<string, string> = { ...values };
  for (const [slot, entryId] of Object.entries(entryIds)) out[slot] = referenced.get(entryId) ?? '';
  return { record_id: recordId, template_id: tpl.id, values: out, owner_id: input.owner_id ?? null };
}

// Update an existing record's slot values (mira-dissolve T2.1, issue #6).
// "Deprecate by flipping a slot value" — base append-only is NOT broken: we change the
// underlying entries.content of the slot's entry, we do not alter table structure / add columns / delete rows.
//   - slot already on the record → UPDATE the linked entries.content.
//   - slot valid for the record's template but not yet present → create entry + entry_value (idempotent grow).
//   - slot not in the template's slots_json → reject (records must stay template-shaped).
// Returns null if the record does not exist.
export async function updateRecord(
  db: D1Database,
  recordId: string,
  values: Record<string, string>,
): Promise<RecordResult | null> {
  // Existing slot → entry_id + template_id for this record.
  // JOIN entries 帶回 owner_id：grow 路徑建新 entry 時要沿用 record 既有 owner_id
  //（portal-auth design §2.2 附帶修復——原本漏帶 → 孤兒 entry（owner_id=NULL），
  //  owner-scoped 查詢（searchByTemplate / searchEntries）看不到該 slot 值）。
  const evRes = await db
    .prepare(
      `SELECT ev.slot_name AS slot_name, ev.entry_id AS entry_id, ev.template_id AS template_id, e.owner_id AS owner_id
       FROM entry_values ev JOIN entries e ON ev.entry_id = e.id
       WHERE ev.record_id = ?`,
    )
    .bind(recordId)
    .all<{ slot_name: string; entry_id: string; template_id: string; owner_id: string | null }>();
  const evRows = evRes.results ?? [];
  if (evRows.length === 0) return null; // record does not exist

  const templateId = evRows[0].template_id;
  // record 的歸屬＝其既有 slot entries 的 owner_id（createRecord 寫入時同一值）。
  const recordOwnerId = evRows.find((r) => r.owner_id != null)?.owner_id ?? null;
  const slotToEntry = new Map(evRows.map((r) => [r.slot_name, r.entry_id]));

  const tpl = await getTemplate(db, templateId);
  const allowed: string[] = tpl ? JSON.parse(tpl.slots_json) : [...slotToEntry.keys()];

  for (const [slot, content] of Object.entries(values)) {
    if (!allowed.includes(slot)) {
      throw new Error(`slot not in template: ${slot}`);
    }
    const entryId = slotToEntry.get(slot);
    if (entryId) {
      // flip the slot value: update the linked entry's content (table structure untouched)
      await db.prepare(`UPDATE entries SET content = ?, updated_at = unixepoch() WHERE id = ?`).bind(content, entryId).run();
    } else {
      // valid template slot not yet on this record → grow it (create entry + link)
      // owner_id 帶 record 既有歸屬（design §2.2 附帶修復，防孤兒 entry）
      const entry = await createEntry(db, { content, entry_type: 'value', owner_id: recordOwnerId });
      await db
        .prepare(`INSERT INTO entry_values (id, record_id, template_id, slot_name, entry_id) VALUES (?, ?, ?, ?, ?)`)
        .bind(uid('ev'), recordId, templateId, slot, entry.id)
        .run();
    }
  }
  return getRecord(db, recordId);
}

export async function getRecord(db: D1Database, recordId: string): Promise<RecordResult | null> {
  const res = await db
    .prepare(
      `SELECT ev.slot_name as slot, e.content as content, ev.template_id as template_id, e.owner_id as owner_id
       FROM entry_values ev JOIN entries e ON ev.entry_id = e.id
       WHERE ev.record_id = ?`,
    )
    .bind(recordId)
    .all<{ slot: string; content: string; template_id: string; owner_id: string | null }>();
  const rows = res.results ?? [];
  if (rows.length === 0) return null;
  const values: Record<string, string> = {};
  for (const r of rows) values[r.slot] = r.content;
  // 歸屬取第一個非 null 的 slot entry owner（同一 record 的 slot entries 同歸屬）
  const owner_id = rows.find((r) => r.owner_id != null)?.owner_id ?? null;
  return { record_id: recordId, template_id: rows[0].template_id, values, owner_id };
}

export async function searchByTemplate(db: D1Database, template: string, owner_id?: string, limit = 100): Promise<RecordResult[]> {
  const tpl = await getTemplate(db, template);
  if (!tpl) return [];
  // owner_id 過濾在 SQL 做：record 的歸屬存在底層 entries.owner_id（createRecord 寫入時帶）。
  // 給了 owner_id → JOIN entries 限定該 owner（租戶隔離，cypher proxy 強制注入）；
  // 沒給 → 不限（內部/全域查詢）。先前 `|| true` 是 stub，會洩漏跨租戶資料（2026-06-14 修）。
  const cap = Math.min(limit, 500);
  const res = owner_id
    ? await db
        .prepare(
          `SELECT DISTINCT ev.record_id as record_id FROM entry_values ev
           JOIN entries e ON ev.entry_id = e.id
           WHERE ev.template_id = ? AND e.owner_id = ?
           ORDER BY ev.created_at DESC LIMIT ?`,
        )
        .bind(tpl.id, owner_id, cap)
        .all<{ record_id: string }>()
    : await db
        .prepare(`SELECT DISTINCT record_id FROM entry_values WHERE template_id = ? ORDER BY created_at DESC LIMIT ?`)
        .bind(tpl.id, cap)
        .all<{ record_id: string }>();
  // 批次撈齊所有 record 的 slot 值（2026-07-18 修 N+1：原本逐筆 getRecord＝每筆 1 次 D1
  // 往返，100 筆 triplet ≈ 19 秒——graph/總圖/rag_chat 的「查詢 20-30 秒」病根就是這裡）。
  // D1 綁定參數上限 100 → id 以 90 一組分批 IN 查詢；輸出保持原排序（created_at DESC）。
  const ids = (res.results ?? []).map((r) => r.record_id);
  if (ids.length === 0) return [];
  const byId = new Map<string, RecordResult>();
  for (let i = 0; i < ids.length; i += 90) {
    const chunk = ids.slice(i, i + 90);
    const placeholders = chunk.map(() => '?').join(',');
    const evRes = await db
      .prepare(
        `SELECT ev.record_id as record_id, ev.slot_name as slot, e.content as content, ev.template_id as template_id, e.owner_id as owner_id
         FROM entry_values ev JOIN entries e ON ev.entry_id = e.id
         WHERE ev.record_id IN (${placeholders})`,
      )
      .bind(...chunk)
      .all<{ record_id: string; slot: string; content: string; template_id: string; owner_id: string | null }>();
    for (const r of evRes.results ?? []) {
      let rec = byId.get(r.record_id);
      if (!rec) {
        rec = { record_id: r.record_id, template_id: r.template_id, values: {}, owner_id: null };
        byId.set(r.record_id, rec);
      }
      rec.values[r.slot] = r.content;
      if (rec.owner_id == null && r.owner_id != null) rec.owner_id = r.owner_id;
    }
  }
  return ids.map((id) => byId.get(id)).filter((r): r is RecordResult => !!r);
}

/**
 * 刪除一筆 record：先刪 entry_values（FK），再刪底層 entries。回 false 表示 record 不存在。
 *
 * 🔴 **還有別人指著的 entry 不刪**（Arcrun#128 的必然配套，不是順手加的）：
 *   slot 值可以指向既有 entry 之後，同一條 entry 會同時被別的 record 指著。
 *   舊寫法「這筆 record 的每個 entry_id 都刪掉」在那種情況下會：
 *     · 把**別人還在用**的那條水池資料一起刪掉（他的 slot 從此指向不存在的列），或
 *     · 撞上 `entry_values.entry_id REFERENCES entries(id)` 的 FK 而整個刪除失敗。
 *   ⇒ 條件改成「已經沒有任何 entry_values 指著它」才刪。
 *
 *   **沒有共用時行為與舊版完全相同**：這筆 record 的關聯列已先刪掉，若沒有別人指著，
 *   `NOT EXISTS` 恆為真 ⇒ 照樣刪。差別只出現在「真的被共用」的那條上。
 */
export async function deleteRecord(db: D1Database, recordId: string): Promise<boolean> {
  const evRes = await db
    .prepare('SELECT entry_id FROM entry_values WHERE record_id = ?')
    .bind(recordId)
    .all<{ entry_id: string }>();
  const rows = evRes.results ?? [];
  if (rows.length === 0) return false;
  await db.prepare('DELETE FROM entry_values WHERE record_id = ?').bind(recordId).run();
  for (const { entry_id } of rows) {
    await db
      .prepare('DELETE FROM entries WHERE id = ? AND NOT EXISTS (SELECT 1 FROM entry_values WHERE entry_id = ?)')
      .bind(entry_id, entry_id)
      .run();
  }
  return true;
}
