import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
import { registerAllTools } from "./tools/registry.js";
import { buildLibraryMapInstructions } from "./lib/library-map.js";
import type { KnowledgeIdentity } from "./lib/portal-client.js";
import { Env } from "./types.js";

/**
 * 【這條連線上有主人的知識庫】——**靜態、必定出現**的指路段（2026-08-13，leo）。
 *
 * leo 原話：「它應該是**你的知識來源**⋯⋯**沒有它你是瞎的**，從你對 Arcrun 的認知就知道
 * 你的內建 Memory 是沒用的。」
 *
 * 為什麼要獨立成一段、而且**不准依賴任何查詢**：
 * 這條連線本來就查得到答案——`kbdb_search(q="Arcrun 是什麼")` 當天實測回 33 筆真答案。
 * 但同一天，一個連著這條 MCP 的 session 為了回答同一題，去讀了 682 行原始碼——
 * **因為它不知道這裡有知識庫可查**。instructions 裡唯一提到知識的，是下面那段【藏書地圖】，
 * 而那段是**選配的**（1500ms 逾時、失敗回 null）；那條 portal 連線就沒收到它。
 * ⇒ **「這裡有知識庫、怎麼查」這句話本身被綁在一個會無聲消失的段落上，才是 AI 開場全瞎的成因。**
 * 所以它跟下面的 Arcrun 指路段同級：靜態常數，KBDB 掛了、地圖抓不到，它照樣出現。
 */
const KNOWLEDGE_FIRST = [
  "【這條連線上有主人的知識庫——先查它，再查別的】",
  "",
  "這條 MCP 連線後面接著一個 **KBDB 知識庫**：這台實例的主人長期累積的筆記、決策、",
  "踩過的坑、專案現況、skill 與工作流紀錄，都在裡面。**你不是從零開始的**——",
  "你對這些專案的內建印象多半是錯的或過時的，庫裡那份才是主人認的版本。",
  "",
  "🔴 **有人問你「X 是什麼／為什麼這樣做／之前怎麼決定的／現在做到哪」——",
  "你的第一個動作是 `kbdb_search`，不是 grep 原始碼、不是上網搜、不是回答「我不知道」。**",
  "",
  '- `kbdb_search({ q: "Arcrun 是什麼" })` — 關鍵字查（預設 `mode:\'keyword\'`，基本盤永遠可用）。',
  "  換幾組講法再放棄；想要語義相似度用 `mode:'semantic'`。**這一支是你的第一站。**",
  "- `kbdb_get_map()` — 不知道該進哪個庫時先看藏書地圖（下面若有【藏書地圖】就是它的快照）。",
  '- `kbdb_graph_neighbors({ subject: "Arcrun" })` — 查某個東西跟誰有關係（三元組遍歷）。',
  "- `kbdb_list_templates` / `kbdb_query` — 按 template 取整批結構化資料。",
  "",
  "🔴 **這三件事不可以講成同一句**（講成同一句就是在騙人）：",
  "① 「知識庫裡沒有」　② 「我沒查」　③ 「地圖沒取到／某庫顯示 0」。",
  "查過真的沒有 → 明說「知識庫裡查不到，以下是我從原始碼／網路推的」，再去讀 code 或上網。",
  "**沒查就回答＝拿你的猜測冒充主人的知識，那是這條連線上最嚴重的錯。**",
  "",
  "🔴 **地圖是索引，不是庫存清單**：某庫顯示 `0 triplets`、或下面整段【藏書地圖】沒出現，",
  "都**不代表**沒有知識（可能只是還沒重算、或這次沒抓到）。要知道有沒有，只有一個方法：`kbdb_search` 查過。",
  "同理，任何工具回 401／連不上／沒權限，那是**讀不到**，不是**不存在**——照它給的 next_actions 修，",
  "別把它改口講成「這裡沒有」（`arcrun_get_skill` 曾把 KBDB 的 401 講成「skill 不存在」，就是這個病）。",
].join("\n");

