/**
 * Skills + Examples lookup MCP tools — LI SDD M3.2 / M3.4
 *
 * 對應 docs/3-specs/llm-interface/ Milestone 3.2 + 3.4。
 *
 * - arcrun_list_skills      — 列 KBDB entry_type=agent-skill 全部
 * - arcrun_get_skill        — 用 slug 拿 skill markdown 全文
 * - arcrun_list_examples    — 列 KBDB entry_type=workflow-example 全部
 * - arcrun_get_example      — 用 slug 拿 example yaml + description + tags
 * - arcrun_search_examples  — use case 關鍵字 → 命中相關 example
 *
 * Skills / examples 由 arcrun/scripts/sync-registry-to-kbdb.py 從
 * arcrun/registry/{skills,examples} 同步進 KBDB。
 *
 * 直接走 KBDB service binding（既有 pattern），不經 cypher-executor。
 *
 * 2026-06-14 重寫：KBDB 降基本盤後（三表 entries/templates/records，無 v3 blocks 表、
 * 無語義 search），原打 /blocks /search 的舊路徑全失效。改打基本盤 /entries：
 *   - entry_type 取代 blocks 的 type 欄（entries 表原生有 entry_type/page_name/tags_json/metadata_json）
 *   - GET /blocks?type=X       → GET /entries?entry_type=X
 *   - GET /blocks?page_name=Y  → GET /entries?page_name=Y（base listEntries 加了 page_name 過濾）
 *   - POST /search（語義）      → GET /entries/search?q=（D1 LIKE 關鍵字，基本盤無語義；
 *       誠實降級：search_examples 現在是「關鍵字」非「語義」。embed 模組（kbdb-base Phase 1）
 *       上線後只換內部、工具簽名不變。
 */

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { toolName } from "../brand.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 { staleIdentityError, type KnowledgeIdentity } from "../lib/portal-client.js";

/**
 * 🔴 2026-08-13：**「讀不到」不准長得像「不存在」**（Arcrun#100／#109 同一族）。
 *
 * 實撞（總管在 leo21c／portal-login 連線上親跑，且已對照證實）：
 *   `arcrun_get_skill('write_intent_workflow')` → 「skill "write_intent_workflow" **不存在**」
 *   但 `kbdb_search` 撈得到 `page_name: "skill-write_intent_workflow"`、
 *   `entry_type: "agent-skill"`、`source: "installer-seed"`——**與本工具查的鍵逐字相符**。
 * 真兇：下面的 `kbdbGetByPageName` 舊版寫 `if (!resp.ok) return null;`
 * ⇒ **一個 HTTP 401 被逐字翻譯成「不存在」**。
 *
 * 401 從哪來（不是這支 code 的錯，但這支 code 把它講成了假話）：`kbdb-client.ts` 只在
 * `env.KBDB_INTERNAL_TOKEN` 存在時才掛 Authorization ⇒ 沒設就匿名送出 ⇒ KBDB 回 401。
 * 而該實例的 `arcrun-mcp` 很可能根本沒拿到那把 token（安裝器的 secret 迴圈漏了它）。
 * ⇒ **修 token 是部署面的事（leo 的手）；這支 code 該做的是把話講對。**
 *
 * 為什麼講對很重要：instructions 叫 AI「第一步先讀 skill」。它照做、拿到「不存在」，
 * 於是結論是「這裡沒有 skill」⇒ 去猜、去 grep repo、去上網——**那正是「AI 是瞎的」的機械過程**。
 *
 * 現在：非 2xx 一律拋 `KbdbAccessError`（帶真的 status），由 `kbdbFailure()` 講成它自己
 * （401/403＝讀不到；5xx／連不上＝連不上），並把**還走得通的那條路**（`kbdb_search`）交給 AI。
 * `not_found` 只保留給「KBDB 正常回應、但真的沒有這張卡」。
 */
