import { useAppStore, type User } from "../../store";
import { t } from "../../i18n";
import { apiFetch } from "../../net/api";
import { isDesktop, desktopRefresh, clearDesktopSession } from "../../net/desktop";
import { fetchMeScope } from "../../net/sessions";
import { stashStepUpPending } from "../qr/stepUp";
import { toast } from "../../ui/feedback";

// ---- dev mode -------------------------------------------------------------

// Dev-mode "login": read ?dev_user=alice (or fall back to localStorage),
// stamp the store with a synthetic User and the build-time dev token.
// This bypasses Casdoor entirely. The middleware on the backend accepts
// any X-Dev-User as long as the bearer matches CDROP_DEV_TOKEN.
export function loginDev(searchParams: URLSearchParams): User
{
    const devUser = searchParams.get("dev_user")
        ?? window.localStorage.getItem("cdrop.dev_user")
        ?? "dev-user";
    window.localStorage.setItem("cdrop.dev_user", devUser);

    const token = import.meta.env.VITE_CDROP_DEV_TOKEN;
    if (!token)
    {
        throw new Error(t("errors.devTokenMissing"));
    }

    const user: User = { id: devUser, name: devUser };
    useAppStore.getState().setAuth({ accessToken: token, user });
    return user;
}

export function logout()
{
    // Fire-and-forget 通知后端立刻把本设备从 Hub 踢掉 + 广播 presence，让对端
    // 不必等 30s 宽限期。apiFetch 同步抓取 access_token 后才让出（fetch 已发出），
    // 即使紧随其后 clearAuth 也不会污染这次请求的 Authorization 头。失败忽略
    // ——只是体验降级回宽限期路径，不影响登出本身。
    void apiFetch("/api/me/disconnect", { method: "POST" }).catch(() => { /* ignore */ });
    // 浏览器端：删除服务端 session 并清除 HttpOnly cookie（cookie 自动随同源请求），
    // 否则下次开机 bootstrapAuth 仍会静默免重登回来。桌面端无 cookie session，跳过。
    if (!isDesktop() && useAppStore.getState().authMode === "prod")
    {
        void fetch("/api/auth/logout", { method: "POST" }).catch(() => { /* ignore */ });
    }
    useAppStore.getState().clearAuth();
    // 桌面端：删除 Go 侧持久化 session 文件，否则下次启动仍会注入已登录态。
    if (isDesktop()) { clearDesktopSession(); }
}

// forceLogout 在「服务端确证会话已失效」时清掉本地登录态——触发点仅一处：
// access token 401 后续期也被服务端以 401 拒绝（refreshTokens 返回 "invalid"，
// 即被远程吊销或已过期）。短期连接失败（网络抖动 / 5xx）绝不走到这里（见
// net/api.ts）。与 logout() 不同，它不发任何服务端请求（会话已死，调用只会再次
// 401），只就地清空 + 提示，让 __root 的反应式守卫把界面送回登录页
// （离线设备「下一次使用即得知」）。
//
// 幂等：靠「已登出即跳过」实现——首个 401 同步清空 user/accessToken 后，并发涌入
// 的其余 401 看到已为空便直接返回，不会重复弹 toast、不重复清桌面 session。
export function forceLogout(): void
{
    const st = useAppStore.getState();
    if (!st.user && !st.accessToken)
    {
        // 已被并发的某个 401 清空，或本就处于登出态：不重复处理（避免 toast 刷屏）。
        return;
    }
    st.clearAuth();
    // 桌面端：删除 Go 侧持久化 session，否则下次启动仍注入这枚已失效的登录态。
    if (isDesktop()) { clearDesktopSession(); }
    toast.error(t("auth.sessionLost.title"), t("auth.sessionLost.body"));
}

// ---- prod (OIDC PKCE) -----------------------------------------------------

interface AuthConfig
{
    auth_mode: "dev" | "prod";
    authorize_url: string;
    client_id: string;
    redirect_uri: string;
    scopes: string;
}

let cachedConfig: AuthConfig | null = null;

export async function fetchAuthConfig(): Promise<AuthConfig>
{
    if (cachedConfig) { return cachedConfig; }
    const r = await fetch("/api/auth/config");
    if (!r.ok) { throw new Error(`auth config ${r.status}`); }
    cachedConfig = await r.json() as AuthConfig;
    return cachedConfig;
}

const PKCE_VERIFIER_KEY = "cdrop.pkce_verifier";
const OAUTH_STATE_KEY   = "cdrop.oauth_state";

