// 批准页 step-up 再认证的会话级暂存。step_up 开启时，批准方在批准新设备前必须做
// 一次新鲜的 prompt=login 再登录（provider 若开了 2FA 即在此处过），把这次拿到的
// 授权码交给后端就地核验。这段往返跨一次整页跳转（→ provider → /oauth/callback →
// 批准页），故用 sessionStorage 串起三个状态：
//
//   1. 跳转去 provider 前：记下「待办」标记 + 这次的 code_verifier + 要回到的批准页
//      URL（PENDING）。
//   2. /oauth/callback 落地（检测到 PENDING）：不调 /api/auth/exchange（那会在服务端
//      消费掉 code 并轮换会话），转而把本次回调的 {code, verifier} 暂存进 STASH，
//      清掉 PENDING，整页跳回批准页 URL。
//   3. 批准页回到后：取出 STASH 的 {code, verifier} → 调 qrApprove 就地核验 → 用完即清。
//
// sessionStorage（非 localStorage）：仅本次再认证往返需要，关标签即清、不跨设备；
// code 是一次性的且与本批准会话强绑定，落 localStorage 反而徒增泄漏面。

const PENDING_KEY = "cdrop.qr.stepup_pending";   // { verifier, returnTo }
const STASH_KEY   = "cdrop.qr.stepup_stash";     // { code, verifier }
// 登录会话登出的 step-up：跨整页跳转记住「要登出的是哪条会话」。回到设置页后取出，
// 重试那条 DELETE。批准页的 step-up 不用它（要批准的 request 已编在 returnTo 里）。
const REVOKE_KEY  = "cdrop.qr.stepup_revoke";     // sessionId（string）

// 仅允许两个站内回流目标，杜绝 open-redirect（外部 URL / 协议相对地址 / 任意路径）：
//   - /link[?…]  ——批准页（扫码登录的 step-up）；
//   - /settings  ——设置页（登录会话登出的 step-up）。
// step-up 往返回来后的整页跳转目标只能是这两处之一；其余一律拒。校验 path 必须以
// "/link" 或 "/settings" 起头且后随串只能是空或 "?…"，挡住 "/linkmalicious" /
// "//evil.com"（协议相对）/ "/settings/../x" 之类绕过。
function isSafeReturnPath(path: string): boolean
{
    for (const base of [ "/link", "/settings" ])
    {
        if (path === base) { return true; }
        if (path.startsWith(base + "?")) { return true; }
    }
    return false;
}

export interface StepUpPending
{
    verifier: string;
    returnTo: string;
}

export interface StepUpStash
{
    code: string;
    verifier: string;
}

// stashStepUpPending 在跳转去 provider 前记下「待办」。returnTo 非法（防开放重定向）
// 即不写——调用方据返回值判定是否安全发起跳转。
export function stashStepUpPending(pending: StepUpPending): boolean
{
    if (typeof window === "undefined") { return false; }
    if (!pending.verifier || !isSafeReturnPath(pending.returnTo)) { return false; }
    window.sessionStorage.setItem(PENDING_KEY, JSON.stringify(pending));
    return true;
}

// peekStepUpPending 只读地探测是否在 step-up 往返中（/oauth/callback 据此分叉，
// 决定不走 /api/auth/exchange）。不消费。
export function peekStepUpPending(): StepUpPending | null
{
    if (typeof window === "undefined") { return null; }
    const raw = window.sessionStorage.getItem(PENDING_KEY);
    if (!raw) { return null; }
    const p = safeParse<StepUpPending>(raw);
    if (!p?.verifier || !isSafeReturnPath(p.returnTo)) { return null; }
    return p;
}

export function clearStepUpPending(): void
{
    if (typeof window === "undefined") { return; }
    window.sessionStorage.removeItem(PENDING_KEY);
}

// stashStepUpCode 把回调拿到的一次性 code + 配套 verifier 暂存，留给批准页交给
// 后端 approve 核验。前端绝不自己 POST /exchange 消费它。
export function stashStepUpCode(stash: StepUpStash): void
{
    if (typeof window === "undefined") { return; }
    if (!stash.code || !stash.verifier) { return; }
    window.sessionStorage.setItem(STASH_KEY, JSON.stringify(stash));
}

// consumeStepUpCode 取出并清除暂存的 {code, verifier}（一次性，用完即清，避免泄漏
// 与复用）。无 / 非法返回 null。
export function consumeStepUpCode(): StepUpStash | null
{
    if (typeof window === "undefined") { return null; }
    const raw = window.sessionStorage.getItem(STASH_KEY);
    window.sessionStorage.removeItem(STASH_KEY);
    if (!raw) { return null; }
    const s = safeParse<StepUpStash>(raw);
    if (!s?.code || !s.verifier) { return null; }
    return s;
}

export function clearStepUpStash(): void
{
    if (typeof window === "undefined") { return; }
    window.sessionStorage.removeItem(STASH_KEY);
}

// stashPendingRevoke 在发起会话登出的 step-up 再认证前记下待登出的 sessionId，
// 跨整页跳转留到回到设置页后取出重试。
export function stashPendingRevoke(sessionId: string): void
{
    if (typeof window === "undefined") { return; }
    if (!sessionId) { return; }
    window.sessionStorage.setItem(REVOKE_KEY, sessionId);
}

// consumePendingRevoke 取出并清除待登出的 sessionId（一次性，用完即清）。
export function consumePendingRevoke(): string | null
{
    if (typeof window === "undefined") { return null; }
    const id = window.sessionStorage.getItem(REVOKE_KEY);
    window.sessionStorage.removeItem(REVOKE_KEY);
    return id || null;
}

function safeParse<T>(raw: string): T | null
{
    try { return JSON.parse(raw) as T; }
    catch { return null; }
}
