import { createSha256Hash, randomBytesBase64url } from './crypto-adapter.js';
import { instrumentedFetch } from '../../utils/fetch-with-timeout.js';
export function createOAuthState(bytes = 24) {
    return randomBytesBase64url(bytes);
}
export function createPkceVerifier(bytes = 32) {
    return randomBytesBase64url(bytes);
}
export async function createPkceChallenge(verifier) {
    return createSha256Hash(verifier);
}
export function parseOAuthScopes(raw) {
    if (typeof raw !== 'string')
        return undefined;
    const scopes = raw.split(' ').map((value) => value.trim()).filter(Boolean);
    return scopes.length > 0 ? scopes : undefined;
}
export async function buildOAuthAuthorizationStart(config, input) {
    const state = input?.state ?? createOAuthState();
    const verifier = input?.verifier ?? createPkceVerifier();
    const redirectUri = input?.redirectUri ?? config.redirectUri;
    const url = new URL(config.authUrl);
    url.searchParams.set('response_type', 'code');
    url.searchParams.set('client_id', config.clientId);
    url.searchParams.set('redirect_uri', redirectUri);
    url.searchParams.set('state', state);
    if (config.scopes && config.scopes.length > 0) {
        url.searchParams.set('scope', config.scopes.join(' '));
    }
    if (config.audience) {
        url.searchParams.set('audience', config.audience);
    }
    if (config.usePkce ?? true) {
        url.searchParams.set('code_challenge', await createPkceChallenge(verifier));
        url.searchParams.set('code_challenge_method', 'S256');
    }
    for (const [key, value] of Object.entries(config.authParams ?? {})) {
        url.searchParams.set(key, value);
    }
    return {
        authorizationUrl: url.toString(),
        state,
        verifier,
        redirectUri,
    };
}
async function exchangeOAuthRequest(url, encoding, payload) {
    const body = encoding === 'json'
        ? JSON.stringify(payload)
        : new URLSearchParams(Object.entries(payload).map(([key, value]) => [key, String(value)])).toString();
    const response = await instrumentedFetch(url, {
        method: 'POST',
        headers: {
            'Content-Type': encoding === 'json' ? 'application/json' : 'application/x-www-form-urlencoded',
        },
        body,
    });
    if (!response.ok) {
        const text = await response.text().catch(() => '');
        throw new Error(`OAuth token exchange failed (${response.status}): ${text}`);
    }
    const json = await response.json();
    if (typeof json.access_token !== 'string' || json.access_token.length === 0) {
        throw new Error('OAuth token exchange did not return an access token.');
    }
    return {
        accessToken: json.access_token,
        ...(typeof json.refresh_token === 'string' && json.refresh_token.length > 0
            ? { refreshToken: json.refresh_token }
            : {}),
        tokenType: typeof json.token_type === 'string' && json.token_type.length > 0 ? json.token_type : 'Bearer',
        ...(typeof json.expires_in === 'number' && Number.isFinite(json.expires_in)
            ? { expiresAt: Date.now() + (json.expires_in * 1000) }
            : {}),
        ...(parseOAuthScopes(json.scope) ? { scopes: parseOAuthScopes(json.scope) } : {}),
    };
}
export async function exchangeOAuthAuthorizationCode(config, input) {
    const payload = {
        grant_type: 'authorization_code',
        client_id: config.clientId,
        redirect_uri: input.redirectUri,
        code: input.code,
    };
    if (config.usePkce ?? true)
        payload.code_verifier = input.verifier;
    if (config.audience)
        payload.audience = config.audience;
    if (config.includeStateInTokenRequest && input.state)
        payload.state = input.state;
    for (const [key, value] of Object.entries(config.tokenRequestExtras ?? {})) {
        payload[key] = value;
    }
    return exchangeOAuthRequest(config.tokenUrl, config.tokenRequestEncoding ?? 'form', payload);
}
export async function refreshOAuthAccessToken(config, refreshToken) {
    const payload = {
        grant_type: 'refresh_token',
        refresh_token: refreshToken,
        client_id: config.clientId,
    };
    if (config.refreshScopes && config.refreshScopes.length > 0) {
        payload.scope = config.refreshScopes.join(' ');
    }
    if (config.audience)
        payload.audience = config.audience;
    for (const [key, value] of Object.entries(config.refreshRequestExtras ?? {})) {
        payload[key] = value;
    }
    return exchangeOAuthRequest(config.tokenUrl, config.refreshRequestEncoding ?? config.tokenRequestEncoding ?? 'form', payload);
}
export function decodeJwtPayload(token) {
    if (typeof atob !== 'function') {
        throw new Error('decodeJwtPayload requires a global atob(); available in browsers, RN Hermes, and Node >= 16');
    }
    const parts = token.split('.');
    if (parts.length < 2)
        return null;
    try {
        // atob is universally available (browsers, React Native, modern Node)
        const base64 = (parts[1]).replace(/-/g, '+').replace(/_/g, '/');
        return JSON.parse(atob(base64));
    }
    catch {
        return null;
    }
}