// step-up 再认证用一对独立的 state / verifier 句柄，绝不复用上面那对常规登录键——
// 否则 /oauth/callback 在 step-up 往返里会被当成普通登录、误调 /api/auth/exchange。
// state 走 sessionStorage（仅本次往返、关标签即清，与 step-up 暂存物同生命周期）；
// verifier 不单独落键，而是塞进 stepUp.ts 的 PENDING（{verifier, returnTo}）。
const STEPUP_STATE_KEY = "cdrop.qr.stepup_state";

// loginProd kicks off OIDC PKCE: generate verifier+challenge+state, stash them
// in localStorage, navigate the browser to the provider's /authorize endpoint.
// The callback page completes the flow.
//
// localStorage (not sessionStorage): a standalone PWA on iOS may run the
// provider round-trip in a separate context and relaunch the PWA on the redirect
// back, which wipes sessionStorage and breaks login. localStorage survives that.
// The verifier is consumed + removed immediately on callback and is useless
// without the matching auth code, so persisting it for the round-trip is fine.
export async function loginProd(): Promise<void>
{
    const cfg = await fetchAuthConfig();
    if (cfg.auth_mode !== "prod")
    {
        throw new Error(`server reports auth_mode=${cfg.auth_mode}, expected prod`);
    }
    if (!cfg.authorize_url || !cfg.client_id || !cfg.redirect_uri)
    {
        throw new Error("OIDC config incomplete (authorize_url / client_id / redirect_uri)");
    }

    const verifier = generateRandomBase64url(32);
    const challenge = await sha256Base64url(verifier);
    const state = generateRandomBase64url(16);

    localStorage.setItem(PKCE_VERIFIER_KEY, verifier);
    localStorage.setItem(OAUTH_STATE_KEY, state);

    const params = new URLSearchParams({
        client_id: cfg.client_id,
        redirect_uri: cfg.redirect_uri,
        response_type: "code",
        scope: cfg.scopes || "openid profile email",
        state,
        code_challenge: challenge,
        code_challenge_method: "S256",
    });
    window.location.href = `${cfg.authorize_url}?${params}`;
}

// startStepUpReauth 发起批准页 step-up 再认证：一次新鲜 PKCE 往返，但追加
// prompt=login + max_age=0 强制 provider 现场重证（2FA 即在此处过）。复用既有
// /oauth/callback 作 redirect_uri；跳转前用 sessionStorage 记下「待办」标记 +
// 这次的 verifier + 要回到的批准页 URL（returnTo 走 stepUp.ts 的同款 open-redirect
// 校验，仅允许 /link）。回调检测到「待办」即分叉，不消费 code（见 oauth.callback）。
//
// 注意：这里不写常规 PKCE_VERIFIER_KEY / OAUTH_STATE_KEY，否则普通登录回调会误判。
// verifier 进 PENDING、state 进 STEPUP_STATE_KEY，二者均会在回调与批准页里清掉。
export async function startStepUpReauth(returnTo: string): Promise<void>
{
    const cfg = await fetchAuthConfig();
    if (cfg.auth_mode !== "prod")
    {
        throw new Error(`server reports auth_mode=${cfg.auth_mode}, expected prod`);
    }
    if (!cfg.authorize_url || !cfg.client_id || !cfg.redirect_uri)
    {
        throw new Error("OIDC config incomplete (authorize_url / client_id / redirect_uri)");
    }

    const verifier = generateRandomBase64url(32);
    const challenge = await sha256Base64url(verifier);
    const state = generateRandomBase64url(16);

    // 先把「待办」落定（含 returnTo 的 open-redirect 校验）；非法即拒，不发起跳转。
    if (!stashStepUpPending({ verifier, returnTo }))
    {
        throw new Error("invalid step-up return path");
    }
    sessionStorage.setItem(STEPUP_STATE_KEY, state);

    const params = new URLSearchParams({
        client_id: cfg.client_id,
        redirect_uri: cfg.redirect_uri,
        response_type: "code",
        scope: cfg.scopes || "openid profile email",
        state,
        code_challenge: challenge,
        code_challenge_method: "S256",
        prompt: "login",
        max_age: "0",
    });
    window.location.href = `${cfg.authorize_url}?${params}`;
}