class KbdbAccessError extends Error {
  constructor(
    readonly status: number,
    readonly what: string,
    readonly detail?: string,
  ) {
    super(`KBDB ${what} HTTP ${status}`);
  }
}

/**
 * KBDB 讀取失敗 → 誠實的錯誤回覆。
 *
 * `searchHint` 是「同一份內容還能從哪裡拿」——這批 registry 卡片（skill/example）本身就住在
 * KBDB entries 裡，而 `kbdb_search` 走的是**另一條**路（登入身分走 portal 資料面），
 * 這條 401 時那條常常還活著（2026-08-13 實測即如此）。所以這不是安慰話，是真的可行的下一步。
 */
function kbdbFailure(e: unknown, searchHint: string, identity: KnowledgeIdentity) {
  const portalNote =
    identity.kind === "portal"
      ? "你這條是帳密登入的連線，但 skill／example 這批工具目前仍走**服務內部憑據**（還沒接上登入身分）——" +
        "所以它讀不到，不代表你的帳號讀不到。"
      : "";
  if (e instanceof KbdbAccessError) {
    const unauthorized = e.status === 401 || e.status === 403;
    return errorResponse(
      unauthorized ? "kbdb_unauthorized" : "kbdb_unreachable",
      (unauthorized
        ? `讀不到 KBDB（HTTP ${e.status}：這條連線的憑據被拒或根本沒帶）。`
        : `讀不到 KBDB（HTTP ${e.status}）。`) +
        "🔴 **這是「讀不到」，不是「不存在」**——內容還在庫裡，只是這條路被擋住了。" +
        (portalNote ? ` ${portalNote}` : ""),
      [
        searchHint,
        "kbdb_get_map() 看這台實例有哪些庫（那條走得通就更確定是這批工具的路壞了，不是庫空了）",
        "🔴 不准把這個錯誤回報成「找不到／沒有這個 skill」——請照實說「KBDB 這條路讀不到」",
        "持續失敗：告訴 leo 這台實例的 arcrun-mcp 少了 secret KBDB_INTERNAL_TOKEN",
      ],
      e.detail,
    );
  }
  return errorResponse(
    "kbdb_unreachable",
    `讀不到 KBDB：${e instanceof Error ? e.message : String(e)}。` +
      "🔴 **這是「讀不到」，不是「不存在」**。" +
      (portalNote ? ` ${portalNote}` : ""),
    [searchHint, "稍後重試", "🔴 不准把它回報成「沒有這個 skill／example」"],
  );
}

// 基本盤 entries row（與舊 v3 block 欄位 1:1，差別只在 type→entry_type）
interface KbdbBlock {
  id: string;
  page_name?: string | null;
  content?: string | null;
  entry_type?: string;
  tags_json?: string;
  metadata_json?: string | null;
  source?: string | null;
  updated_at?: number;
}

async function kbdbList(env: Env, entryType: string, limit = 100): Promise<KbdbBlock[]> {
  const resp = await kbdbFetch(env, `/entries?entry_type=${encodeURIComponent(entryType)}&limit=${limit}`);
  if (!resp.ok) {
    throw new KbdbAccessError(resp.status, `list entry_type=${entryType}`, await resp.text().catch(() => ""));
  }
  const data = await resp.json<{ entries?: KbdbBlock[] }>();
  return data.entries ?? [];
}

/**
 * 依 page_name 取一張卡。
 *
 * 🔴 回 `null` **只代表「KBDB 好好回答了，而它說沒有這張卡」**。
 * 讀不到（401／5xx／連不上）一律拋 `KbdbAccessError`——**絕不 return null**，
 * 否則呼叫端會把它講成「不存在」（2026-08-13 的真實事故，見檔頭）。
 */
