/**
 * 藏書地圖 MCP 薄殼（library-map SDD M4；源頭 Arcrun#39）
 *
 * rule 07 §5（薄殼鐵律）：地圖的聚合 SQL 全住 kbdb base（actions/library-map.ts，D6 裁定），
 * MCP 只做介面轉換——經既有 KBDB service binding（kbdbFetch）打 GET /map／/map/:library，
 * 不碰 D1、不新增 binding。與 #68 kbdb_graph_neighbors 同族（D17 KBDB MCP 面，kbdb_* 前綴）。
 *
 * 端點契約（kbdb/src/routes/map.ts，M2 已 merge；entry_count 為 Arcrun#87 三次收尾新增）：
 *   GET /map           → { success, libraries:[{library, narrative, top_entities(名字 top3),
 *                          triplet_count, entry_count, updated_at}], count }
 *   GET /map/:library  → { success, map:{record_id, library, narrative, content, top_entities,
 *                          relation_profile, bridges, triplet_count, entry_count, commit_hash,
 *                          status, updated_at} }
 *   404 → { success:false, error:'not found' }（該庫從未 recompute）
 *
 * entry_count vs triplet_count（讀這份地圖前務必分清楚，否則會誤判某庫「沒有知識」）：
 *   triplet_count＝萃取出的三元組數；entry_count＝ingest 進來的原始內容（卡片/skill/block）數。
 *   兩者不同源，一個 0 一個非 0 是正常狀態——triplet_count＝0 只代表「還沒萃取關係」，
 *   不代表「沒有資料」。entry_count＞0 時該用 kbdb_search 查內容，不要因為 triplet_count＝0
 *   就跳過這個庫。
 *
 * slot 值防禦：top_entities/relation_profile/bridges 底層存 JSON 字串，正常 base 已 parse；
 * 但仍容錯「字串形直出」（live 曾觀測）——字串就 parse、失敗當空陣列，絕不 crash（鐵律）。
 */

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import type { Env } from "../types.js";
import { kbdbFetch } from "../lib/kbdb-client.js";
import { errorResponse, successResponse } from "../lib/cypher-client.js";
import { entityNames, parseSlotArray, type LibraryMapRow } from "../lib/library-map.js";
import {
  portalFetch,
  portalError,
  staleIdentityError,
  type KnowledgeIdentity,
} from "../lib/portal-client.js";

/**
 * 空庫/404 時的指引（誠實回報＋給下一步，鐵律：不假綠）。
 *
 * 2026-08-08 修正：舊版寫「地圖由 ingest 尾端自動重算（M3）」——那件事從沒接上過（總管實測
 * grep 全 repo 查無任何呼叫點），是假話。真正的機制：kbdb `GET /map`／`GET /map/:library`
 * 每次查詢都會自動核對即時三元組數，落差就地重算（`kbdb/src/actions/library-map.ts`
 * `ensureFreshLibraryMaps`）——資料進來後下一次查詢就會反映，不需要任何人記得呼叫任何端點。
 * `POST /map/recompute` 仍然存在，但只在極少數情況才需要手動打：舊三元組沒有 `library` slot
 * 值、只能靠 `source_uri` 前綴回填時（`source_prefix` 參數）。
 */
const RECOMPUTE_HINTS = [
  "地圖每次查詢都會自動核對即時三元組數並重算過期的庫，不必手動處理",
  "少見情況（舊三元組沒有 library 標記）才需要手動：POST /map/recompute?library=<庫名>（可帶 body {narrative, source_prefix}）",
];

/** 註冊全部藏書地圖工具（library-map M4）。 */
export function registerAllKbdbMapTools(server: McpServer, env: Env, identity: KnowledgeIdentity) {
  registerGetMap(server, env, identity);
}

/** 單庫詳圖回傳形狀（GET /map/:library 的 map，slot 陣列已 parse 成物件）。 */
interface LibraryMapDetail {
  record_id?: string;
  library?: string;
  narrative?: string | null;
  content?: string | null;
  top_entities?: unknown;
  relation_profile?: unknown;
  bridges?: unknown;
  triplet_count?: number | string;
  // Arcrun#87 三次收尾：同 lib/library-map.ts LibraryMapRow.entry_count 的欄位（見該檔註解）。
  entry_count?: number | string;
  commit_hash?: string | null;
  status?: string;
  updated_at?: number;
}

