/**
 * Portal 資料面 client — 「授權的 AI」用登入者的身分查東西的唯一管道。
 *
 * leo 2026-08-12：「人類進 Portal 輸入帳密表示你是主人，可以查到你權限所有東西；
 *   AI 透過輸入帳密的 MCP 查詢表示是授權的 AI，可以查到主人允許查的任何東西。」
 *   「掛上 MCP 並輸入帳密，那個動作本身就是授權」⇒ **下游不得再要求第二次認證**。
 *
 * 所以這裡帶的是 **portal session token**（同意頁輸入帳密時 cypher 發的那張，
 * 與人類在 portal 網頁上拿到的完全同一種），不是任何服務內部金鑰。
 * 端點是 cypher 的 `/portal/data/*`——庫過濾、租戶注入、停用即時生效全在那邊 server 側做完，
 * 本檔不做任何判斷（薄殼鐵律 rule 07：能力長在 API，介面只轉換）。
 *
 * 走既有 CYPHER_EXECUTOR service binding，不新增 binding、不新增金鑰。
 */

import type { Env } from "../types.js";
import type { PortalIdentity } from "../oauth/store.js";
import { errorResponse } from "./cypher-client.js";

export interface PortalCallOpts {
  method?: string;
  body?: unknown;
  query?: Record<string, string | number | undefined>;
}

/** 用登入者的 session 打 cypher 的 portal 資料面。 */
export async function portalFetch(
  env: Env,
  session: string,
  path: string,
  opts: PortalCallOpts = {},
): Promise<Response> {
  if (!env.CYPHER_EXECUTOR) {
    throw new Error("CYPHER_EXECUTOR service binding not configured");
  }
  const url = new URL(`https://cypher${path}`);
  for (const [k, v] of Object.entries(opts.query ?? {})) {
    if (v !== undefined && v !== "") url.searchParams.set(k, String(v));
  }
  return env.CYPHER_EXECUTOR.fetch(url.toString(), {
    method: opts.method ?? "GET",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${session}`,
    },
    body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
  });
}

/**
 * 知識面工具的身分解析結果。
 *
 * 三態刻意分開，因為「查不到」和「沒有」不可以長得一樣（leo 的老原則）：
 *   - portal   ：有登入者 → 走 portal 資料面（權限＝這個人的權限）
 *   - service  ：服務級憑據（static token / partner key）→ 維持既有 KBDB 直連（零回歸）
 *   - stale    ：OAuth token 但沒帶身分（本次改版前簽發的舊 token）→ **誠實要求重新連線**，
 *                不偷偷退回服務金鑰那條老路（那正是要修掉的「不管誰登入都看到同一格」）
 */
export type KnowledgeIdentity =
  | { kind: "portal"; portal: PortalIdentity }
  | { kind: "service" }
  | { kind: "stale" };

export function resolveKnowledgeIdentity(
  authPath: "oauth" | "service",
  portal: PortalIdentity | undefined,
): KnowledgeIdentity {
  if (authPath !== "oauth") return { kind: "service" };
  return portal?.session ? { kind: "portal", portal } : { kind: "stale" };
}

/** 舊 token（沒帶身分）時的統一回覆：講清楚怎麼修，不假裝查不到資料。 */
export function staleIdentityError() {
  return errorResponse(
    "identity_missing",
    "這條 MCP 連線是舊版簽發的 token，裡面沒有登入者身分，因此查不到任何知識內容。" +
      "重新連線一次（在 claude.ai 的 connector 設定裡重新授權、輸入你的 Portal 帳密）即可——" +
      "不需要另外找任何 credential 或金鑰。",
    [
      "到 claude.ai → Settings → Connectors，把這個 connector 重新連線一次（會跳出輸入 Portal 帳密的頁面）",
      "重連後 kbdb_* 全部工具都會用你這個帳號的權限查詢",
    ],
  );
}

/**
 * portal 資料面的錯誤 → 給 AI 看的訊息。
 * 401/403 特別處理：那代表**登入階段過期或帳號被停用**，不是「資料不存在」——
 * 兩者混在一起會讓 AI 對使用者說「你的知識庫是空的」，那是畫面在說謊。
 */
export async function portalError(res: Response, what: string) {
  const detail = await res.text().catch(() => "");
  if (res.status === 401) {
    return errorResponse(
      "session_expired",
      `${what}失敗：登入階段已過期（portal session 到期或已登出）。`,
      [
        "到 claude.ai → Settings → Connectors 重新連線這個 connector（重新輸入 Portal 帳密）",
        "重連後權限與你在 portal 網頁上看到的一致",
      ],
      detail,
    );
  }
  if (res.status === 403) {
    return errorResponse(
      "forbidden",
      `${what}失敗：這個帳號沒有這項權限（帳號可能已停用，或沒有被授權該知識庫）。`,
      ["請知識庫管理員在 portal 的帳號管理裡確認你的狀態與可用知識庫"],
      detail,
    );
  }
  return errorResponse(`portal_${res.status}`, `${what}失敗（HTTP ${res.status}）`, ["稍後重試"], detail);
}
