// OAuth 短效認證儲存（KV）。儲存鐵律（leo + wiki）：
//   只有「取得的暫時性認證」可進 KV，且帶 TTL——authorization code、access token 是也。
//   長效機密（owner secret）走 CF Secrets，不進此處。DCR client 不落地（見 OAUTH.md 相容決策）。
// KV key 一律用 SHA-256 hex（不把 raw code/token 當 key）→ 就算 KV list 也拿不到可用憑證。
import { sha256Hex } from "./crypto.js";

/**
 * 登入者的身分（authorize 時用帳密換到，之後跟著 token 走）。
 *
 * leo 2026-08-12：「掛上 MCP 並輸入帳密，那個動作本身就是授權。」
 * ⇒ 驗完帳密**不能只留一個布林值**——身分要接住並攜帶，下游才不必再要一次認證。
 *
 * `session` 是 cypher `/portal/login` 發的 portal session token，與人類在 portal 網頁上
 * 拿到的完全同一種。它是「取得的暫時性認證」，正合本檔開頭的儲存鐵律（可進 KV、帶 TTL）；
 * access_token 的 TTL 會被夾到不超過它（見 routes.ts），兩者一起到期，不會出現
 * 「MCP 還連著、底下 session 早死」的鬼打牆。
 *
 * display_name / role / libraries 只是**給人看的回報值**（arcrun_whoami）。
 * 真正的權限判定每次都由 cypher 回讀 user record 現算——這裡的副本不是判準，
 * 所以管理員改權限或停用帳號會立刻生效，不必等 token 過期。
 */
export interface PortalIdentity {
  session: string;
  display_name: string;
  role: string;
  libraries: string[];
}

/** authorization code 綁定的資料（一次性；/token 驗證後即刪）。 */
export interface AuthCodeData {
  client_id: string;
  redirect_uri: string;
  code_challenge: string;
  code_challenge_method: string;
  scope: string;
  /** RFC 8707 resource：綁定 token 的目標 MCP server（audience）。 */
  resource: string;
  /** 換發後 token 綁定的資料分區（owner namespace）。 */
  namespace: string;
  /** 這張 code 是誰換的（帳密驗過的那個人）。 */
  portal: PortalIdentity;
  /** portal session 剩餘秒數（authorize 當下）；access_token TTL 不得超過它。 */
  portal_session_expires_in: number;
}

/** access token 綁定的資料。 */
export interface AccessTokenData {
  namespace: string;
  client_id: string;
  scope: string;
  /** token 的目標受眾（= 本 MCP server 的 canonical URI），驗證時比對。 */
  aud: string;
  /** 過期時間（epoch 秒），與 KV TTL 雙保險。 */
  exp: number;
  /**
   * 持這張 token 的是誰。**舊 token（本次改版前簽發的）沒有這欄** → undefined，
   * 知識面工具會誠實要求重新連線，而不是偷偷退回服務金鑰那條老路（fail-closed）。
   */
  portal?: PortalIdentity;
}

const CODE_PREFIX = "oauth:code:";
const TOKEN_PREFIX = "oauth:tok:";
/** authorization code 存活秒數（一次性、極短效）。 */
export const AUTH_CODE_TTL_SECONDS = 600;

/** 存 authorization code（TTL 極短）。回傳 raw code 給 client。 */
export async function putAuthCode(kv: KVNamespace, code: string, data: AuthCodeData): Promise<void> {
  const key = CODE_PREFIX + (await sha256Hex(code));
  await kv.put(key, JSON.stringify(data), { expirationTtl: AUTH_CODE_TTL_SECONDS });
}

/** 取出並「消費」authorization code（一次性：讀到即刪，防重放）。找不到回 null。 */
export async function consumeAuthCode(kv: KVNamespace, code: string): Promise<AuthCodeData | null> {
  const key = CODE_PREFIX + (await sha256Hex(code));
  const raw = await kv.get(key);
  if (!raw) return null;
  await kv.delete(key); // 一次性使用（OAuth 2.1：code 用過必失效）
  try {
    return JSON.parse(raw) as AuthCodeData;
  } catch {
    return null;
  }
}

/** 存 access token（TTL = ttlSeconds）。回傳 raw token 給 client。 */
export async function putAccessToken(
  kv: KVNamespace,
  token: string,
  data: AccessTokenData,
  ttlSeconds: number,
): Promise<void> {
  const key = TOKEN_PREFIX + (await sha256Hex(token));
  await kv.put(key, JSON.stringify(data), { expirationTtl: ttlSeconds });
}

/** 查 access token → 綁定資料。找不到 / 過期回 null（KV TTL 到期會自動消失，另做 exp 雙檢）。 */
export async function getAccessToken(kv: KVNamespace, token: string): Promise<AccessTokenData | null> {
  const key = TOKEN_PREFIX + (await sha256Hex(token));
  const raw = await kv.get(key);
  if (!raw) return null;
  try {
    const data = JSON.parse(raw) as AccessTokenData;
    if (typeof data.exp === "number" && data.exp * 1000 < Date.now()) return null;
    return data;
  } catch {
    return null;
  }
}
