import { useAppStore } from "../store";
import { refreshTokens, forceLogout } from "../features/auth/auth";
import { readInjectedApiBase, readInjectedDeviceType } from "../store/helpers";
import { toAsciiDeviceName } from "../utils/format";
import { t } from "../i18n";

// /api 请求的来源前缀：桌面 Windows 注入 loopback 代理地址（绕开会缓冲 SSE 的
// custom scheme），macOS / 浏览器为空（同源相对 URL）。读一次注入值即定。
const API_BASE = readInjectedApiBase();

// 客户端类型（browser / macos / windows / linux），随每个请求发给后端登记设备。
const DEVICE_TYPE = readInjectedDeviceType();

function withBase(input: RequestInfo): RequestInfo
{
    if (typeof input === "string" && API_BASE && input.startsWith("/"))
    {
        return API_BASE + input;
    }
    return input;
}

// Centralised fetch wrapper:
//   - in dev, attaches Authorization: Bearer <VITE_CDROP_DEV_TOKEN> + X-Dev-User
//   - in prod, attaches Authorization: Bearer <accessToken from store>
//   - always sets X-Device-Name from the store
//   - in prod, on a 401 response transparently refreshes the access token via
//     /api/auth/refresh and retries once
//   - in prod, ONLY if the server authoritatively confirms the session is gone
//     (refresh → HTTP 401), forces a local logout so the app returns to login
//
// Returns the fetch Response untouched so callers can drive streaming bodies
// (SSE, relay GET stream) themselves.
//
// State is cleared ONLY on server-confirmed credential invalidation. Short-term
// failures never clear it: a transient refresh response (5xx / same-origin 403)
// yields "transient", and a network error during refresh THROWS out of here —
// both keep the session, leaving the original 401 for the caller as a retryable
// error. So a flaky network or a server blip can't log the user out; only a
// genuine revoke/expire (refresh → 401) does.
export async function apiFetch(input: RequestInfo, init?: RequestInit): Promise<Response>
{
    let r = await doFetch(input, init);
    if (r.status === 401 && useAppStore.getState().authMode === "prod")
    {
        const outcome = await refreshTokens();
        if (outcome === "refreshed")
        {
            r = await doFetch(input, init);
        }
        else if (outcome === "invalid")
        {
            // 服务端确证会话已失效（/auth/refresh 返回 401，已被吊销 / 过期）→
            // 清空本地登录态，由 __root 守卫把界面送回登录页。返回这次 401 让调用
            // 方照常收尾。"transient"（5xx / 网络抖动）不进此分支：保留登录态。
            forceLogout();
        }
    }
    return r;
}

async function doFetch(input: RequestInfo, init?: RequestInit): Promise<Response>
{
    const state = useAppStore.getState();
    const headers = new Headers(init?.headers ?? {});

    if (state.authMode === "dev")
    {
        const devToken = import.meta.env.VITE_CDROP_DEV_TOKEN;
        if (!devToken)
        {
            throw new Error(t("errors.devTokenMissing"));
        }
        headers.set("Authorization", `Bearer ${devToken}`);
        if (state.user?.id)
        {
            headers.set("X-Dev-User", state.user.id);
        }
    }
    else
    {
        // A QR-paired device sends its broker access token as Bearer. A global-SSO
        // browser has none — the broker's domain cookie carries identity, so we send
        // no Authorization header and let the edge inject X-Auth from that cookie.
        if (state.accessToken)
        {
            headers.set("Authorization", `Bearer ${state.accessToken}`);
        }
    }

    if (state.selfDeviceName)
    {
        // Device names are ASCII-only; coerce here so a legacy non-ASCII name
        // can't make Headers.set throw (the server sanitises identically).
        const safeName = toAsciiDeviceName(state.selfDeviceName);
        if (safeName) { headers.set("X-Device-Name", safeName); }
    }
    headers.set("X-Device-Type", DEVICE_TYPE);

    return fetch(withBase(input), { ...init, headers });
}

export async function apiJSON<T>(input: RequestInfo, init?: RequestInit): Promise<T>
{
    const r = await apiFetch(input, init);
    if (!r.ok)
    {
        const text = await r.text().catch(() => r.statusText);
        throw new Error(`${r.status}: ${text}`);
    }
    if (r.status === 204) { return undefined as T; }
    return ( await r.json() ) as T;
}