// verifyStepUpState 在 /oauth/callback 的 step-up 分叉里校 state（防 CSRF，与常规
// 登录回调同口径）。匹配即消费掉 STEPUP_STATE_KEY 并返回 true；不匹配返回 false。
export function verifyStepUpState(state: string): boolean
{
    const expected = sessionStorage.getItem(STEPUP_STATE_KEY);
    sessionStorage.removeItem(STEPUP_STATE_KEY);
    return !!expected && state === expected;
}

// clearStepUpState 兜底清掉 step-up state（异常路径，避免残留）。
export function clearStepUpState(): void
{
    sessionStorage.removeItem(STEPUP_STATE_KEY);
}

interface ExchangeResp
{
    access_token: string;
    expires_in: number;
    user: { id: string; name: string; avatar?: string };
    // refresh_token 不再回传：服务端把它加密存进 web_sessions，浏览器只拿到一枚
    // HttpOnly 的 session cookie（随 /api/auth/exchange 响应一并下发）。
}

// completeOAuthLogin runs on /oauth/callback after the provider redirects back.
// Verifies state (CSRF), trades code+verifier for tokens via the backend
// proxy, and stamps the store with the user identity.
//
// Idempotent：若 store 中已有 user + accessToken（典型场景：useEffect 在 React
// 渲染管线中重入,首次已成功 setAuth + 消耗 localStorage,二次入场不能再被
// "PKCE verifier missing" / "state mismatch" 当作失败处理）→ 当作 noop 直接
// resolve，由上层 navigate 接管。
export async function completeOAuthLogin(code: string, state: string): Promise<void>
{
    const cur = useAppStore.getState();
    if (cur.user && cur.accessToken)
    {
        return;
    }

    const expectedState = localStorage.getItem(OAUTH_STATE_KEY);
    if (!expectedState || state !== expectedState)
    {
        throw new Error("OAuth state mismatch (possible CSRF or stale tab)");
    }
    const verifier = localStorage.getItem(PKCE_VERIFIER_KEY);
    if (!verifier)
    {
        throw new Error("PKCE verifier missing — restart the login flow");
    }

    const r = await fetch("/api/auth/exchange", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ code, code_verifier: verifier }),
    });
    if (!r.ok)
    {
        const text = await r.text().catch(() => r.statusText);
        throw new Error(`token exchange ${r.status}: ${text}`);
    }
    const data = await r.json() as ExchangeResp;

    localStorage.removeItem(PKCE_VERIFIER_KEY);
    localStorage.removeItem(OAUTH_STATE_KEY);

    useAppStore.getState().setAuth({
        accessToken: data.access_token,
        user: { id: data.user.id, name: data.user.name, avatar: data.user.avatar },
    });
}

interface CookieRefreshResp
{
    access_token: string;
    expires_in: number;
    user: { id: string; name: string; avatar?: string };
    device_name?: string;
}

// CookieRefreshResult 区分「服务端确证会话失效」与「短期失败」，使上层只在前者
// 清空登录态：
//   ok        —— 换到新 access token
//   invalid   —— /api/auth/refresh 返回 401：服务端查证会话已失效 / 被吊销 / 过期
//                （其处理器同时清掉 cookie），凭据确凿不可恢复
//   transient —— 5xx / 同源校验 403 / 响应残缺等非确证失败（网络层抛错则根本不进此
//                函数、照常向上抛）——一律保留登录态，按短期错误处理
type CookieRefreshResult =
    | { kind: "ok"; accessToken: string; user: User; deviceName: string }
    | { kind: "invalid" }
    | { kind: "transient" };

// cookieRefresh 用 HttpOnly session cookie 静默换一枚新 access_token。无 body——
// cookie 自动随同源请求发出（Path=/api/auth）。返回见 CookieRefreshResult：只有
// 服务端明确以 401 拒绝才算 invalid，短期失败一律 transient，不误伤登录态。
async function cookieRefresh(): Promise<CookieRefreshResult>
{
    const r = await fetch("/api/auth/refresh", { method: "POST" });
    if (!r.ok)
    {
        return { kind: r.status === 401 ? "invalid" : "transient" };
    }
    const data = await r.json().catch(() => null) as CookieRefreshResp | null;
    if (!data?.access_token || !data.user?.id) { return { kind: "transient" }; }
    return {
        kind: "ok",
        accessToken: data.access_token,
        user: { id: data.user.id, name: data.user.name, avatar: data.user.avatar },
        deviceName: data.device_name ?? "",
    };
}

// RefreshOutcome 是续期对上层的三态结论；只有 "invalid"（服务端确证失效）才允许
// 上层 forceLogout，"transient" 必须保留登录态（短期连接失败不清状态）。
export type RefreshOutcome = "refreshed" | "invalid" | "transient";