async function kbdbGetByPageName(env: Env, pageName: string): Promise<KbdbBlock | null> {
  const resp = await kbdbFetch(env, `/entries?page_name=${encodeURIComponent(pageName)}&limit=1`);
  if (!resp.ok) {
    throw new KbdbAccessError(resp.status, `get page_name=${pageName}`, await resp.text().catch(() => ""));
  }
  const data = await resp.json<{ entries?: KbdbBlock[] }>();
  return data.entries?.[0] ?? null;
}

function parseTags(tagsJson?: string): string[] {
  if (!tagsJson) return [];
  try {
    const arr = JSON.parse(tagsJson);
    return Array.isArray(arr) ? arr : [];
  } catch {
    return [];
  }
}

export function registerListSkills(server: McpServer, env: Env, identity: KnowledgeIdentity) {
  server.tool(
    toolName("list_skills"),
    "列所有 agent-skill blocks（從 arcrun/registry/skills/ 同步進 KBDB）。每個 skill 是個 markdown playbook，描述 AI 面對 X 問題該怎麼想 + 該用哪個 example。回 [{slug, title, tags}]。call get_skill(slug) 拿完整內文。",
    {
      tag: z.string().optional().describe("optional 標籤過濾。如 'rag' / 'watcher' / 'debug'"),
    },
    async ({ tag }) => {
      if (identity.kind === "stale") return staleIdentityError();
      try {
        const blocks = await kbdbList(env, "agent-skill", 100);
        const skills = blocks
          .map((b) => {
            const tags = parseTags(b.tags_json);
            let title = b.page_name?.replace(/^skill-/, "") ?? "(no title)";
            try {
              const meta = b.metadata_json ? JSON.parse(b.metadata_json) : null;
              if (meta?.title) title = meta.title;
            } catch {}
            return {
              slug: b.page_name?.replace(/^skill-/, "") ?? "",
              page_name: b.page_name,
              title,
              tags,
              chars: (b.content ?? "").length,
            };
          })
          .filter((s) => !tag || s.tags.includes(`skill:${tag}`) || s.tags.includes(tag) || s.slug.includes(tag));

        return successResponse(
          { count: skills.length, skills },
          [
            skills.length === 0
              ? "沒有 skill 命中。試 list_skills() 不帶 tag 看全部"
              : "call arcrun_get_skill(slug) 拿單個 skill 完整 markdown",
            // 誠實：這裡回的是**這台實例被 seed 進去的那幾支**，不是「全世界的 skill 目錄」。
            // 上面清單沒有的名字（例如 'INDEX'）就是這台沒有——別照舊教材去猜一個 slug。
            "🔴 只用上面清單裡真的有的 slug；清單沒有＝這台實例沒 seed 進去，不要硬猜名字",
          ],
        );
      } catch (e) {
        return kbdbFailure(e, "改用 kbdb_search({ q: 'skill' }) 直接在知識庫裡找 skill 卡片（那條路走的是另一組憑據）", identity);
      }
    },
  );
}

export function registerGetSkill(server: McpServer, env: Env, identity: KnowledgeIdentity) {
  server.tool(
    toolName("get_skill"),
    "拿單一 agent-skill 完整 markdown playbook。slug 從 list_skills 取得。",
    {
      slug: z.string().describe("skill slug，例如 'build_watcher_workflow' / 'rag_with_arcrun'"),
    },
    async ({ slug }) => {
      if (identity.kind === "stale") return staleIdentityError();
      try {
        const pageName = slug.startsWith("skill-") ? slug : `skill-${slug}`;
        const block = await kbdbGetByPageName(env, pageName);
        if (!block) {
          // 走到這裡＝KBDB **有正常回答**，而它說沒有這張卡（讀不到的情況上面已經拋出去了）。
          return errorResponse(
            "not_found",
            `KBDB 正常回應，但沒有 page_name="${pageName}" 這張卡——這台實例沒有 seed 這支 skill。` +
              "（不同實例 seed 的 skill 不一樣，別照舊教材假設某個名字一定在。）",
            [
              "call arcrun_list_skills() 看**這台實例真的有**哪幾支",
              `kbdb_search({ q: '${slug}' }) 看內容是不是被存成別的名字`,
              "確認拼字正確（不需要 'skill-' prefix）",
            ],
          );
        }
        return successResponse({
          slug,
          page_name: block.page_name,
          content: block.content,
          tags: parseTags(block.tags_json),
        });
      } catch (e) {
        return kbdbFailure(
          e,
          `改用 kbdb_search({ q: 'skill-${slug}' }) 撈同一張卡片（skill 就住在知識庫的 entries 裡，那條路走另一組憑據）`,
          identity,
        );
      }
    },
  );
}