/** triplet_count＝0 但 entry_count＞0 時給的提示（別讓「0 triplets」被讀成「這庫沒有知識」）。 */
function entryOnlyHint(tripletCount: number, entryCount: number, library?: string): string[] {
  if (tripletCount > 0 || entryCount <= 0) return [];
  const lib = library ? `「${library}」` : "這個庫";
  return [
    `${lib} triplet_count＝0，但有 ${entryCount} 筆原始內容（entries）——三元組萃取還沒對它跑過，` +
      "不代表沒有知識。用 kbdb_search（關鍵字或語義）直接查得到內容。",
  ];
}

/**
 * kbdb_get_map — 藏書地圖。無參數＝全館（每庫一行）；帶 library＝該庫詳圖。
 * design §6 retrieval 流程的第一站：地圖 → get_map(library) 細節 → graph/search 進庫。
 */
export function registerGetMap(server: McpServer, env: Env, identity: KnowledgeIdentity) {
  server.tool(
    "kbdb_get_map",
    "藏書地圖：KBDB 全館導覽。不帶參數＝全館地圖（每庫一行：庫名＋narrative＋核心 top 3 entities＋" +
      "triplet 數），帶 library 參數＝該庫詳圖（完整 top_entities/relation_profile/跨庫 bridges）。" +
      "不確定該查什麼時，先呼叫此工具——先看地圖定位該進哪個庫，再用 kbdb_search（關鍵字/語義）或 " +
      "kbdb_graph_neighbors（關係遍歷）進庫查細節。",
    {
      library: z.string().min(1).optional().describe(
        "庫名（如 'kb'／'notes'）。帶了回該庫詳圖；不帶回全館地圖（先看全館再挑庫）",
      ),
      owner_id: z.string().optional().describe(
        "限定某資料歸屬範圍（選填；登入身分下不生效，看得到哪些庫由你的帳號權限決定）",
      ),
    },
    async ({ library, owner_id }) => {
      if (identity.kind === "stale") return staleIdentityError();
      try {
        // 登入身分：走 cypher 的 portal 資料面 —— 只會回這個帳號有權限的庫
        //（KBDB 的 /map 對權限無知，會回全館；過濾在 cypher 那邊 server 側做）。
        const isPortal = identity.kind === "portal";
        const qs = !isPortal && owner_id ? `?owner_id=${encodeURIComponent(owner_id)}` : "";
        const mapFetch = (path: string) =>
          identity.kind === "portal"
            ? portalFetch(env, identity.portal.session, `/portal/data${path}`)
            : kbdbFetch(env, path);

        if (!library) {
          // 全館地圖：每庫一行（library＋narrative＋top 3 entities＋triplet_count）。
          const res = await mapFetch(`/map${qs}`);
          if (!res.ok && isPortal) return portalError(res, "取全館地圖");
          if (!res.ok) {
            return errorResponse(
              "map_fetch_failed",
              `取全館地圖失敗 HTTP ${res.status}`,
              ["稍後重試", ...RECOMPUTE_HINTS],
              await res.text().catch(() => ""),
            );
          }
          const data = (await res.json()) as { libraries?: LibraryMapRow[]; count?: number; note?: string };
          const libraries = (Array.isArray(data.libraries) ? data.libraries : []).map((l) => ({
            ...l,
            // 防禦：top_entities 若是 JSON 字串形就 parse 成名字清單（失敗當空，誠實不 crash）。
            top_entities: entityNames(l.top_entities, 3),
            triplet_count: Number(l.triplet_count ?? 0) || 0,
            entry_count: Number(l.entry_count ?? 0) || 0,
          }));
          if (libraries.length === 0) {
            // 空庫誠實回報：不是錯誤（端點正常）。地圖是讀時即時核對重算的（見 RECOMPUTE_HINTS
            // 註解），所以「地圖是空的」現在真的等於「這個租戶目前沒有任何三元組資料」，
            // 不再是「沒人跑過 recompute」那種曖昧狀態。
            // 登入身分下還有第二種可能：這個帳號一個庫都沒被授權——「沒權限看」與「沒有資料」
            // 不可以長得一樣，所以分開講（cypher 端會附 note 說明）。
            return successResponse({ libraries: [], count: 0 }, [
              isPortal
                ? "看不到任何庫：可能是這個知識庫真的還沒有三元組資料，也可能是你的帳號還沒被授權任何庫——請向管理員確認你的可用知識庫"
                : "全館地圖是空的：這個租戶目前沒有任何三元組資料（不是地圖沒算，是真的還沒有資料）",
              ...(data.note ? [data.note] : []),
              ...RECOMPUTE_HINTS,
            ]);
          }
          // Arcrun#87 三次收尾：某些庫 triplet_count＝0 但 entry_count＞0（entries 有、三元組
          // 萃取沒跑過）——這件事只在「這種庫真的存在」時才提一次，不是每個庫都印一行（避免洗版），
          // 讓讀這份地圖的 session 知道「觸目所及的 0」不能直接當成「沒有知識」。
          const entryOnlyLibs = libraries.filter(
            (l) => (l.triplet_count ?? 0) === 0 && (l.entry_count ?? 0) > 0,
          );
          return successResponse({ libraries, count: libraries.length }, [
            "要看某庫細節：kbdb_get_map(library='庫名')",
            "進庫查內容：kbdb_search（關鍵字/語義）；查關係：kbdb_graph_neighbors",
            ...(entryOnlyLibs.length > 0
              ? [
                  `${entryOnlyLibs.map((l) => l.library).join("、")} 這幾庫 triplet_count＝0 但 entry_count＞0：` +
                    "有原始內容，只是還沒萃取出三元組關係——別把 0 triplets 讀成「沒有知識」，直接 kbdb_search 進去查。",
                ]
              : []),
          ]);
        }

        // 單庫詳圖：完整 slots（slot 陣列 parse 成物件再回）。
        const res = await mapFetch(`/map/${encodeURIComponent(library)}${qs}`);
        if (res.status === 404) {
          // 地圖是讀時即時核對重算的：只要這個庫「已知」（有三元組、entries 蓋過章、或登記過），
          // 上一步就會自動把它補成一筆 triplet_count:0 的地圖，走不到這個分支。真的落到 404，
          // 代表這個名字在這個租戶的資料裡從沒出現過——不是「這庫是空的」，是根本沒有這個庫
          // （可能打錯字，或這個庫在別的租戶／別的 owner_id 底下）。
          return errorResponse(
            "map_not_found",
            isPortal
              ? `查無庫「${library}」——這個名字不存在，或不在你被授權的知識庫範圍內（兩者刻意同一句話，不洩漏某個庫存不存在）`
              : `查無庫「${library}」——這個名字在這個租戶的資料裡從沒出現過（不是「這庫是空的」，是根本沒有這個庫；地圖是即時核對重算的，不是忘了 recompute）`,
            ["kbdb_get_map 不帶參數看全館有哪些庫（確認庫名／確認你有權限的庫）", ...RECOMPUTE_HINTS],
          );
        }
        if (!res.ok && isPortal) return portalError(res, `取庫「${library}」詳圖`);
        if (!res.ok) {
          return errorResponse(
            "map_fetch_failed",
            `取庫「${library}」詳圖失敗 HTTP ${res.status}`,
            ["稍後重試", ...RECOMPUTE_HINTS],
            await res.text().catch(() => ""),
          );
        }
        const data = (await res.json()) as { map?: LibraryMapDetail };
        const raw = data.map ?? {};
        const map = {
          ...raw,
          // slot 字串容錯 parse（任務規格：JSON 字串形要 parse 成物件再回，失敗當空陣列）。
          top_entities: parseSlotArray<{ name: string; degree: number }>(raw.top_entities),
          relation_profile: parseSlotArray<{ predicate: string; count: number }>(raw.relation_profile),
          bridges: parseSlotArray<{ entity: string; libraries: string[] }>(raw.bridges),
          triplet_count: Number(raw.triplet_count ?? 0) || 0,
          entry_count: Number(raw.entry_count ?? 0) || 0,
        };
        return successResponse({ map }, [
          "bridges＝此庫 entity 同時出現在哪些其他庫（只有兩側三元組都標了 library 值才抓得到，舊資料若沒標會偏稀疏，是誠實現況不是 bug）",
          "沿核心 entity 挖關係：kbdb_graph_neighbors(subject=entity 名)",
          ...entryOnlyHint(map.triplet_count, map.entry_count, library),
        ]);
      } catch (e) {
        return errorResponse("internal_error", e instanceof Error ? e.message : String(e), ["稍後重試"]);
      }
    },
  );
}
