/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { parseGoogleApiError, } from './googleErrors.js';
import { getErrorStatus, ModelNotFoundError } from './httpErrors.js';
// Enum for Google API type strings
var GoogleApiType;
(function (GoogleApiType) {
    GoogleApiType["ERROR_INFO"] = "type.googleapis.com/google.rpc.ErrorInfo";
    GoogleApiType["HELP"] = "type.googleapis.com/google.rpc.Help";
    GoogleApiType["QUOTA_FAILURE"] = "type.googleapis.com/google.rpc.QuotaFailure";
    GoogleApiType["RETRY_INFO"] = "type.googleapis.com/google.rpc.RetryInfo";
})(GoogleApiType || (GoogleApiType = {}));
/**
 * A non-retryable error indicating a hard quota limit has been reached (e.g., daily limit).
 */
export class TerminalQuotaError extends Error {
    cause;
    retryDelayMs;
    reason;
    constructor(message, cause, retryDelaySeconds, reason) {
        super(message);
        this.cause = cause;
        this.name = 'TerminalQuotaError';
        this.retryDelayMs = retryDelaySeconds
            ? retryDelaySeconds * 1000
            : undefined;
        this.reason = reason;
    }
    get isInsufficientCredits() {
        return this.reason === 'INSUFFICIENT_G1_CREDITS_BALANCE';
    }
}
/**
 * A retryable error indicating a temporary quota issue (e.g., per-minute limit).
 */
export class RetryableQuotaError extends Error {
    cause;
    retryDelayMs;
    constructor(message, cause, retryDelaySeconds) {
        super(message);
        this.cause = cause;
        this.name = 'RetryableQuotaError';
        this.retryDelayMs = retryDelaySeconds
            ? retryDelaySeconds * 1000
            : undefined;
    }
}
/**
 * An error indicating that user validation is required to continue.
 */
export class ValidationRequiredError extends Error {
    cause;
    validationLink;
    validationDescription;
    learnMoreUrl;
    userHandled = false;
    constructor(message, cause, validationLink, validationDescription, learnMoreUrl) {
        super(message);
        this.cause = cause;
        this.name = 'ValidationRequiredError';
        this.validationLink = validationLink;
        this.validationDescription = validationDescription;
        this.learnMoreUrl = learnMoreUrl;
    }
}
/**
 * Parses a duration string (e.g., "34.074824224s", "60s", "900ms") and returns the time in seconds.
 * @param duration The duration string to parse.
 * @returns The duration in seconds, or null if parsing fails.
 */
function parseDurationInSeconds(duration) {
    if (duration.endsWith('ms')) {
        const milliseconds = parseFloat(duration.slice(0, -2));
        return isNaN(milliseconds) ? null : milliseconds / 1000;
    }
    if (duration.endsWith('s')) {
        const seconds = parseFloat(duration.slice(0, -1));
        return isNaN(seconds) ? null : seconds;
    }
    return null;
}
/**
 * Maximum retry delay (in seconds) before a retryable error is treated as terminal.
 * If the server suggests waiting longer than this, the user is effectively locked out,
 * so we trigger the fallback/credits flow instead of silently waiting.
 */
const MAX_RETRYABLE_DELAY_SECONDS = 300; // 5 minutes
/**
 * Valid Cloud Code API domains for VALIDATION_REQUIRED errors.
 */
const CLOUDCODE_DOMAINS = [
    'cloudcode-pa.googleapis.com',
    'staging-cloudcode-pa.googleapis.com',
    'autopush-cloudcode-pa.googleapis.com',
];
/**
 * Checks if the given domain belongs to a Cloud Code API endpoint.
 * Sanitizes stray characters that SSE stream parsing can inject into the
 * domain string before comparing.
 */
function isCloudCodeDomain(domain) {
    const sanitized = domain.replace(/[^a-zA-Z0-9.-]/g, '');
    return CLOUDCODE_DOMAINS.includes(sanitized);
}
/**
 * Checks if a 403 error requires user validation and extracts validation details.
 *
 * @param googleApiError The parsed Google API error to check.
 * @returns A `ValidationRequiredError` if validation is required, otherwise `null`.
 */
