/**
 * KBDB 資料層 proxy（kbdb-base Phase 9.5，HANDOFF §2 + §3b 後續）
 *
 * 為什麼存在：CLI 是 client，只認證到 cypher-executor（X-Arcrun-API-Key），達不到獨立的
 * KBDB worker（MCP 走內部 service binding 可達，CLI 不行）。故在 cypher 開一條 proxy，
 * 讓 CLI 薄殼（acr kbdb *）透過「它本來就連的 cypher」打 KBDB 基本盤 API。
 *
 * 薄殼鐵律（rule 07）：本檔是 **proxy**，純轉發到 KBDB 基本盤 HTTP API，
 *   無業務邏輯、不寫 SQL、不建表、不直連 D1。能力真身在 KBDB 基本盤（kbdb/src/routes/*）。
 *
 * KBDB 鐵律（leo 2026-06-14）：只暴露 template/record/query/search，**不開建表/SQL**。
 *
 * 租戶隔離（leo 2026-06-14 拍板，選項①）：
 *   - X-Arcrun-API-Key（namespace/api_key）→ 自動當 owner_id 注入 records/entries 的寫入與查詢。
 *     不同 namespace 的資料互相看不到。與 cypher 其他端點同身份模型。
 *   - **templates 全域共享**（虛擬表定義是 schema 不是資料；類 Supabase 的表結構大家共用）→ 不注入 owner_id。
 *
 * cypher→KBDB 連法沿用既有慣例（webhook-handlers.ts / recipes.ts）：
 *   KBDB_BASE_URL HTTP fetch + 選用 KBDB_INTERNAL_TOKEN Bearer。**不新增 service binding**（rule 02 §3.1）。
 */
import { Hono } from 'hono';
import type { Bindings } from '../types';

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

/**
 * KBDB 基本盤 base URL + internal headers。
 * fallback 指**現役** arcrun-kbdb（workers.dev，無 auth、不需 token）——
 * 不沿用 webhook-handlers.ts 的舊 fallback kbdb.finally.click（inkstone 遺留、已死、要 token）。
 * KBDB_BASE_URL 可覆蓋（self-hosted fork 指自己的 KBDB）。
 */
export function kbdbBase(env: Bindings): { base: string; headers: Record<string, string> } {
  const base = (env.KBDB_BASE_URL ?? 'https://arcrun-kbdb.uncle6-me.workers.dev').replace(/\/$/, '');
  const headers: Record<string, string> = { 'Content-Type': 'application/json' };
  if (env.KBDB_INTERNAL_TOKEN) headers['Authorization'] = `Bearer ${env.KBDB_INTERNAL_TOKEN}`;
  return { base, headers };
}

/** 取租戶身份（owner_id）。缺 header → 401（與 cypher 其他資料端點一致）。 */
function tenant(c: { req: { header: (k: string) => string | undefined } }): string | null {
  return c.req.header('X-Arcrun-API-Key') ?? null;
}

const NEED_KEY = { error: '缺少 X-Arcrun-API-Key header' } as const;

// ── templates（全域共享，不注入 owner_id）──────────────────────────────────────

// POST /kbdb/templates — 建 template（name + slots）。鐵律：這是「虛擬表定義」非建真表。
kbdbProxyRouter.post('/kbdb/templates', async (c) => {
  const owner = tenant(c);
  if (!owner) return c.json(NEED_KEY, 401);
  const body = await c.req.json().catch(() => null);
  if (!body || !body.name || !Array.isArray(body.slots)) {
    return c.json({ error: 'name 與 slots[] 必填' }, 400);
  }
  const { base, headers } = kbdbBase(c.env);
  const res = await fetch(`${base}/templates`, {
    method: 'POST',
    headers,
    // created_by 帶上租戶當溯源，但 template 本身全域可見可用
    body: JSON.stringify({ name: body.name, slots: body.slots, description: body.description, created_by: owner }),
  });
  return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});

// GET /kbdb/templates — 列出所有 template（全域）。
kbdbProxyRouter.get('/kbdb/templates', async (c) => {
  if (!tenant(c)) return c.json(NEED_KEY, 401);
  const { base, headers } = kbdbBase(c.env);
  const res = await fetch(`${base}/templates`, { headers });
  return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});

// GET /kbdb/templates/:idOrName — 取單一 template。
kbdbProxyRouter.get('/kbdb/templates/:idOrName', async (c) => {
  if (!tenant(c)) return c.json(NEED_KEY, 401);
  const { base, headers } = kbdbBase(c.env);
  const res = await fetch(`${base}/templates/${encodeURIComponent(c.req.param('idOrName'))}`, { headers });
  return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});

