/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import type {
  ErrorInfo,
  GoogleApiError,
  Help,
  QuotaFailure,
  RetryInfo,
} from './googleErrors.js';
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 {
  retryDelayMs?: number;

  constructor(
    message: string,
    override readonly cause: GoogleApiError,
    retryDelaySeconds?: number,
  ) {
    super(message);
    this.name = 'TerminalQuotaError';
    this.retryDelayMs = retryDelaySeconds
      ? retryDelaySeconds * 1000
      : undefined;
  }
}

/**
 * A retryable error indicating a temporary quota issue (e.g., per-minute limit).
 */
export class RetryableQuotaError extends Error {
  retryDelayMs?: number;

  constructor(
    message: string,
    override readonly cause: GoogleApiError,
    retryDelaySeconds?: number,
  ) {
    super(message);
    this.name = 'RetryableQuotaError';
    this.retryDelayMs = retryDelaySeconds
      ? retryDelaySeconds * 1000
      : undefined;
  }
}

/**
 * An error indicating that user validation is required to continue.
 */
export class ValidationRequiredError extends Error {
  validationLink?: string;
  validationDescription?: string;
  learnMoreUrl?: string;
  userHandled: boolean = false;

  constructor(
    message: string,
    override readonly cause?: GoogleApiError,
    validationLink?: string,
    validationDescription?: string,
    learnMoreUrl?: string,
  ) {
    super(message);
    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: string): number | null {
  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;
}

/**
 * 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 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: GoogleApiError,
): ValidationRequiredError | null {
  const errorInfo = googleApiError.details.find(
    (d): d is ErrorInfo =>
      d['@type'] === 'type.googleapis.com/google.rpc.ErrorInfo',
  );

  if (!errorInfo) {
    return null;
  }

  if (
    !CLOUDCODE_DOMAINS.includes(errorInfo.domain) ||
    errorInfo.reason !== 'VALIDATION_REQUIRED'
  ) {
    return null;
  }

  // Try to extract validation info from Help detail first
  const helpDetail = googleApiError.details.find(
    (d): d is Help => d['@type'] === 'type.googleapis.com/google.rpc.Help',
  );

  let validationLink: string | undefined;
  let validationDescription: string | undefined;
  let learnMoreUrl: string | undefined;

  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: unknown): unknown {
  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;
    }
  }

  if (
    !googleApiError ||
    googleApiError.code !== 429 ||
    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) {
        return new RetryableQuotaError(
          errorMessage,
          googleApiError ?? {
            code: 429,
            message: errorMessage,
            details: [],
          },
          retryDelaySeconds,
        );
      }
    } else if (status === 429) {
      // Fallback: If it is a 429 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: 429,
          message: errorMessage,
          details: [],
        },
      );
    }

    return error; // Not a 429 error we can handle with structured details or a parsable retry message.
  }

  const quotaFailure = googleApiError.details.find(
    (d): d is QuotaFailure =>
      d['@type'] === 'type.googleapis.com/google.rpc.QuotaFailure',
  );

  const errorInfo = googleApiError.details.find(
    (d): d is ErrorInfo =>
      d['@type'] === 'type.googleapis.com/google.rpc.ErrorInfo',
  );

  const retryInfo = googleApiError.details.find(
    (d): d is RetryInfo =>
      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) {
    // New Cloud Code API quota handling
    if (errorInfo.domain) {
      const validDomains = [
        'cloudcode-pa.googleapis.com',
        'staging-cloudcode-pa.googleapis.com',
        'autopush-cloudcode-pa.googleapis.com',
      ];
      if (validDomains.includes(errorInfo.domain)) {
        if (errorInfo.reason === 'RATE_LIMIT_EXCEEDED') {
          return new RetryableQuotaError(
            `${googleApiError.message}`,
            googleApiError,
            delaySeconds ?? 10,
          );
        }
        if (errorInfo.reason === 'QUOTA_EXHAUSTED') {
          return new TerminalQuotaError(
            `${googleApiError.message}`,
            googleApiError,
            delaySeconds,
          );
        }
      }
    }
  }

  // 2. Check for delays in RetryInfo
  if (retryInfo?.retryDelay && 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, we return retryable.
  if (status === 429) {
    const errorMessage =
      googleApiError?.message ||
      (error instanceof Error ? error.message : String(error));
    return new RetryableQuotaError(
      errorMessage,
      googleApiError ?? {
        code: 429,
        message: errorMessage,
        details: [],
      },
    );
  }
  return error; // Fallback to original error if no specific classification fits.
}
