/**
 * Portal 密碼 KDF 模組（portal-auth design §4.1 / D-5、D-6，Gitea #24/#25 P2）
 *
 * 職責界線（rule 2.1/2.2 對照，design D-5 已釐清）：
 *   這是「UI session 登入」的密碼雜湊——console-auth.ts 同類先例，**不是** workflow
 *   credential 原語（那些屬 WASM auth primitive，本檔不碰 crypto.subtle.decrypt /
 *   RSASSA / template 展開）。只用 WebCrypto 原生 PBKDF2（crypto.subtle.deriveBits）。
 *
 * 規格：
 *   - PBKDF2-SHA256、100,000 iterations（CF Workers runtime 上限，見下）、salt 16 bytes、輸出 256-bit
 *   - 儲存格式 `pbkdf2-sha256$<iterations>$<salt_b64>$<hash_b64>`（自帶演算法前綴，
 *     未來換 KDF 可共存漸進遷移——verify 按前綴解析，不寫死參數）
 *   - 驗證用常數時間比對（同 mcp/src/oauth/crypto.ts PR#15 慣例）
 *   - 密碼永不明碼儲存、永不進 log（本模組不 console.log 任何輸入）
 */

export const PBKDF2_ALGO_PREFIX = 'pbkdf2-sha256';
/**
 * CF Workers **正式 runtime** 的 PBKDF2 上限是 100,000 iterations——超過時
 * `crypto.subtle.deriveBits` 直接拒絕（2026-07-14 uncle6 真雲實撞：/portal/admin/bootstrap 500；
 * miniflare 無此限制 → 本機測試全綠的假象）。OWASP 建議 600k 但平台封頂只能 100k。
 * 儲存格式自帶迭代數（verify 從儲存值解析）→ 未來平台放寬可無痛升，舊 hash 照驗。
 */
export const PBKDF2_ITERATIONS = 100_000;

function b64encode(bytes: Uint8Array): string {
  let bin = '';
  for (const b of bytes) bin += String.fromCharCode(b);
  return btoa(bin);
}

function b64decode(s: string): Uint8Array | null {
  try {
    const bin = atob(s);
    const out = new Uint8Array(bin.length);
    for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
    return out;
  } catch {
    return null;
  }
}

async function deriveBits(password: string, salt: Uint8Array, iterations: number): Promise<Uint8Array> {
  const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(password), 'PBKDF2', false, [
    'deriveBits',
  ]);
  const bits = await crypto.subtle.deriveBits(
    { name: 'PBKDF2', hash: 'SHA-256', salt: salt as BufferSource, iterations },
    key,
    256,
  );
  return new Uint8Array(bits);
}

/**
 * 常數時間字串比對（防 timing attack）。與 mcp/src/oauth/crypto.ts 同實作
 * （cypher-executor 與 mcp 是不同 package，無共用 lib 路徑，故各持一份同款）。
 */
export function constantTimeEqual(a: string, b: string): boolean {
  const ab = new TextEncoder().encode(a);
  const bb = new TextEncoder().encode(b);
  let diff = ab.length ^ bb.length;
  const len = Math.max(ab.length, bb.length);
  for (let i = 0; i < len; i++) {
    diff |= (ab[i] ?? 0) ^ (bb[i] ?? 0);
  }
  return diff === 0;
}

/** 雜湊一組密碼 → `pbkdf2-sha256$100000$<salt_b64>$<hash_b64>`。 */
export async function hashPassword(password: string, iterations: number = PBKDF2_ITERATIONS): Promise<string> {
  const salt = crypto.getRandomValues(new Uint8Array(16));
  const hash = await deriveBits(password, salt, iterations);
  return `${PBKDF2_ALGO_PREFIX}$${iterations}$${b64encode(salt)}$${b64encode(hash)}`;
}

/**
 * 驗證密碼 vs 儲存格式。格式壞掉 / 前綴不認得 → false（誠實拒絕，不拋錯洩漏細節）。
 * iterations 從儲存值解析（漸進遷移：舊 hash 用舊參數驗，新寫入用現行常數）。
 */
export async function verifyPassword(password: string, stored: string): Promise<boolean> {
  const parts = (stored ?? '').split('$');
  if (parts.length !== 4 || parts[0] !== PBKDF2_ALGO_PREFIX) return false;
  const iterations = Number.parseInt(parts[1], 10);
  if (!Number.isFinite(iterations) || iterations < 1 || iterations > 10_000_000) return false;
  const salt = b64decode(parts[2]);
  if (!salt || salt.length === 0) return false;
  const derived = await deriveBits(password, salt, iterations);
  return constantTimeEqual(b64encode(derived), parts[3]);
}

/** 密碼學等級隨機 hex token（session token 用；與 console-auth randomHex 同款）。 */
export function randomHex(bytes: number): string {
  const arr = new Uint8Array(bytes);
  crypto.getRandomValues(arr);
  return Array.from(arr)
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('');
}

/**
 * SHA-256 → hex。**用途只有一個**：把「修改密碼連結」的 token 換成查詢用的 KV key（D62）。
 *
 * 為什麼不直接拿 token 當 key：連結裡的 token 是**能改密碼的憑據**，直接當 key 等於
 * 把它明碼存在 KV 裡；改存雜湊後，看得到 KV 的人也拿不到可用的連結。
 * （這裡只做 digest——不是 rule 2.2 禁的 `crypto.subtle.decrypt` / RSASSA 簽章。）
 */
export async function sha256Hex(input: string): Promise<string> {
  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input));
  return Array.from(new Uint8Array(digest))
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('');
}

/**
 * 產生一次性隨機密碼（admin reset-password / 建帳號未給密碼時用）。
 * 16 字元、大小寫+數字（去掉易混淆字元），熵約 93 bits。
 */
export function generatePassword(length = 16): string {
  const charset = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789';
  const arr = new Uint8Array(length);
  crypto.getRandomValues(arr);
  let out = '';
  for (const b of arr) out += charset[b % charset.length];
  return out;
}