// ── records（以租戶 namespace 為 owner_id 隔離）────────────────────────────────

// POST /kbdb/records — 填一筆 record（template + values）。owner_id 自動注入。
kbdbProxyRouter.post('/kbdb/records', async (c) => {
  const owner = tenant(c);
  if (!owner) return c.json(NEED_KEY, 401);
  const body = await c.req.json().catch(() => null);
  if (!body || !body.template || !body.values) {
    return c.json({ error: 'template 與 values 必填' }, 400);
  }
  const { base, headers } = kbdbBase(c.env);
  const res = await fetch(`${base}/records`, {
    method: 'POST',
    headers,
    // 強制以租戶身份隔離：忽略 caller 自帶 owner_id，一律用 header 身份（防跨租戶寫入）
    body: JSON.stringify({ template: body.template, values: body.values, owner_id: owner }),
  });
  return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});

// GET /kbdb/records/by-template/:template — 列某 template 下「本租戶」的 records。
kbdbProxyRouter.get('/kbdb/records/by-template/:template', async (c) => {
  const owner = tenant(c);
  if (!owner) return c.json(NEED_KEY, 401);
  const { base, headers } = kbdbBase(c.env);
  const res = await fetch(
    `${base}/records/by-template/${encodeURIComponent(c.req.param('template'))}?owner_id=${encodeURIComponent(owner)}`,
    { headers },
  );
  return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});

// GET /kbdb/records/:recordId — 取單筆 record。
kbdbProxyRouter.get('/kbdb/records/:recordId', async (c) => {
  if (!tenant(c)) return c.json(NEED_KEY, 401);
  const { base, headers } = kbdbBase(c.env);
  const res = await fetch(`${base}/records/${encodeURIComponent(c.req.param('recordId'))}`, { headers });
  return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});

// PATCH /kbdb/records/:recordId — 翻某筆 record 的 slot 值（{ values:{slot:content} }）。
// 補上基本盤既有能力（kbdb/src/routes/records.ts 的 PATCH /records/:recordId，mira-dissolve T2.1）
// 缺的對外通道——2026-08-11 leo 三元組 library 補標核實：base 早有這個端點，但這條 proxy
// 之前只轉發 GET/POST，插件/工作流打不到，補標三元組只能繞去改表（違 D38）。單純轉發，無業務邏輯。
// by-id 沿用既有慣例（require-key，不額外做 owner 比對——與本檔 GET .../:recordId、
// PATCH /kbdb/entries/:id 同款）。
kbdbProxyRouter.patch('/kbdb/records/:recordId', async (c) => {
  if (!tenant(c)) return c.json(NEED_KEY, 401);
  const body = await c.req.json().catch(() => null);
  if (!body || typeof body.values !== 'object' || body.values === null) {
    return c.json({ error: 'values 必填（{slot名: 內容}）' }, 400);
  }
  const { base, headers } = kbdbBase(c.env);
  const res = await fetch(`${base}/records/${encodeURIComponent(c.req.param('recordId'))}`, {
    method: 'PATCH',
    headers,
    body: JSON.stringify({ values: body.values }),
  });
  return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});

// ── search（限本租戶範圍內）────────────────────────────────────────────────────

// GET /kbdb/search?q=&entry_type=&source=&library=&mode= — entries 搜尋，限本租戶 owner_id。
// 透傳 entry_type（base 通用 filter，workflow-discovery Q4）/ source / library（多值逗號分隔，
// portal-auth P1 順延項——owner/admin 面自選庫過濾；portal 一般用戶不經這，走 /portal/data/*
// 的 server 注入）/ mode 給 KBDB /entries/search。
kbdbProxyRouter.get('/kbdb/search', async (c) => {
  const owner = tenant(c);
  if (!owner) return c.json(NEED_KEY, 401);
  const q = c.req.query('q');
  if (!q) return c.json({ error: 'q 必填' }, 400);
  const { base, headers } = kbdbBase(c.env);
  const params = new URLSearchParams({ q, owner_id: owner });
  for (const k of ['entry_type', 'source', 'library', 'mode']) {
    const v = c.req.query(k);
    if (v) params.set(k, v);
  }
  const res = await fetch(`${base}/entries/search?${params.toString()}`, { headers });
  return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});

