import { Context, Next } from "hono";
import { Env } from "../types.js";
import { getAccessToken, type PortalIdentity } from "../oauth/store.js";
import { constantTimeEqual } from "../oauth/crypto.js";
import { originOf, resourceUri, wwwAuthenticateHeader } from "../oauth/metadata.js";

/**
 * 這條連線是**用誰的身分**進來的。決定知識面（kbdb_*）走哪條路：
 *   - "oauth"：有人在同意頁輸入過 Portal 帳密 → 帶著他的 portal session 走 portal 資料面，
 *              權限＝他在 portal 網頁上看得到的那些（庫過濾照吃）。
 *   - "service"：static token / partner key 這類**服務級**憑據（本身就是真祕密，
 *              代表整個實例或整個租戶，不是某個人）→ 維持既有的 KBDB 直連行為，零回歸。
 * 兩條路刻意分開命名，因為「這張 token 背後有沒有一個人」正是本次要能分辨的事。
 */
export type AuthPath = "oauth" | "service";

/**
 * MCP / GUI 端點認證中介層。
 *
 * 安全定調（leo 鐵律）：**只知道「網址 + 明碼 namespace」的人，必須讀不到任何資料。**
 * 認證順序（先到先得）：
 *   1. OAuth 2.1 access_token（OAUTH_KV 查得到）→ 解出綁定 namespace。遠端 claude.ai 走這條。
 *   2. MCP_STATIC_TOKEN（CF Secret，真祕密）→ 解成 MCP_OWNER_NAMESPACE。本機 CLI/GUI/Claude Code 相容用。
 *   3. 官方 SaaS（MULTI_TENANT 未設 / "true"）→ KBDB partner-key 驗證（行為不變）。
 *   4. 【預設關】ALLOW_PLAINTEXT_NAMESPACE="true" → 恢復舊「Bearer 明碼即 namespace」（＝已修掉的漏洞，僅遷移期）。
 *   5. 皆不符 → 401 + WWW-Authenticate（RFC 9728），讓 claude.ai 發現 OAuth。
 *
 * ⚠️ 舊行為（MULTI_TENANT=false 時把 Bearer 明碼直接當 org_namespace）是本次要修掉的漏洞，
 *    已從預設路徑移除；只在明確設 ALLOW_PLAINTEXT_NAMESPACE="true" 的遷移情境才恢復。
 */
export async function partnerAuthMiddleware(
  c: Context<{
    Bindings: Env;
    Variables: {
      org_namespace: string;
      partner_token: string;
      portal?: PortalIdentity;
      auth_path: AuthPath;
    };
  }>,
  next: Next
) {
  const origin = originOf(c.req.url);
  const unauthorized = (desc: string, error?: string) =>
    c.json({ error: "unauthorized", error_description: desc }, 401, {
      "WWW-Authenticate": wwwAuthenticateHeader(origin, error),
    });

  const authHeader = c.req.header("Authorization");
  if (!authHeader?.startsWith("Bearer ")) {
    return unauthorized("Missing or invalid Authorization header");
  }
  const token = authHeader.slice(7);
  if (!token) {
    return unauthorized("Empty bearer token");
  }

  // 1) OAuth 2.1 access token（遠端 connector 的正規路徑）。
  if (c.env.OAUTH_KV) {
    const at = await getAccessToken(c.env.OAUTH_KV, token);
    if (at) {
      // RFC 8707 audience 驗證：token 只能用在它被簽發的目標 MCP server。
      // token 的 aud 是簽發時綁定的 resource；須等於「本次請求 origin 算出的 canonical resource URI」，
      // 否則拒絕（防別的 arcrun-mcp 部署簽的 token 拿來這裡用 = token passthrough）。
      const expectedAud = resourceUri(origin);
      if (at.aud !== expectedAud) {
        return unauthorized("Access token audience mismatch", "invalid_token");
      }
      c.set("org_namespace", at.namespace);
      c.set("partner_token", at.namespace); // 下游 cypher 用 namespace 當 X-Arcrun-API-Key（與 CLI 同一份身份）
      // 登入者的身分（2026-08-12）：知識面工具（kbdb_*）帶著它打 cypher 的 portal 資料面，
      // 權限與這個人在 portal 網頁上看到的完全一致。舊 token 沒有這欄 → undefined，
      // 知識面工具會要求重新連線（不偷偷退回服務金鑰那條老路）。
      c.set("portal", at.portal);
      c.set("auth_path", "oauth");
      await next();
      return;
    }
  }

  // 2) 本機相容：真祕密 static token（CF Secret）→ owner namespace。取代舊明碼路徑。
  if (c.env.MCP_STATIC_TOKEN && constantTimeEqual(token, c.env.MCP_STATIC_TOKEN)) {
    const ns = c.env.MCP_OWNER_NAMESPACE || "leo";
    c.set("org_namespace", ns);
    c.set("partner_token", ns);
    c.set("auth_path", "service");
    await next();
    return;
  }

  // 3) 官方 SaaS：KBDB partner-key 驗證（行為完全不變）。
  if (c.env.MULTI_TENANT !== "false") {
    const resp = await c.env.KBDB.fetch(
      `http://kbdb/partners/${encodeURIComponent(token)}/info`,
      { headers: { Authorization: `Bearer ${c.env.KBDB_INTERNAL_TOKEN}` } }
    );
    if (!resp.ok) {
      return unauthorized("Invalid or expired partner key", "invalid_token");
    }
    const info = await resp.json<{ valid: boolean; org_namespace: string }>();
    if (!info.valid) {
      return unauthorized("Invalid or expired partner key", "invalid_token");
    }
    c.set("org_namespace", info.org_namespace);
    c.set("partner_token", token);
    c.set("auth_path", "service");
    await next();
    return;
  }

  // 4)【預設關，不安全，SUNSET】遷移逃生門：恢復舊明碼 namespace 行為。
  //    ⚠️ 這是暫時相容路徑，遷移驗收完即刪整段（含 Env.ALLOW_PLAINTEXT_NAMESPACE 欄位）。
  //    追蹤 issue：見 OAUTH.md §5 / wrangler.toml 註解。留著只為遷移期，別當長期選項。
  if (c.env.ALLOW_PLAINTEXT_NAMESPACE === "true") {
    c.set("org_namespace", token);
    c.set("partner_token", token);
    c.set("auth_path", "service");
    await next();
    return;
  }

  // 5) self-hosted 但沒帶 OAuth token / static token → 拒絕（明碼 namespace 不再放行）。
  return unauthorized("Bearer token not recognized; complete OAuth to obtain an access token", "invalid_token");
}