export function registerListExamples(server: McpServer, env: Env, identity: KnowledgeIdentity) {
  server.tool(
    toolName("list_examples"),
    "列所有 workflow-example blocks（從 arcrun/registry/examples/ 同步進 KBDB）。每個 example 是可直接 push 的 workflow YAML 範本 + description。回 [{slug, tags}]。call get_example / search_examples 拿細節。",
    {
      tag: z.string().optional().describe("optional 標籤過濾。如 'rag' / 'cron' / 'llm' / 'webhook'"),
    },
    async ({ tag }) => {
      if (identity.kind === "stale") return staleIdentityError();
      try {
        const blocks = await kbdbList(env, "workflow-example", 200);
        const examples = blocks
          .map((b) => {
            const tags = parseTags(b.tags_json);
            return {
              slug: b.page_name?.replace(/^example-/, "") ?? "",
              page_name: b.page_name,
              tags,
              chars: (b.content ?? "").length,
            };
          })
          .filter((e) => !tag || e.tags.includes(tag) || e.tags.includes(`example:${tag}`) || e.slug.includes(tag));

        return successResponse(
          { count: examples.length, examples },
          [
            examples.length === 0
              ? "沒有 example 命中。試 list_examples() 不帶 tag 看全部"
              : "call arcrun_get_example(slug) 拿單個 YAML + description",
          ],
        );
      } catch (e) {
        return kbdbFailure(e, "改用 kbdb_search({ q: 'example' }) 直接在知識庫裡找 example 卡片（那條路走另一組憑據）", identity);
      }
    },
  );
}

export function registerGetExample(server: McpServer, env: Env, identity: KnowledgeIdentity) {
  server.tool(
    toolName("get_example"),
    "拿單一 workflow-example 完整 YAML + description。slug 從 list_examples / search_examples 取得。可直接拿 YAML 改成你自己的 → push。",
    {
      slug: z.string().describe("example slug，例如 'rag-search-answer' / 'cron-watcher'"),
    },
    async ({ slug }) => {
      if (identity.kind === "stale") return staleIdentityError();
      try {
        const pageName = slug.startsWith("example-") ? slug : `example-${slug}`;
        const block = await kbdbGetByPageName(env, pageName);
        if (!block) {
          // KBDB 好好回答了，而它說沒有這張卡（讀不到的情況已在 kbdbGetByPageName 拋出）。
          return errorResponse(
            "not_found",
            `KBDB 正常回應，但沒有 page_name="${pageName}" 這張卡——這台實例沒 seed 這個 example。`,
            [
              "call arcrun_list_examples() 看**這台實例真的有**哪些 slug",
              "或 arcrun_search_examples(use_case) 用關鍵字找",
              `kbdb_search({ q: '${slug}' }) 看內容是不是被存成別的名字`,
            ],
          );
        }

        let description_md = "";
        try {
          const meta = block.metadata_json ? JSON.parse(block.metadata_json) : null;
          description_md = meta?.description_md ?? "";
        } catch {}

        return successResponse({
          slug,
          page_name: block.page_name,
          workflow_yaml: block.content,
          description_md,
          tags: parseTags(block.tags_json),
        }, [
          "拿 workflow_yaml 改成你自己的 → call arcrun_push_workflow",
          "看 description_md 了解設計意圖 / 改造方向",
        ]);
      } catch (e) {
        return kbdbFailure(
          e,
          `改用 kbdb_search({ q: 'example-${slug}' }) 撈同一張卡片（example 就住在知識庫的 entries 裡）`,
          identity,
        );
      }
    },
  );
}