// refreshTokens 换新 access_token；api.ts 在请求拿到 401 时惰性调用。
export async function refreshTokens(): Promise<RefreshOutcome>
{
    const store = useAppStore.getState();
    if (store.authMode !== "prod") { return "transient"; }

    // 桌面端：refresh 完全在 Go 内部完成（refresh_token 只在 Go，JS 不持有），
    // 直接无参调用。Go 侧暂不区分「被吊销」与「短期失败」，故失败一律按 transient
    // 处理（不清空登录态）——桌面端即时吊销留后续（AUTH.md §3.1）。
    if (isDesktop())
    {
        const res = await desktopRefresh();
        if (!res?.access_token) { return "transient"; }
        const cur = useAppStore.getState();
        if (!cur.user) { return "transient"; }
        cur.setAuth({ accessToken: res.access_token, user: cur.user });
        return "refreshed";
    }

    // 浏览器端：走 HttpOnly cookie session（refresh_token 在服务端，JS 不持有）。
    const res = await cookieRefresh();
    if (res.kind !== "ok") { return res.kind; }   // "invalid" | "transient"，透传给上层
    const cur = useAppStore.getState();
    cur.setAuth({ accessToken: res.accessToken, user: res.user });
    // 设备名以服务端为权威；本地缺失（PWA 存储被清）时回填。
    if (res.deviceName && !cur.selfDeviceName) { cur.setSelfDeviceName(res.deviceName); }
    return "refreshed";
}

// bootstrapAuth 在 App 挂载前尝试「免重登」：浏览器端若本会话尚无 access_token，
// 用 HttpOnly cookie 静默续期，成功即直接进入已登录态（并回填服务端所记设备名），
// 失败则照常落到登录页。桌面端由注入式水合负责、dev 无 cookie 流程，均直接跳过。
export async function bootstrapAuth(): Promise<void>
{
    if (isDesktop()) { return; }
    const store = useAppStore.getState();
    if (store.authMode !== "prod") { return; }
    if (store.accessToken && store.user) { return; }   // 同会话已登录（sessionStorage 命中）

    const res = await cookieRefresh();
    if (res.kind !== "ok") { return; }   // 开机水合失败（含 invalid / transient）→ 照常落登录页
    const cur = useAppStore.getState();
    cur.setAuth({ accessToken: res.accessToken, user: res.user });
    if (res.deviceName && !cur.selfDeviceName) { cur.setSelfDeviceName(res.deviceName); }
}

// refreshSessionScope 拉一次 /api/me 校正本会话权限级别（full / guest）并写进
// store。在登录成功 / 开机水合后调用一次，使受限访客的账号管理入口被隐藏。失败
// 静默回退 "full"——只多展示入口，后端仍 403 兜底。dev 模式无 guest 概念，跳过。
export async function refreshSessionScope(): Promise<void>
{
    const store = useAppStore.getState();
    if (store.authMode !== "prod") { return; }
    if (!store.user || !store.accessToken) { return; }
    const scope = await fetchMeScope();
    useAppStore.getState().setSessionScope(scope);
}

// syncWebDeviceName 把本机设备名推到服务端 session（cookie 鉴权），让 PWA 存储被清
// 后开机仍能从 cookie session 恢复设备名、不再误跳 /setup。仅浏览器 prod；桌面端走
// persistDesktopDeviceName（Go 持久化），dev 无 session。失败静默——只是体验降级。
export function syncWebDeviceName(name: string): void
{
    if (isDesktop()) { return; }
    if (useAppStore.getState().authMode !== "prod") { return; }
    void fetch("/api/auth/device", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ device_name: name }),
    }).catch(() => { /* ignore */ });
}

// ---- PKCE helpers ---------------------------------------------------------

function generateRandomBase64url(numBytes: number): string
{
    const arr = new Uint8Array(numBytes);
    crypto.getRandomValues(arr);
    return base64urlEncode(arr);
}

async function sha256Base64url(input: string): Promise<string>
{
    const buf = new TextEncoder().encode(input);
    const hash = await crypto.subtle.digest("SHA-256", buf);
    return base64urlEncode(new Uint8Array(hash));
}

function base64urlEncode(bytes: Uint8Array): string
{
    let s = "";
    for (let i = 0; i < bytes.length; i += 1) { s += String.fromCharCode(bytes[i]); }
    return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
