import { canonicalizeResourceIndicator } from "../resource-indicator.js";
const MAX_JS_DATE_MS = 8_640_000_000_000_000;
export class OAuthError extends Error {
    error;
    errorDescription;
    errorUri;
    error_description;
    error_uri;
    status;
    retryable;
    terminal;
    constructor(shape, status) {
        super(shape.error_description ?? shape.error);
        this.name = "OAuthError";
        this.error = shape.error;
        this.errorDescription = shape.error_description;
        this.errorUri = shape.error_uri;
        this.error_description = shape.error_description;
        this.error_uri = shape.error_uri;
        this.status = status;
        this.retryable = isRetryableOAuthError(this);
        this.terminal = !this.retryable;
    }
}
export function isRetryableOAuthError(error) {
    return (error instanceof OAuthError &&
        (error.status >= 500 ||
            error.error === "server_error" ||
            error.error === "temporarily_unavailable"));
}
export async function exchangeAuthorizationCode(input) {
    const resource = canonicalizeResourceIndicator(input.resource);
    return requestTokens({
        tokenEndpoint: input.tokenEndpoint,
        clientId: input.clientId,
        clientSecret: input.clientSecret,
        params: {
            grant_type: "authorization_code",
            code: input.code,
            code_verifier: input.codeVerifier,
            redirect_uri: input.redirectUri,
            resource
        },
        fetch: input.fetch,
        now: input.now
    });
}
export async function refreshAccessToken(input) {
    const resource = canonicalizeResourceIndicator(input.resource);
    return requestTokens({
        tokenEndpoint: input.tokenEndpoint,
        clientId: input.clientId,
        clientSecret: input.clientSecret,
        params: {
            grant_type: "refresh_token",
            refresh_token: input.refreshToken,
            resource
        },
        fetch: input.fetch,
        now: input.now
    });
}
async function requestTokens(input) {
    const body = new URLSearchParams({
        client_id: input.clientId,
        ...input.params
    });
    if (input.clientSecret !== undefined) {
        body.set("client_secret", input.clientSecret);
    }
    const response = await input.fetch(input.tokenEndpoint, {
        method: "POST",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded"
        },
        body: body.toString()
    });
    const payload = await readOAuthJsonObjectResponse(response);
    const accessToken = getOwnEntry(payload, "access_token");
    if (typeof accessToken !== "string" || accessToken.trim().length === 0) {
        throw new Error("OAuth token response missing access_token");
    }
    const normalizedAccessToken = accessToken.trim();
    const tokenType = normalizeBearerTokenType(getOwnEntry(payload, "token_type"));
    if (tokenType === null) {
        throw new Error("OAuth token response missing token_type=Bearer");
    }
    const expiresIn = getOwnEntry(payload, "expires_in");
    let expiresAt = null;
    if (expiresIn !== undefined) {
        if (typeof expiresIn !== "number" ||
            !Number.isFinite(expiresIn) ||
            !Number.isInteger(expiresIn) ||
            expiresIn < 0) {
            throw new Error("OAuth token response has invalid expires_in");
        }
        expiresAt = input.now() + expiresIn * 1000;
        if (!Number.isSafeInteger(expiresAt) ||
            expiresAt > MAX_JS_DATE_MS ||
            !Number.isFinite(new Date(expiresAt).getTime())) {
            throw new Error("OAuth token response has invalid expires_in");
        }
    }
    const refreshToken = getOwnEntry(payload, "refresh_token");
    const scope = getOwnEntry(payload, "scope");
    const normalizedRefreshToken = typeof refreshToken === "string" && refreshToken.trim().length > 0
        ? refreshToken.trim()
        : undefined;
    const normalizedScope = typeof scope === "string" && scope.trim().length > 0 ? scope.trim() : undefined;
    return {
        accessToken: normalizedAccessToken,
        refreshToken: normalizedRefreshToken === undefined ? undefined : normalizedRefreshToken,
        tokenType,
        expiresAt,
        scope: normalizedScope === undefined ? undefined : normalizedScope
    };
}
export async function readOAuthJsonObjectResponse(response) {
    const fallbackError = createFallbackOAuthError(response.status);
    let payload;
    try {
        payload = await response.json();
    }
    catch {
        if (!response.ok) {
            throw fallbackError;
        }
        throw new Error("OAuth response must be a JSON object");
    }
    if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
        if (!response.ok) {
            throw fallbackError;
        }
        throw new Error("OAuth response must be a JSON object");
    }
    const record = payload;
    if (!response.ok) {
        throw new OAuthError(readOAuthError(record, fallbackError.error), response.status);
    }
    return record;
}
function readOAuthError(payload, fallbackError = "server_error") {
    const error = getOwnEntry(payload, "error");
    const errorDescription = getOwnEntry(payload, "error_description");
    const errorUri = getOwnEntry(payload, "error_uri");
    return {
        error: typeof error === "string" ? error : fallbackError,
        error_description: typeof errorDescription === "string" ? errorDescription : undefined,
        error_uri: typeof errorUri === "string" ? errorUri : undefined
    };
}
function getOwnEntry(record, key) {
    return Object.prototype.hasOwnProperty.call(record, key) ? record[key] : undefined;
}
function createFallbackOAuthError(status) {
    const error = status === 503 ? "temporarily_unavailable" : "server_error";
    return new OAuthError({ error }, status);
}
function normalizeBearerTokenType(value) {
    if (typeof value !== "string") {
        return null;
    }
    return value.toLowerCase() === "bearer" ? "Bearer" : null;
}