function classifyValidationRequiredError(googleApiError) {
    const errorInfo = googleApiError.details.find((d) => d['@type'] === GoogleApiType.ERROR_INFO);
    if (!errorInfo) {
        return null;
    }
    if (!errorInfo.domain ||
        !isCloudCodeDomain(errorInfo.domain) ||
        errorInfo.reason !== 'VALIDATION_REQUIRED') {
        return null;
    }
    // Try to extract validation info from Help detail first
    const helpDetail = googleApiError.details.find((d) => d['@type'] === GoogleApiType.HELP);
    let validationLink;
    let validationDescription;
    let learnMoreUrl;
    if (helpDetail?.links && helpDetail.links.length > 0) {
        // First link is the validation link, extract description and URL
        const validationLinkInfo = helpDetail.links[0];
        validationLink = validationLinkInfo.url;
        validationDescription = validationLinkInfo.description;
        // Look for "Learn more" link - identified by description or support.google.com hostname
        const learnMoreLink = helpDetail.links.find((link) => {
            if (link.description.toLowerCase().trim() === 'learn more')
                return true;
            const parsed = URL.parse(link.url);
            return parsed?.hostname === 'support.google.com';
        });
        if (learnMoreLink) {
            learnMoreUrl = learnMoreLink.url;
        }
    }
    // Fallback to ErrorInfo metadata if Help detail not found
    if (!validationLink) {
        validationLink = errorInfo.metadata?.['validation_link'];
    }
    return new ValidationRequiredError(googleApiError.message, googleApiError, validationLink, validationDescription, learnMoreUrl);
}
/**
 * Analyzes a caught error and classifies it as a specific error type if applicable.
 *
 * Classification logic:
 * - 404 errors are classified as `ModelNotFoundError`.
 * - 403 errors with `VALIDATION_REQUIRED` from cloudcode-pa domains are classified
 *   as `ValidationRequiredError`.
 * - 429 or 499 errors are classified as either `TerminalQuotaError` or `RetryableQuotaError`:
 *   - CloudCode API: `RATE_LIMIT_EXCEEDED` → `RetryableQuotaError`, `QUOTA_EXHAUSTED` → `TerminalQuotaError`.
 *   - If the error indicates a daily limit (in QuotaFailure), it's a `TerminalQuotaError`.
 *   - If the error has a retry delay, it's a `RetryableQuotaError`.
 *   - If the error indicates a per-minute limit, it's a `RetryableQuotaError`.
 *   - If the error message contains the phrase "Please retry in X[s|ms]", it's a `RetryableQuotaError`.
 * - 503 errors are classified as `RetryableQuotaError`.
 *
 * @param error The error to classify.
 * @returns A classified error or the original `unknown` error.
 */