/** 地圖沒拿到時的**明講**（不可靜默）：「沒取到」和「這裡沒有知識」不可以長得一樣。 */
const MAP_UNAVAILABLE_NOTE = [
  "【藏書地圖：這次沒取到】",
  "地圖抓取逾時／回錯／或它回報的清單是空的（也可能只是還沒重算過）。",
  "🔴 **這是「地圖沒拿到」，不是「這裡沒有知識」。** 上面那條規則照舊：",
  "要知道庫裡有什麼，直接 `kbdb_search`；想再抓一次地圖呼叫 `kbdb_get_map()`（它會回報真正的原因）。",
].join("\n");

/** 舊 token（stale）：拿不到地圖是**身分問題**，同樣要明講，並給可執行的修法。 */
const MAP_STALE_NOTE = [
  "【藏書地圖：拿不到，因為這條連線是舊版簽發的 token】",
  "這條連線的 token 沒帶登入者身分，`kbdb_*` 會回 `identity_missing`。",
  "🔴 **這不代表知識庫是空的**——是這條連線還沒認得你。",
  "仍然先呼叫一次 `kbdb_search` 確認錯誤碼；若真的是 `identity_missing`，",
  "請使用者到 claude.ai → Settings → Connectors 把這個 connector 重新連線一次（重新輸入 Portal 帳密），",
  "**不要改口說「查不到資料」或自己去猜答案。**",
].join("\n");

/**
 * 組這條連線的 server instructions（`initialize` 時送出，**AI 沒辦法自己再要一次** ⇒ 必須一次到位）。
 *
 * 匯出給測試：三種情境（地圖抓得到／抓不到／舊 token）各印一份完整字串，逐份確認
 * 「一個什麼都不知道的 AI 讀完，下一個動作會不會是去查知識庫」。
 */
