import { logger } from "../utils/logger.js";
import { tokenStore } from "../auth/tokenStore.js";
import { writeJsonSnapshotAtomically } from "./snapshotPersistence.js";
const REFRESH_URL = "https://api.anthropic.com/v1/oauth/token";
const REFRESH_URL_FALLBACK = "https://console.anthropic.com/v1/oauth/token";
const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
const BUFFER_MS = 5 * 60 * 1000;
const SUCCESS_CACHE_MS = 60_000;
const USER_AGENT = "claude-cli/2.1.80 (external, cli)";
const refreshesInFlight = new Map();
export function needsRefresh(account) {
    return !!(account.expiresAt &&
        account.expiresAt <= Date.now() + BUFFER_MS &&
        account.refreshToken);
}
function isRefreshCredentialRejection(status) {
    return status === 400 || status === 401 || status === 403 || status === 404;
}
export function isPermanentRefreshFailure(result) {
    return isRefreshCredentialRejection(result.status);
}
async function performTokenRefresh(account) {
    if (!account.refreshToken) {
        return { success: false, error: "No refresh token available" };
    }
    const formBody = new URLSearchParams({
        grant_type: "refresh_token",
        refresh_token: account.refreshToken,
        client_id: CLIENT_ID,
    }).toString();
    const headers = {
        "Content-Type": "application/x-www-form-urlencoded",
        "User-Agent": USER_AGENT,
    };
    const urls = [REFRESH_URL, REFRESH_URL_FALLBACK];
    let terminalFailure;
    for (const url of urls) {
        try {
            const resp = await fetch(url, {
                method: "POST",
                headers,
                body: formBody,
                signal: AbortSignal.timeout(10_000),
            });
            if (!resp.ok) {
                const errorBody = await resp.text();
                logger.warn(`[token-refresh] failed for ${account.label} at ${url}`, {
                    status: resp.status,
                    error: errorBody.slice(0, 500),
                });
                const failure = {
                    success: false,
                    error: errorBody,
                    status: resp.status,
                };
                if (isRefreshCredentialRejection(resp.status)) {
                    terminalFailure = failure;
                }
                // If primary URL returned a non-ok status, try fallback.
                if (url === REFRESH_URL) {
                    continue;
                }
                return terminalFailure ?? failure;
            }
            const data = (await resp.json());
            account.token = data.access_token;
            account.expiresAt =
                data.expires_in !== undefined
                    ? Date.now() + data.expires_in * 1000
                    : Date.now() + 55 * 60 * 1000;
            if (data.refresh_token) {
                account.refreshToken = data.refresh_token;
            }
            logger.debug(`[token-refresh] refreshed for ${account.label}`);
            return { success: true };
        }
        catch (e) {
            logger.warn(`[token-refresh] exception for ${account.label} at ${url}`, {
                error: String(e),
            });
            // If primary URL threw, try fallback
            if (url === REFRESH_URL) {
                continue;
            }
            return terminalFailure ?? { success: false, error: String(e) };
        }
    }
    // Should not reach here, but guard against empty urls array
    return (terminalFailure ?? {
        success: false,
        error: "No refresh URLs available",
    });
}
/**
 * Serialize refreshes by refresh-token value. OAuth refresh tokens may rotate,
 * so concurrent refreshes with the same old token can make all but the first
 * request fail with invalid_grant. A short success cache covers callers that
 * loaded either the old token or the newly rotated token around persistence.
 */
export async function refreshToken(account) {
    if (!account.refreshToken) {
        return { success: false, error: "No refresh token available" };
    }
    const lockKey = account.refreshToken;
    let sharedPromise = refreshesInFlight.get(lockKey);
    if (!sharedPromise) {
        const refreshAccount = { ...account };
        const createdPromise = performTokenRefresh(refreshAccount).then((result) => {
            const shared = {
                result,
                token: refreshAccount.token,
                refreshToken: refreshAccount.refreshToken,
                expiresAt: refreshAccount.expiresAt,
            };
            if (result.success) {
                const cachedKeys = new Set([lockKey]);
                const rotatedKey = refreshAccount.refreshToken;
                if (rotatedKey &&
                    rotatedKey !== lockKey &&
                    !refreshesInFlight.has(rotatedKey)) {
                    refreshesInFlight.set(rotatedKey, createdPromise);
                    cachedKeys.add(rotatedKey);
                }
                const timer = setTimeout(() => {
                    for (const key of cachedKeys) {
                        if (refreshesInFlight.get(key) === createdPromise) {
                            refreshesInFlight.delete(key);
                        }
                    }
                }, SUCCESS_CACHE_MS);
                timer.unref?.();
            }
            else if (refreshesInFlight.get(lockKey) === createdPromise) {
                refreshesInFlight.delete(lockKey);
            }
            return shared;
        }, (error) => {
            if (refreshesInFlight.get(lockKey) === createdPromise) {
                refreshesInFlight.delete(lockKey);
            }
            throw error;
        });
        refreshesInFlight.set(lockKey, createdPromise);
        sharedPromise = createdPromise;
    }
    const shared = await sharedPromise;
    if (shared.result.success) {
        account.token = shared.token;
        account.refreshToken = shared.refreshToken;
        account.expiresAt = shared.expiresAt;
    }
    return shared.result;
}
export function clearRefreshStateForTests() {
    refreshesInFlight.clear();
}
export async function persistTokens(target, account) {
    if (typeof target !== "string" && "providerKey" in target) {
        await persistTokenStoreAccount(target.providerKey, account);
        return;
    }
    const credPath = typeof target === "string" ? target : target.credPath;
    await persistLegacyCredentials(credPath, account);
}
async function persistLegacyCredentials(credPath, account) {
    try {
        const fs = await import("fs/promises");
        const existing = JSON.parse(await fs.readFile(credPath, "utf8"));
        existing.oauth = {
            ...existing.oauth,
            accessToken: account.token,
            expiresAt: account.expiresAt,
            refreshToken: account.refreshToken,
        };
        existing.updatedAt = Date.now();
        await writeJsonSnapshotAtomically(credPath, existing, 0o600);
    }
    catch (err) {
        logger.warn("[token-refresh] Failed to persist legacy credentials", {
            error: err instanceof Error ? err.message : String(err),
        });
    }
}
async function persistTokenStoreAccount(providerKey, account) {
    try {
        const existing = await tokenStore.loadTokens(providerKey);
        const merged = {
            accessToken: account.token,
            refreshToken: account.refreshToken ?? existing?.refreshToken,
            expiresAt: account.expiresAt ?? existing?.expiresAt ?? Date.now() + 55 * 60 * 1000,
            tokenType: existing?.tokenType ?? "Bearer",
            ...(existing?.scope ? { scope: existing.scope } : {}),
        };
        await tokenStore.saveTokens(providerKey, merged);
    }
    catch (err) {
        logger.warn("[token-refresh] Failed to persist TokenStore credentials", {
            error: err instanceof Error ? err.message : String(err),
        });
    }
}
//# sourceMappingURL=tokenRefresh.js.map