/**
 * arcrun console 登入（Arcrun#3 發現②，2026-07-03）
 *
 * 背景：console v0 那格叫「API Key」但 self-hosted 單租戶下其實只是 namespace 明碼字串，
 * 不是註冊制 key（leo 原話：理論上根本沒有 API Key 這件事）。leo 拍板：
 * 換成簡單 email+password 登入頁（自己設一組帳密即可，不用第三方 OAuth 或高級機制），
 * 登入成功後端發 session token 存 localStorage；**後端 API 呼叫仍是用固定租戶字串打
 * KBDB**（登入系統只是擋外人看到頁面，不是要做多租戶）。
 *
 * 與 routes/auth.ts 的差異：auth.ts 是官方 SaaS 的 Google/GitHub OAuth 多租戶註冊（每個
 * 使用者各自一把 ak_... api_key，各自一個租戶）。這裡是 self-hosted console 的「單一管理員
 * 帳密」— 全站只有一組帳密，只為擋外人看頁面，不產生新租戶、不核發 API key。
 *
 * 帳密怎麼設（不是雲端工人幫 leo 決定密碼）：首次造訪 /console 時若尚未設定過，前端會走
 * 「首次設定」流程（POST /console/setup）——leo 自己在瀏覽器輸入 email/password，一次性寫入
 * SESSIONS_KV `console:credentials`（已存在就 409，不能覆蓋，換帳密走 /console/setup/reset
 * 需帶舊密碼）。之後才是一般登入（POST /console/login）。
 *
 * 固定租戶字串：CONSOLE_TENANT（[vars]，非機密——self-hosted 架構本就是明碼 namespace）。
 * 預設 "leo"（Arcrun#3 發現①已核實：owner_id='leo' 是 D1 中 458,357 筆資料實際使用的租戶字串，
 * ak_... 只有 2 筆孤兒資料，故統一收斂到 'leo'，不製造第三個租戶）。
 */
import { Hono } from 'hono';
import type { Bindings } from '../types';
// D61 補充（2026-08-14，leo confirm「走C」，pending-changes.md「認證儲存要不要搬回 D1/KV」）：
// D61 把這組管理員帳密搬去認證儲存（CF Workers Secrets）是為了躲開「重裝時 binding 被安裝器
// 照名字重新指到新建的空資源」這個病根——但 Workers Secrets 的**寫入**需要外部
// `CF_SECRETS_API_TOKEN`，而這把 token 從安裝那天起就沒被種過，於是每一台全新實例永遠建不出
// 第一個帳號（arcrun-rag#99）。
// 病根本身已經在 2026-08-13 被更早、更通用的 `shared/resource-rule`（Arcrun#97）解掉——
// 現在每次安裝/更新都會沿用既有 binding，不會再把 SESSIONS_KV 重指到空資源。既然病根已解，
// 就不需要為了躲 binding 而去揹「需要外部 token」這筆新債：**帳密改回住 SESSIONS_KV**
// （`console:credentials`，binding，永不需要外部 CF token）。
// 認證儲存（Workers Secrets）留著當「已經在跑 D61 的舊實例」的**讀路徑**——讀取零成本、
// 零外部憑證需求（只有寫入才要 token）——查到就順手搬回 SESSIONS_KV（見 loadCredentials）。
import { readAuthStore } from '../lib/portal-auth-store';
// Arcrun#108：租戶字串唯一產地。
import { knowledgeOwner } from '../lib/tenant';

export const consoleAuthRouter = new Hono<{ Bindings: Bindings }>();

const CREDS_KEY = 'console:credentials';
const SESSION_PREFIX = 'console_sess:';
const SESSION_TTL_SECONDS = 30 * 24 * 60 * 60; // 30 天

/**
 * 驗證 console session（給其他 route 共用，如 /console/triage-data）。
 * authHeader 形如 "Bearer <token>"；有效回 true。
 */
export async function validateConsoleSession(env: Bindings, authHeader: string | undefined): Promise<boolean> {
  const token = (authHeader ?? '').match(/^Bearer\s+(\S+)/i)?.[1];
  if (!token) return false;
  const sess = await env.SESSIONS_KV.get(`${SESSION_PREFIX}${token}`);
  return !!sess;
}

interface StoredCredentials {
  email: string;
  salt: string;      // hex
  hash: string;       // hex，sha256(salt + password) 迭代 3 次
  created_at: string;
}

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('');
}

async function sha256Hex(input: string): Promise<string> {
  const data = new TextEncoder().encode(input);
  const digest = await crypto.subtle.digest('SHA-256', data);
  return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, '0')).join('');
}

/** 簡易迭代雜湊（非 bcrypt/argon2，但比單輪 SHA-256 好一點；self-hosted 單管理員帳密，威脅模型輕）。 */
async function hashPassword(password: string, salt: string): Promise<string> {
  let h = `${salt}:${password}`;
  for (let i = 0; i < 3; i++) h = await sha256Hex(h);
  return h;
}

