/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { parseGoogleApiError, } from './googleErrors.js';
import { getErrorStatus, ModelNotFoundError } from './httpErrors.js';
/**
 * 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'] === 'type.googleapis.com/google.rpc.ErrorInfo');
    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'] === 'type.googleapis.com/google.rpc.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 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`.
 *
 * @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);
    if (status === 404) {
        const message = googleApiError?.message ||
            (error instanceof Error ? error.message : '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;
        }
    }
    // Check for 503 Service Unavailable errors
    if (status === 503) {
        const errorMessage = googleApiError?.message ||
            (error instanceof Error ? error.message : String(error));
        return new RetryableQuotaError(errorMessage, googleApiError ?? {
            code: 503,
            message: errorMessage,
            details: [],
        });
    }
    if (!googleApiError ||
        (googleApiError.code !== 429 && googleApiError.code !== 499) ||
        googleApiError.details.length === 0) {
        // Fallback: try to parse the error message for a retry delay
        const errorMessage = googleApiError?.message ||
            (error instanceof Error ? error.message : String(error));
        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) {
            // Fallback: If it is a 429 or 499 but doesn't have a specific "retry in" message,
            // assume it is a temporary rate limit and retry after 5 sec (same as DEFAULT_RETRY_OPTIONS).
            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'] === 'type.googleapis.com/google.rpc.QuotaFailure');
    const errorInfo = googleApiError.details.find((d) => d['@type'] === 'type.googleapis.com/google.rpc.ErrorInfo');
    const retryInfo = googleApiError.details.find((d) => d['@type'] === 'type.googleapis.com/google.rpc.RetryInfo');
    // 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 and the status is still 429 or 499, we return retryable.
    if (status === 429 || status === 499) {
        const errorMessage = googleApiError?.message ||
            (error instanceof Error ? error.message : String(error));
        return new RetryableQuotaError(errorMessage, googleApiError ?? {
            code: status,
            message: errorMessage,
            details: [],
        });
    }
    return error; // Fallback to original error if no specific classification fits.
}
//# sourceMappingURL=googleQuotaErrors.js.map