// ── entries（原子資料 / 樹節點，以租戶 namespace 為 owner_id 隔離）─────────────────
//
// kbdb-base 9.6：基本盤 /entries CRUD 的 proxy（HANDOFF §2 缺口①，mira _kbdb_client.py 遷移目標）。
// 租戶隔離同 records（選項①）：寫入強制注入 owner_id、list 強制以本租戶 owner_id 過濾；
// by-id 沿用既有 records by-id 慣例（require-key，不額外做 owner 比對——與本檔其他 by-id 端點一致）。

// POST /kbdb/entries — 建一個 entry（entry_type 必填，如 block/value/project/workflow）。owner_id 自動注入。
kbdbProxyRouter.post('/kbdb/entries', async (c) => {
  const owner = tenant(c);
  if (!owner) return c.json(NEED_KEY, 401);
  const body = await c.req.json().catch(() => null);
  if (!body || !body.entry_type) return c.json({ error: 'entry_type 必填' }, 400);
  const { base, headers } = kbdbBase(c.env);
  const res = await fetch(`${base}/entries`, {
    method: 'POST',
    headers,
    // 強制以租戶身份隔離：忽略 caller 自帶 owner_id，一律用 header 身份（防跨租戶寫入）
    body: JSON.stringify({ ...body, owner_id: owner }),
  });
  return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});

// GET /kbdb/entries — list（filters: entry_type / parent_id / page_name / source / library / q(search) / limit / offset）。
// owner_id 強制覆寫成本租戶（防跨租戶讀；caller 不能查別人的 owner_id）。
// Arcrun#3 發現①根因：本白名單原本沒有 q/search，caller 帶 search= 會被這裡靜默丟棄，
// 打到 base 永遠是「無過濾 list」——不是 458K 筆搜不到，是這個 filter 從沒被轉發過。
// 修法：q 與 search 都收，統一轉發成 base 認得的 q（base 端見 entries.ts 同步修）。
kbdbProxyRouter.get('/kbdb/entries', async (c) => {
  const owner = tenant(c);
  if (!owner) return c.json(NEED_KEY, 401);
  const { base, headers } = kbdbBase(c.env);
  const params = new URLSearchParams();
  params.set('owner_id', owner); // 強制本租戶，不接受 caller 覆寫
  for (const k of ['entry_type', 'parent_id', 'page_name', 'source', 'library', 'limit', 'offset']) {
    const v = c.req.query(k);
    if (v) params.set(k, v);
  }
  const q = c.req.query('q') || c.req.query('search');
  if (q) params.set('q', q);
  const res = await fetch(`${base}/entries?${params.toString()}`, { headers });
  return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});

// GET /kbdb/entries/:id — 取單筆 entry。
kbdbProxyRouter.get('/kbdb/entries/:id', async (c) => {
  if (!tenant(c)) return c.json(NEED_KEY, 401);
  const { base, headers } = kbdbBase(c.env);
  const res = await fetch(`${base}/entries/${encodeURIComponent(c.req.param('id'))}`, { headers });
  return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});

// ── graph（kbdb-graph-plugin proxy，Mira Console 卡片詳頁「關聯視圖」）───────────────
//
// 純轉發（rule 07）：圖能力真身在 kbdb-graph-plugin worker（GET /graph/neighbors/:name，
// 回 { node, edges[], neighbors[], edgeCount, neighborCount }）。瀏覽器不能持 KBDB_INTERNAL_TOKEN，
// 故經 cypher 代轉（token 只在 server 側）。plugin base 現算慣例同 registry/KBDB_BASE_URL。

export function graphBase(env: Bindings): string {
  if (env.KBDB_GRAPH_URL) return env.KBDB_GRAPH_URL.replace(/\/$/, '');
  return `https://kbdb-graph-plugin.${env.WORKER_SUBDOMAIN}.workers.dev`;
}

/**
 * kbdb-graph-plugin 的 internal headers。**打 plugin 一律用這支，不要各自手拼**（Arcrun#100）。
 *
 * plugin 端（kbdb-graph-plugin/src/index.ts）對 `/triplets` `/graph` `/search` `/entities`
 * 四個前綴掛了 Bearer 閘：設了 KBDB_INTERNAL_TOKEN 就必須帶，否則一律 401。
 * 原本三處手拼（本檔 neighbors、portal-data neighbors、console-dashboard 兩支 stats），
 * 前兩處帶了、後兩處漏了 → `/triplets/stats` 永遠 401 → 前端「三元組 0」。
 * 收斂成一支函式＝新的呼叫點不可能再漏（漂移的根，不是那兩行本身）。
 */
