import { ErrorClassification } from './types';

/**
 * Error types reported through the errorHandler in the client
 */
export enum ErrorType {
  NetworkUnexpectedHTTPCode,
  NetworkServerLimited,
  NetworkServerRejected,
  NetworkUnknown,

  JsonUnableToSerialize,
  JsonUnableToDeserialize,
  JsonUnknown,

  PluginError,

  InitializationError,
  ResetError,
  FlushError,
  EventsDropped,
}

/**
 * Segment Error object for ErrorHandler option
 */
export class SegmentError extends Error {
  type: ErrorType;
  message: string;
  innerError?: unknown;
  metadata?: Record<string, unknown>;

  constructor(
    type: ErrorType,
    message: string,
    innerError?: unknown,
    metadata?: Record<string, unknown>
  ) {
    super(message);
    Object.setPrototypeOf(this, SegmentError.prototype);
    this.type = type;
    this.message = message;
    this.innerError = innerError;
    this.metadata = metadata;
  }
}

/**
 * Custom Error type for Segment HTTP Error responses
 */
export class NetworkError extends SegmentError {
  statusCode: number;
  type:
    | ErrorType.NetworkServerLimited
    | ErrorType.NetworkServerRejected
    | ErrorType.NetworkUnexpectedHTTPCode
    | ErrorType.NetworkUnknown;

  constructor(statusCode: number, message: string, innerError?: unknown) {
    let type: ErrorType;
    if (statusCode === 429) {
      type = ErrorType.NetworkServerLimited;
    } else if (statusCode > 300 && statusCode < 400) {
      type = ErrorType.NetworkUnexpectedHTTPCode;
    } else if (statusCode >= 400) {
      type = ErrorType.NetworkServerRejected;
    } else {
      type = ErrorType.NetworkUnknown;
    }

    super(type, message, innerError);
    Object.setPrototypeOf(this, NetworkError.prototype);

    this.statusCode = statusCode;
    this.type = type;
  }
}

/**
 * Error type for JSON Serialization errors
 */
export class JSONError extends SegmentError {
  constructor(
    type: ErrorType.JsonUnableToDeserialize | ErrorType.JsonUnableToSerialize,
    message: string,
    innerError?: unknown
  ) {
    super(type, message, innerError);
    Object.setPrototypeOf(this, JSONError.prototype);
  }
}

/**
 * Utility method for handling HTTP fetch errors
 * @param response Fetch Response
 * @returns response if status OK, throws NetworkError for everything else
 */
export const checkResponseForErrors = (response: Response) => {
  if (!response.ok) {
    throw new NetworkError(response.status, response.statusText);
  }

  return response;
};

/**
 * Converts a .fetch() error to a SegmentError object for reporting to the error handler
 * @param error any JS error instance
 * @returns a SegmentError object
 */
export const translateHTTPError = (error: unknown): SegmentError => {
  // SegmentError already
  if (error instanceof SegmentError) {
    return error;
    // JSON Deserialization Errors
  } else if (error instanceof SyntaxError) {
    return new JSONError(
      ErrorType.JsonUnableToDeserialize,
      error.message,
      error
    );
    // HTTP Errors
  } else {
    const message =
      error instanceof Error
        ? error.message
        : typeof error === 'string'
        ? error
        : 'Unknown error';
    return new NetworkError(-1, message, error);
  }
};

/**
 * Classify an HTTP status code into rate_limit, transient, or permanent.
 *
 * Precedence:
 * 1. statusCodeOverrides — explicit per-code overrides
 * 2. 429 — rate limiting (if rateLimitEnabled !== false)
 * 3. default4xxBehavior / default5xxBehavior — range defaults
 * 4. Fallback — permanent (non-retryable)
 */
export const classifyError = (
  statusCode: number,
  config?: {
    default4xxBehavior?: 'drop' | 'retry';
    default5xxBehavior?: 'drop' | 'retry';
    statusCodeOverrides?: Record<string, 'drop' | 'retry'>;
    rateLimitEnabled?: boolean;
    backoffEnabled?: boolean;
  }
): ErrorClassification => {
  const override = config?.statusCodeOverrides?.[statusCode.toString()];
  if (override !== undefined) {
    if (override === 'retry') {
      // If the relevant config is disabled, treat retry overrides as permanent
      if (statusCode === 429 && config?.rateLimitEnabled === false) {
        return new ErrorClassification('permanent');
      }
      if (statusCode !== 429 && config?.backoffEnabled === false) {
        return new ErrorClassification('permanent');
      }
      return statusCode === 429
        ? new ErrorClassification('rate_limit')
        : new ErrorClassification('transient');
    }
    return new ErrorClassification('permanent');
  }

  if (statusCode === 429) {
    return config?.rateLimitEnabled !== false
      ? new ErrorClassification('rate_limit')
      : new ErrorClassification('permanent');
  }

  if (statusCode >= 400 && statusCode < 500) {
    const behavior = config?.default4xxBehavior ?? 'drop';
    if (behavior === 'retry' && config?.backoffEnabled === false) {
      return new ErrorClassification('permanent');
    }
    return new ErrorClassification(
      behavior === 'retry' ? 'transient' : 'permanent'
    );
  }

  if (statusCode >= 500 && statusCode < 600) {
    const behavior = config?.default5xxBehavior ?? 'retry';
    if (behavior === 'retry' && config?.backoffEnabled === false) {
      return new ErrorClassification('permanent');
    }
    return new ErrorClassification(
      behavior === 'retry' ? 'transient' : 'permanent'
    );
  }

  return new ErrorClassification('permanent');
};

/**
 * Parse Retry-After header value (seconds or HTTP-date format).
 * Returns delay in seconds clamped to maxRetryInterval, or undefined if invalid.
 */
export const parseRetryAfter = (
  retryAfterValue: string | null,
  maxRetryInterval = 300
): number | undefined => {
  if (retryAfterValue === null || retryAfterValue === '') return undefined;

  const asNumber = Number(retryAfterValue);
  if (Number.isFinite(asNumber)) {
    if (asNumber < 0) return undefined;
    return Math.min(asNumber, maxRetryInterval);
  }

  const retryDate = new Date(retryAfterValue);
  if (!isNaN(retryDate.getTime())) {
    const secondsUntil = Math.ceil((retryDate.getTime() - Date.now()) / 1000);
    return Math.min(Math.max(secondsUntil, 0), maxRetryInterval);
  }

  return undefined;
};