/**
 * console 登入後下發給前端當 api_key 用的租戶字串（舊 console 的設計，與 portal 不同：
 * portal 絕不下發，console 會）。
 *
 * Arcrun#108：這是**知識資料面**的 owner_id（前端拿它直打 `/kbdb/*`），所以必須與寫入端
 * （CLI／小幫手／MCP 用的實例 namespace）同源。以前直接讀 `env.CONSOLE_TENANT || 'leo'`
 * ⇒ 與 portal 同一個病：資料在 `bfezv28v`、過濾拿 `leo`，console 首頁的藏書地圖同樣是空的。
 * 現在走唯一產地 `lib/tenant.ts`。
 */
function tenantOf(c: { env: Bindings }): string {
  return knowledgeOwner(c.env);
}

// ── 帳密的家（2026-08-14 起：SESSIONS_KV 為主，認證儲存為舊實例回退讀路徑）────────────

/**
 * 讀出 console 管理員帳密。**SESSIONS_KV（binding）優先**；沒有才回退讀舊家
 * （D61 的認證儲存，CF Workers Secrets——讀取零成本、零外部憑證需求），
 * 且一旦從舊家讀到就順手搬回 SESSIONS_KV（best-effort，搬不動不影響本次登入）。
 */
async function loadCredentials(env: Bindings): Promise<{ creds: StoredCredentials | null; source: 'kv' | 'legacy-secrets' | 'none' }> {
  const raw = await env.SESSIONS_KV.get(CREDS_KEY);
  if (raw) {
    try {
      return { creds: JSON.parse(raw) as StoredCredentials, source: 'kv' };
    } catch {
      /* KV 這份壞了，當作沒有，往下查舊家 */
    }
  }

  // 舊家（D61 的認證儲存）：純讀 env 字串，零網路呼叫、不需要任何外部 CF 憑證。
  const legacy = readAuthStore(env).console;
  if (!legacy) return { creds: null, source: 'none' };
  try {
    // best-effort 搬回 SESSIONS_KV——這是 binding put，本來就不需要外部 token，
    // 幾乎不會失敗；失敗也不影響本次用這份舊資料繼續（狀態看 /console/auth-status）。
    await env.SESSIONS_KV.put(CREDS_KEY, JSON.stringify(legacy));
  } catch {
    /* 照舊用這份，下次再試著搬一次 */
  }
  return { creds: legacy as StoredCredentials, source: 'legacy-secrets' };
}

/**
 * 寫入 console 管理員帳密——**只寫 SESSIONS_KV**（binding，永不需要外部 CF token）。
 * 不再寫回認證儲存（Workers Secrets）：那是要被淘汰的舊家，寫回去等於把債種回土裡。
 */
async function saveCredentials(env: Bindings, record: StoredCredentials): Promise<void> {
  await env.SESSIONS_KV.put(CREDS_KEY, JSON.stringify(record));
}

/** `/console/auth-status`、`/health` 共用的儲存狀態區塊（不洩漏 email/雜湊，只回統計）。 */
function consoleAuthStoreStatus(env: Bindings): { home: 'sessions-kv'; writable: true; legacy_secrets_present: boolean } {
  return {
    home: 'sessions-kv',
    writable: true, // binding-based，只要 wrangler.toml 有這個 binding 就一定寫得進去
    legacy_secrets_present: readAuthStore(env).console !== null,
  };
}

// GET /console/auth-status — 前端用來決定顯示「首次設定」還是「登入」表單。不洩漏 email。
consoleAuthRouter.get('/console/auth-status', async (c) => {
  const { creds, source } = await loadCredentials(c.env);
  // 多回一個 auth_store 區塊——「認證住在哪、寫不寫得進去」要在實例自己這一側看得出來，
  // 不是等用戶登不進去才發現（#10「寧可明顯失敗，不要靜默錯置」）。
  return c.json({ configured: !!creds, credentials_source: source, auth_store: consoleAuthStoreStatus(c.env) });
});

// POST /console/setup — 首次設定帳密（body: {email, password}）。已設定過 → 409（不可覆蓋，防外人搶注）。
consoleAuthRouter.post('/console/setup', async (c) => {
  const { creds: existing } = await loadCredentials(c.env);
  if (existing) {
    // D61 明顯失敗：舊版只說「已設定過」，**沒說剛才填的那組密碼被整個丟掉了**——
    // 用戶（含安裝精靈裡的 leo）以為自己剛設好了新密碼，其實從頭到尾沒有被採用過。
    return c.json(
      {
        error:
          '這台實例已經有管理員帳密了，**你剛才輸入的密碼沒有被採用**，目前的密碼仍是當初設定的那一組。' +
          '要用舊密碼登入，或用 /console/setup/reset（需要舊密碼）換一組。',
        code: 'already_configured',
        password_applied: false,
        reset_path: '/console/setup/reset',
      },
      409,
    );
  }

  const body = await c.req.json().catch(() => null);
  const email = (body?.email ?? '').trim();
  const password = body?.password ?? '';
  if (!email || !password) return c.json({ error: 'email 與 password 必填' }, 400);
  if (password.length < 8) return c.json({ error: '密碼至少 8 碼' }, 400);

  const salt = randomHex(16);
  const hash = await hashPassword(password, salt);
  const record: StoredCredentials = { email: email.toLowerCase(), salt, hash, created_at: new Date().toISOString() };
  try {
    // 2026-08-14 起：寫 SESSIONS_KV（binding），不再需要安裝精靈遞任何臨時 CF token
    // （arcrun-rag#99 那個結構性缺口——見檔頭說明——已經隨儲存層搬回 binding 一併解掉）。
    await saveCredentials(c.env, record);
  } catch (e) {
    // 寫不進去就誠實回報（不假綠：binding put 幾乎不會失敗，於是沒人處理過這條路）
    return c.json({ error: `帳密沒有存起來：${e instanceof Error ? e.message : String(e)}`, code: 'auth_store_not_writable' }, 502);
  }

  const token = randomHex(32);
  await c.env.SESSIONS_KV.put(`${SESSION_PREFIX}${token}`, JSON.stringify({ created_at: Date.now() }), {
    expirationTtl: SESSION_TTL_SECONDS,
  });
  return c.json({ success: true, session_token: token, tenant: tenantOf(c) });
});