export async function buildServerInstructions(
  env: Env,
  identity: KnowledgeIdentity,
): Promise<string> {
  // library-map SDD M4（design §4/§6）：連線時把全館藏書地圖嵌進 server instructions，
  // session 一開就知道館裡有哪些庫（push 零查詢）。builder 內建 timeout＋isolate TTL 快取
  //（選型理由見 lib/library-map.ts 檔頭）；任何失敗回 null → 絕不擋 MCP 連線（鐵律）。
  //
  // 🔴 2026-08-12：以帳密連線時**改用登入者的身分**組地圖——否則 instructions 會把
  //    整個知識庫的庫名一次推給一個可能只有部分權限的帳號（地圖本身就是情報）。
  //    快取也因此改成 per-session key（見 lib/library-map.ts）。
  //
  // 🔴 2026-08-13：失敗**不再靜默略過**。原本 null → 整段消失，於是「地圖沒取到」與
  //    「這裡沒有知識」在 AI 眼裡長得一模一樣（Arcrun#109 同一族：保險拒絕了 vs 程式碼不存在，
  //    畫面上都是沉默）。現在改成印一句明話。鐵律沒變——**失敗仍然不擋連線**，只是不再無聲。
  const mapInstructions = await buildLibraryMapInstructions(env, identity);

  // 2026-07-30（leo 問「人類說『幫我用 arcrun 寫 xxx』，Haiku 會知道要用這些資源嗎？
  // 如果不會，要寫什麼在外面讓它一聽到就知道？」）：
  // 實測答案是**不會**——AI 只看到 41 個工具名，會自己猜（很可能直接跳到 push_workflow
  // 瞎編，或去讀 registry/examples 那 8/13 引用不存在零件的壞範例）。
  // instructions 是唯一「AI 一連上就必看」的欄位 ⇒ 開場就指路，不依賴任何查詢成功。
  // ⚠️ 這段是靜態常數：即使 KBDB 掛了、藏書地圖抓不到，它也必須出現（鐵律：不擋連線）。
  const startHere = [
    "# Arcrun — 你已經配備了這套工具，別上網找",
    "",
    "**Arcrun 是什麼**：跑在 Cloudflare 上的工作流引擎（類 n8n）。你用 `>>` 寫「意圖」，",
    "系統告訴你有哪些現成零件／recipe 可用，你只要填 payload——**不必自己寫程式**。",
    "**你現在就有完整能力**：查零件、查 recipe、看實跑過的 workflow、部署、觸發、看執行紀錄。",
    "⚠️ **不要上網搜 Arcrun 文件**（網路上沒有／會過時）。答案都在下面的工具裡。",
    "",
    "【先讀這裡】要在 Arcrun 上做任何事（用戶說「幫我用 Arcrun 做 X」），**照這個順序**：",
    "",
    "1. `arcrun_get_skill('write_intent_workflow')` — **必讀第一支**。",
    "   教你用 `>>` 寫「意圖工作流」。你**不需要先知道有哪些零件**，先寫意圖。",
    "   ⚠️ 這支若回錯（401／連不上／`kbdb_unreachable`），那是**這條連線讀不到 KBDB**，",
    "   **不是 skill 不存在**——同一份內容用 `kbdb_search({ q: 'skill-write_intent_workflow' })` 撈得到。",
    "2. `arcrun_whoami()` — 確認連到哪個帳號（勿自行 curl 猜帳號 URL）。",
    "3. 把意圖丟 `POST /cypher/search` 或 `arcrun_validate_yaml` — 系統告訴你哪些零件存在。",
    "4. 卡住／不知道該查什麼 → 先 `arcrun_list_skills()` **看這台實例真的有哪幾支**，再挑一支讀。",
    "   （2026-08-13 實測：不同實例 seed 的 skill 不一樣，有的實例只有兩支、連 `INDEX` 都沒有。",
    "   **不要照教材直接指名一個 slug** ——先列清單，或 `kbdb_search({ q: 'skill' })` 直接在庫裡找。）",
    "5. 缺零件時：缺 API → 寫 recipe（`arcrun_recipe_push`）；缺能力 → 投稿零件 PR。",
    "   🔴 **不要因為查不到零件就改寫成 `code` 節點**——那叫「腹語術」（表面用 Arcrun、",
    "   實際全寫 JS）。`code` 只用於局部整形（例：剝掉 LLM 回應的雜訊）。",
    "",
    "邊：`ON_SUCCESS`（成功往下）、`對每個 <變數>`（FOREACH）、",
    "以及**條件分支**（2026-08-01 起引擎支援）：",
    "`ON_TRUE`／`ON_FALSE`（配 `if_control`）、`ON_BRANCH`＋`branch:` 標籤",
    "（配 `switch` 的每個 case／`try_catch` 的 try·catch）。",
    "🔴 **需要判斷時用分支邊，不要寫 code 判斷**——查零件的回應會附 `branch_hint`",
    "（哪些邊型＋可照抄範例），照著接即可。",
    "🔴 **分支跑完怎麼判斷成功**：看 `verdict`（`arcrun_get_execution_trace` 或",
    "`GET /workflows/<name>/executions`）。**走 true 路時 false 路的節點不出現＝正確行為**，",
    "不是失敗——別因為「只有一條路有輸出」就以為壞掉而改寫成 code（2026-08-01 實撞）。",
    "第一個節點固定是 `input`。",
  ].join("\n");

  // 地圖那段永遠有東西可印：拿到 → 印地圖；沒拿到 → 印「沒拿到」，不是消失。
  // stale 與「抓失敗」分開講，因為修法不同（前者要使用者重新連線，後者只是這次沒抓到）。
  const mapSection =
    mapInstructions ?? (identity.kind === "stale" ? MAP_STALE_NOTE : MAP_UNAVAILABLE_NOTE);

  // 知識段排在 Arcrun 指路段之前：AI 最常被問的是「X 是什麼」，那題的正解是查庫，不是查零件。
  return [KNOWLEDGE_FIRST, startHere, mapSection].join("\n\n---\n\n");
}

export async function handleMcpRequest(
  request: Request,
  env: Env,
  orgNamespace: string,
  partnerToken: string,
  identity: KnowledgeIdentity,
): Promise<Response> {
  const instructions = await buildServerInstructions(env, identity);

  const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined });
  const server = new McpServer(
    { name: "arcrun-mcp-server", version: "1.0.0" },
    { instructions },
  );

  registerAllTools(server, env, orgNamespace, partnerToken, identity);
  await server.connect(transport);

  return transport.handleRequest(request);
}