export function registerSearchExamples(server: McpServer, env: Env, identity: KnowledgeIdentity) {
  server.tool(
    toolName("search_examples"),
    "用 use case 關鍵字搜 workflow examples，回最相關 N 個。" +
      "注意：基本盤目前是 D1 LIKE 關鍵字搜尋（非語義 embedding；語義是 kbdb-base Phase 1 的 embed 模組，尚未上）。" +
      "→ 用具體詞（'email'、'cron'、'rag'）比整句自然語言命中率高。也會比對 slug/tag。",
    {
      query: z.string().min(2).describe("use case 關鍵字，例如 'email 摘要' / 'cron 排程' / 'rag'。基本盤是關鍵字非語義，用詞要具體"),
      top_k: z.number().int().min(1).max(20).optional().describe("回幾個結果（預設 5）"),
    },
    async ({ query, top_k }) => {
      if (identity.kind === "stale") return staleIdentityError();
      try {
        const k = top_k ?? 5;
        const q = query.trim();

        // 基本盤無語義 search：撈全部 workflow-example，用 query 對 content/slug/tag 做關鍵字比對排序。
        // （examples 只有 ~10 筆，client 端過濾零負擔；embed 模組上線後可改打語義 search）
        const blocks = await kbdbList(env, "workflow-example", 200);
        const ql = q.toLowerCase();
        const terms = ql.split(/\s+/).filter(Boolean);

        const scored = blocks
          .map((b) => {
            const slug = b.page_name?.replace(/^example-/, "") ?? "";
            const tags = parseTags(b.tags_json);
            const hay = `${slug} ${tags.join(" ")} ${(b.content ?? "")}`.toLowerCase();
            // 每個 term 命中 +1；slug/tag 命中額外加權
            let score = 0;
            for (const t of terms) {
              if (hay.includes(t)) score += 1;
              if (slug.toLowerCase().includes(t)) score += 2;
              if (tags.some((tag) => tag.toLowerCase().includes(t))) score += 2;
            }
            return { b, slug, tags, score };
          })
          .filter((r) => r.score > 0)
          .sort((a, b) => b.score - a.score)
          .slice(0, k);

        const examples = scored.map((r) => ({
          slug: r.slug,
          page_name: r.b.page_name,
          score: r.score,
          tags: r.tags,
          preview: (r.b.content ?? "").slice(0, 200),
        }));

        if (examples.length === 0) {
          return successResponse(
            { count: 0, examples: [], query: q },
            [
              "關鍵字沒命中（基本盤是 LIKE 非語義，換更具體/不同的詞再試）",
              "改用 arcrun_list_examples(tag='...') 走 tag 過濾",
              "或 arcrun_list_examples() 看全部清單自己挑",
            ],
          );
        }

        return successResponse(
          { count: examples.length, examples, query: q, search_mode: "keyword" },
          [
            "call arcrun_get_example(slug) 拿完整 YAML",
            "score 高 = 關鍵字命中越多（slug/tag 命中加權）",
            "search_mode:keyword — 基本盤無語義，命中靠字面；換具體詞可改善",
          ],
        );
      } catch (e) {
        return kbdbFailure(e, `改用 kbdb_search({ q: '${query.trim()}' }) 直接查知識庫（那條路走另一組憑據）`, identity);
      }
    },
  );
}

export function registerAllSkillExampleTools(server: McpServer, env: Env, identity: KnowledgeIdentity) {
  registerListSkills(server, env, identity);
  registerGetSkill(server, env, identity);
  registerListExamples(server, env, identity);
  registerGetExample(server, env, identity);
  registerSearchExamples(server, env, identity);
}