export function classifyGoogleError(error) {
    const googleApiError = parseGoogleApiError(error);
    const status = googleApiError?.code ?? getErrorStatus(error);
    const errorMessage = googleApiError?.message || extractErrorMessage(error);
    if (status === 404) {
        const message = errorMessage.trim() || 'Model not found';
        return new ModelNotFoundError(message, status);
    }
    // Check for 403 VALIDATION_REQUIRED errors from Cloud Code API
    if (status === 403 && googleApiError) {
        const validationError = classifyValidationRequiredError(googleApiError);
        if (validationError) {
            return validationError;
        }
    }
    // Universal limit: 0 check (moved outside and before the fallback block)
    const lowerMessage = errorMessage.toLowerCase();
    if ((status === 429 || status === 499 || status === 503) &&
        /limit:\s*0(?!\d|\.\d)/.test(lowerMessage)) {
        const cause = googleApiError ?? {
            code: status ?? 429,
            message: errorMessage,
            details: [],
        };
        return new TerminalQuotaError(errorMessage, cause);
    }
    if (!googleApiError ||
        (googleApiError.code !== 429 &&
            googleApiError.code !== 499 &&
            googleApiError.code !== 503) ||
        googleApiError.details.length === 0) {
        // Fallback: try to parse the error message for a retry delay
        const match = errorMessage.match(/Please retry in ([0-9.]+(?:ms|s))/);
        if (match?.[1]) {
            const retryDelaySeconds = parseDurationInSeconds(match[1]);
            if (retryDelaySeconds !== null) {
                const cause = googleApiError ?? {
                    code: status ?? 429,
                    message: errorMessage,
                    details: [],
                };
                if (retryDelaySeconds > MAX_RETRYABLE_DELAY_SECONDS) {
                    return new TerminalQuotaError(errorMessage, cause, retryDelaySeconds);
                }
                return new RetryableQuotaError(errorMessage, cause, retryDelaySeconds);
            }
        }
        else if (status === 429 || status === 499 || status === 503) {
            // Fallback: If it is a 429, 499, or 503 but doesn't have a specific "retry in" message,
            // assume it is a temporary rate limit and retry.
            return new RetryableQuotaError(errorMessage, googleApiError ?? {
                code: status,
                message: errorMessage,
                details: [],
            });
        }
        return error; // Not a retryable error we can handle with structured details or a parsable retry message.
    }
    const quotaFailure = googleApiError.details.find((d) => d['@type'] === GoogleApiType.QUOTA_FAILURE);
    const errorInfo = googleApiError.details.find((d) => d['@type'] === GoogleApiType.ERROR_INFO);
    const retryInfo = googleApiError.details.find((d) => d['@type'] === GoogleApiType.RETRY_INFO);
    // 1. Check for long-term limits in QuotaFailure or ErrorInfo
    if (quotaFailure) {
        for (const violation of quotaFailure.violations) {
            const quotaId = violation.quotaId ?? '';
            if (quotaId.includes('PerDay') || quotaId.includes('Daily')) {
                return new TerminalQuotaError(`You have exhausted your daily quota on this model.`, googleApiError);
            }
        }
    }
    let delaySeconds;
    if (retryInfo?.retryDelay) {
        const parsedDelay = parseDurationInSeconds(retryInfo.retryDelay);
        if (parsedDelay) {
            delaySeconds = parsedDelay;
        }
    }
    if (errorInfo) {
        // INSUFFICIENT_G1_CREDITS_BALANCE is always terminal, regardless of domain
        if (errorInfo.reason === 'INSUFFICIENT_G1_CREDITS_BALANCE') {
            return new TerminalQuotaError(googleApiError.message, googleApiError, delaySeconds, errorInfo.reason);
        }
        // New Cloud Code API quota handling
        if (errorInfo.domain) {
            if (isCloudCodeDomain(errorInfo.domain)) {
                if (errorInfo.reason === 'RATE_LIMIT_EXCEEDED') {
                    const effectiveDelay = delaySeconds ?? 10;
                    if (effectiveDelay > MAX_RETRYABLE_DELAY_SECONDS) {
                        return new TerminalQuotaError(googleApiError.message, googleApiError, effectiveDelay, errorInfo.reason);
                    }
                    return new RetryableQuotaError(googleApiError.message, googleApiError, effectiveDelay);
                }
                if (errorInfo.reason === 'QUOTA_EXHAUSTED') {
                    return new TerminalQuotaError(googleApiError.message, googleApiError, delaySeconds, errorInfo.reason);
                }
            }
        }
    }
    // 2. Check for delays in RetryInfo
    if (retryInfo?.retryDelay && delaySeconds) {
        if (delaySeconds > MAX_RETRYABLE_DELAY_SECONDS) {
            return new TerminalQuotaError(`${googleApiError.message}\nSuggested retry after ${retryInfo.retryDelay}.`, googleApiError, delaySeconds);
        }
        return new RetryableQuotaError(`${googleApiError.message}\nSuggested retry after ${retryInfo.retryDelay}.`, googleApiError, delaySeconds);
    }
    // 3. Check for short-term limits in QuotaFailure or ErrorInfo
    if (quotaFailure) {
        for (const violation of quotaFailure.violations) {
            const quotaId = violation.quotaId ?? '';
            if (quotaId.includes('PerMinute')) {
                return new RetryableQuotaError(`${googleApiError.message}\nSuggested retry after 60s.`, googleApiError, 60);
            }
        }
    }
    if (errorInfo) {
        const quotaLimit = errorInfo.metadata?.['quota_limit'] ?? '';
        if (quotaLimit.includes('PerMinute')) {
            return new RetryableQuotaError(`${errorInfo.reason}\nSuggested retry after 60s.`, googleApiError, 60);
        }
    }
    // If we reached this point, the status is 429, 499, or 503 and we have details,
    // but no specific violation was matched. We return a generic retryable error.
    return new RetryableQuotaError(errorMessage, googleApiError);
}
function extractErrorMessage(error) {
    if (typeof error === 'string') {
        return error;
    }
    if (typeof error === 'object' && error !== null && 'message' in error) {
        const msg = error.message;
        if (typeof msg === 'string') {
            return msg;
        }
    }
    return '';
}
//# sourceMappingURL=googleQuotaErrors.js.map