/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { ApiError } from '@google/genai';
import { AuthType } from '../core/contentGenerator.js';
import { classifyGoogleError, RetryableQuotaError, TerminalQuotaError, } from './googleQuotaErrors.js';
import { delay, createAbortError } from './delay.js';
import { debugLogger } from './debugLogger.js';
import { getErrorStatus, ModelNotFoundError } from './httpErrors.js';
const FETCH_FAILED_MESSAGE = 'exception TypeError: fetch failed sending request';
const DEFAULT_RETRY_OPTIONS = {
    maxAttempts: 3,
    initialDelayMs: 5000,
    maxDelayMs: 30000, // 30 seconds
    shouldRetryOnError: defaultShouldRetry,
};
/**
 * Default predicate function to determine if a retry should be attempted.
 * Retries on 429 (Too Many Requests) and 5xx server errors.
 * @param error The error object.
 * @param retryFetchErrors Whether to retry on specific fetch errors.
 * @returns True if the error is a transient error, false otherwise.
 */
function defaultShouldRetry(error, retryFetchErrors) {
    if (retryFetchErrors &&
        error instanceof Error &&
        error.message.includes(FETCH_FAILED_MESSAGE)) {
        return true;
    }
    // Priority check for ApiError
    if (error instanceof ApiError) {
        // Explicitly do not retry 400 (Bad Request)
        if (error.status === 400)
            return false;
        return error.status === 429 || (error.status >= 500 && error.status < 600);
    }
    // Check for status using helper (handles other error shapes)
    const status = getErrorStatus(error);
    if (status !== undefined) {
        return status === 429 || (status >= 500 && status < 600);
    }
    return false;
}
/**
 * Retries a function with exponential backoff and jitter.
 * @param fn The asynchronous function to retry.
 * @param options Optional retry configuration.
 * @returns A promise that resolves with the result of the function if successful.
 * @throws The last error encountered if all attempts fail.
 */
export async function retryWithBackoff(fn, options) {
    if (options?.signal?.aborted) {
        throw createAbortError();
    }
    if (options?.maxAttempts !== undefined && options.maxAttempts <= 0) {
        throw new Error('maxAttempts must be a positive number.');
    }
    const cleanOptions = options
        ? Object.fromEntries(Object.entries(options).filter(([_, v]) => v != null))
        : {};
    const { maxAttempts, initialDelayMs, maxDelayMs, onPersistent429, authType, shouldRetryOnError, shouldRetryOnContent, retryFetchErrors, signal, } = {
        ...DEFAULT_RETRY_OPTIONS,
        ...cleanOptions,
    };
    let attempt = 0;
    let currentDelay = initialDelayMs;
    while (attempt < maxAttempts) {
        if (signal?.aborted) {
            throw createAbortError();
        }
        attempt++;
        try {
            const result = await fn();
            if (shouldRetryOnContent &&
                shouldRetryOnContent(result)) {
                const jitter = currentDelay * 0.3 * (Math.random() * 2 - 1);
                const delayWithJitter = Math.max(0, currentDelay + jitter);
                await delay(delayWithJitter, signal);
                currentDelay = Math.min(maxDelayMs, currentDelay * 2);
                continue;
            }
            return result;
        }
        catch (error) {
            if (error instanceof Error && error.name === 'AbortError') {
                throw error;
            }
            const classifiedError = classifyGoogleError(error);
            const errorCode = getErrorStatus(error);
            if (classifiedError instanceof TerminalQuotaError ||
                classifiedError instanceof ModelNotFoundError) {
                if (onPersistent429 && authType === AuthType.LOGIN_WITH_GOOGLE) {
                    try {
                        const fallbackModel = await onPersistent429(authType, classifiedError);
                        if (fallbackModel) {
                            attempt = 0; // Reset attempts and retry with the new model.
                            currentDelay = initialDelayMs;
                            continue;
                        }
                    }
                    catch (fallbackError) {
                        debugLogger.warn('Fallback to Flash model failed:', fallbackError);
                    }
                }
                throw classifiedError; // Throw if no fallback or fallback failed.
            }
            const is500 = errorCode !== undefined && errorCode >= 500 && errorCode < 600;
            if (classifiedError instanceof RetryableQuotaError || is500) {
                if (attempt >= maxAttempts) {
                    if (onPersistent429 && authType === AuthType.LOGIN_WITH_GOOGLE) {
                        try {
                            const fallbackModel = await onPersistent429(authType, classifiedError);
                            if (fallbackModel) {
                                attempt = 0; // Reset attempts and retry with the new model.
                                currentDelay = initialDelayMs;
                                continue;
                            }
                        }
                        catch (fallbackError) {
                            console.warn('Model fallback failed:', fallbackError);
                        }
                    }
                    throw classifiedError instanceof RetryableQuotaError
                        ? classifiedError
                        : error;
                }
                if (classifiedError instanceof RetryableQuotaError) {
                    console.warn(`Attempt ${attempt} failed: ${classifiedError.message}. Retrying after ${classifiedError.retryDelayMs}ms...`);
                    await delay(classifiedError.retryDelayMs, signal);
                    continue;
                }
                else {
                    const errorStatus = getErrorStatus(error);
                    logRetryAttempt(attempt, error, errorStatus);
                    // Exponential backoff with jitter for non-quota errors
                    const jitter = currentDelay * 0.3 * (Math.random() * 2 - 1);
                    const delayWithJitter = Math.max(0, currentDelay + jitter);
                    await delay(delayWithJitter, signal);
                    currentDelay = Math.min(maxDelayMs, currentDelay * 2);
                    continue;
                }
            }
            // Generic retry logic for other errors
            if (attempt >= maxAttempts ||
                !shouldRetryOnError(error, retryFetchErrors)) {
                throw error;
            }
            const errorStatus = getErrorStatus(error);
            logRetryAttempt(attempt, error, errorStatus);
            // Exponential backoff with jitter for non-quota errors
            const jitter = currentDelay * 0.3 * (Math.random() * 2 - 1);
            const delayWithJitter = Math.max(0, currentDelay + jitter);
            await delay(delayWithJitter, signal);
            currentDelay = Math.min(maxDelayMs, currentDelay * 2);
        }
    }
    throw new Error('Retry attempts exhausted');
}
/**
 * Logs a message for a retry attempt when using exponential backoff.
 * @param attempt The current attempt number.
 * @param error The error that caused the retry.
 * @param errorStatus The HTTP status code of the error, if available.
 */
function logRetryAttempt(attempt, error, errorStatus) {
    let message = `Attempt ${attempt} failed. Retrying with backoff...`;
    if (errorStatus) {
        message = `Attempt ${attempt} failed with status ${errorStatus}. Retrying with backoff...`;
    }
    if (errorStatus === 429) {
        debugLogger.warn(message, error);
    }
    else if (errorStatus && errorStatus >= 500 && errorStatus < 600) {
        console.error(message, error);
    }
    else if (error instanceof Error) {
        // Fallback for errors that might not have a status but have a message
        if (error.message.includes('429')) {
            debugLogger.warn(`Attempt ${attempt} failed with 429 error (no Retry-After header). Retrying with backoff...`, error);
        }
        else if (error.message.match(/5\d{2}/)) {
            console.error(`Attempt ${attempt} failed with 5xx error. Retrying with backoff...`, error);
        }
        else {
            debugLogger.warn(message, error); // Default to warn for other errors
        }
    }
    else {
        debugLogger.warn(message, error); // Default to warn if error type is unknown
    }
}
//# sourceMappingURL=retry.js.map