// POST /console/setup/reset — 換帳密（body: {current_password, email, password}）。需驗舊密碼，防外人重設。
consoleAuthRouter.post('/console/setup/reset', async (c) => {
  const { creds: existing } = await loadCredentials(c.env);
  if (!existing) return c.json({ error: '尚未設定過，請用 /console/setup' }, 400);

  const body = await c.req.json().catch(() => null);
  const currentPassword = body?.current_password ?? '';
  const email = (body?.email ?? '').trim();
  const password = body?.password ?? '';
  if (!currentPassword || !email || !password) return c.json({ error: 'current_password、email、password 必填' }, 400);
  if (password.length < 8) return c.json({ error: '新密碼至少 8 碼' }, 400);

  const currentHash = await hashPassword(currentPassword, existing.salt);
  if (currentHash !== existing.hash) return c.json({ error: '舊密碼不正確' }, 401);

  const salt = randomHex(16);
  const hash = await hashPassword(password, salt);
  const record: StoredCredentials = { email: email.toLowerCase(), salt, hash, created_at: existing.created_at };
  try {
    await saveCredentials(c.env, record);
  } catch (e) {
    return c.json({ error: `新帳密沒有存起來：${e instanceof Error ? e.message : String(e)}`, code: 'auth_store_not_writable' }, 502);
  }
  return c.json({ success: true });
});

// POST /console/login — body: {email, password}。成功 → session token（localStorage 存這個，不存密碼）。
consoleAuthRouter.post('/console/login', async (c) => {
  const { creds: existing } = await loadCredentials(c.env);
  if (!existing) {
    // 明顯失敗（#10「寧可明顯失敗，不要靜默錯置」）：這是「這台實例讀不到認證資料」，
    // 不是「你帳密打錯」——兩句話混成一句正是 2026-08-09 leo 被誤鎖 15 分鐘的根因。
    return c.json(
      {
        error: '這台實例還沒有管理員帳密（或讀不到）——不是密碼錯。請先完成首次設定。',
        code: 'auth_store_empty',
        auth_store: consoleAuthStoreStatus(c.env),
      },
      400,
    );
  }

  const body = await c.req.json().catch(() => null);
  const email = (body?.email ?? '').trim().toLowerCase();
  const password = body?.password ?? '';
  if (!email || !password) return c.json({ error: 'email 與 password 必填' }, 400);

  const hash = await hashPassword(password, existing.salt);
  if (email !== existing.email || hash !== existing.hash) {
    return c.json({ error: 'email 或密碼錯誤' }, 401);
  }

  const token = randomHex(32);
  await c.env.SESSIONS_KV.put(`${SESSION_PREFIX}${token}`, JSON.stringify({ created_at: Date.now() }), {
    expirationTtl: SESSION_TTL_SECONDS,
  });
  return c.json({ success: true, session_token: token, tenant: tenantOf(c) });
});

// GET /console/session — Authorization: Bearer <session_token>。前端載入頁面時用來確認 session 還有效
// + 拿回固定租戶字串（不必再手貼 API Key）。
consoleAuthRouter.get('/console/session', async (c) => {
  const auth = c.req.header('authorization') ?? '';
  const token = auth.match(/^Bearer\s+(\S+)/i)?.[1];
  if (!token) return c.json({ valid: false }, 401);
  const sess = await c.env.SESSIONS_KV.get(`${SESSION_PREFIX}${token}`);
  if (!sess) return c.json({ valid: false }, 401);
  return c.json({ valid: true, tenant: tenantOf(c) });
});

// POST /console/logout — Authorization: Bearer <session_token>。
consoleAuthRouter.post('/console/logout', async (c) => {
  const auth = c.req.header('authorization') ?? '';
  const token = auth.match(/^Bearer\s+(\S+)/i)?.[1];
  if (token) await c.env.SESSIONS_KV.delete(`${SESSION_PREFIX}${token}`);
  return c.json({ success: true });
});