export function graphHeaders(env: Bindings): Record<string, string> {
  const headers: Record<string, string> = {};
  if (env.KBDB_INTERNAL_TOKEN) headers['Authorization'] = `Bearer ${env.KBDB_INTERNAL_TOKEN}`;
  return headers;
}

// GET /kbdb/graph/neighbors/:name — 查某節點（entity/卡片名）的鄰居 + 邊。
// 查無 triplet 資料時 plugin 回空陣列——前端據此顯示「尚無關聯資料」（誠實，不編造關聯）。
kbdbProxyRouter.get('/kbdb/graph/neighbors/:name', async (c) => {
  if (!tenant(c)) return c.json(NEED_KEY, 401);
  const base = graphBase(c.env);
  const headers = graphHeaders(c.env);
  try {
    const res = await fetch(`${base}/graph/neighbors/${encodeURIComponent(c.req.param('name'))}`, { headers });
    return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
  } catch (e) {
    // plugin worker 沒部署 / 不可達 → 誠實回報（前端顯示「關聯服務未部署」而非假裝無關聯）
    return c.json({ error: `kbdb-graph-plugin 不可達（${base}）：${e instanceof Error ? e.message : String(e)}` }, 502);
  }
});

// ── map（藏書地圖，library-map SDD M5/R4：console 首頁全館地圖）──────────────────
//
// 純轉發（rule 07）：聚合真身在 KBDB 基本盤（kbdb/src/routes/map.ts，M2 已 merge）。
// owner_id **不強制注入、只選擇性透傳**——與 MCP kbdb_get_map（M4）同義，理由兩層：
//   1. map block 是庫級聚合摘要（narrative＋top entities＋規模），非租戶私資料；
//   2. 現行 backfill recompute 未帶 owner（map block owner_id=NULL）——若比照 search
//      強制注入租戶，`e.owner_id = ?` 會把 NULL 列全濾掉 → 首頁永遠假空狀態（不誠實）。
// 仍要求 X-Arcrun-API-Key（與本檔其他端點同閘；console 登入後才有 tenant 字串）。

// GET /kbdb/map — 全館地圖（每庫一行：library＋narrative＋top 3 entities＋triplet_count＋updated_at）。
kbdbProxyRouter.get('/kbdb/map', async (c) => {
  if (!tenant(c)) return c.json(NEED_KEY, 401);
  const { base, headers } = kbdbBase(c.env);
  const owner = c.req.query('owner_id');
  const qs = owner ? `?owner_id=${encodeURIComponent(owner)}` : '';
  try {
    const res = await fetch(`${base}/map${qs}`, { headers });
    return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
  } catch (e) {
    // KBDB 不可達 → 誠實回報（前端顯示「地圖服務不可達」，不擋首頁其他功能）
    return c.json({ success: false, error: `KBDB 不可達（${base}）：${e instanceof Error ? e.message : String(e)}` }, 502);
  }
});

// GET /kbdb/map/:library — 該庫詳圖（完整 top_entities 帶 degree／relation_profile／bridges／commit_hash）。
kbdbProxyRouter.get('/kbdb/map/:library', async (c) => {
  if (!tenant(c)) return c.json(NEED_KEY, 401);
  const { base, headers } = kbdbBase(c.env);
  const owner = c.req.query('owner_id');
  const qs = owner ? `?owner_id=${encodeURIComponent(owner)}` : '';
  try {
    const res = await fetch(`${base}/map/${encodeURIComponent(c.req.param('library'))}${qs}`, { headers });
    return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
  } catch (e) {
    return c.json({ success: false, error: `KBDB 不可達（${base}）：${e instanceof Error ? e.message : String(e)}` }, 502);
  }
});

// PATCH /kbdb/entries/:id — 更新單筆 entry。owner_id 不可被改（剝除 caller 自帶的 owner_id）。
kbdbProxyRouter.patch('/kbdb/entries/:id', async (c) => {
  if (!tenant(c)) return c.json(NEED_KEY, 401);
  const body = await c.req.json().catch(() => ({}));
  // 不讓 patch 改 owner_id（防把別人的資料認領過來或踢給別人）
  const { owner_id: _drop, ...patch } = body ?? {};
  const { base, headers } = kbdbBase(c.env);
  const res = await fetch(`${base}/entries/${encodeURIComponent(c.req.param('id'))}`, {
    method: 'PATCH',
    headers,
    body: JSON.stringify(patch),
  });
  return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});
