/**
 * Error Normalizer
 *
 * Converts provider-specific errors (Brevo SDK errors, Mailchimp SDK errors,
 * Zoho axios errors) into a unified ProviderError type. All ProviderGateway
 * methods return ProviderResult<T> which uses ProviderError for error cases.
 *
 * Error mapping:
 *   401/403 → AUTH_FAILED (not retryable)
 *   404     → NOT_FOUND (not retryable)
 *   429     → RATE_LIMITED (retryable)
 *   5xx     → PROVIDER_ERROR (retryable)
 *   Network → CONNECTION_ERROR (retryable)
 *   400     → VALIDATION_ERROR (not retryable)
 */

import type { ProviderName } from './types';

// ============================================
// Error code union
// ============================================

export type ProviderErrorCode =
  | 'AUTH_FAILED'          // 401/403 — invalid credentials or expired token
  | 'RATE_LIMITED'         // 429 — too many requests
  | 'NOT_FOUND'            // 404 — resource not found
  | 'VALIDATION_ERROR'     // 400 — bad request / invalid parameters
  | 'CONNECTION_ERROR'     // Network / DNS / timeout
  | 'PROVIDER_ERROR'       // 5xx or unknown provider-side error
  | 'QUOTA_EXCEEDED'       // Plan limit reached
  | 'DUPLICATE_CONTACT'    // Contact already exists
  | 'WORKFLOW_NOT_FOUND'   // Automation/workflow not found
  | 'LIST_NOT_FOUND'       // List not found
  | 'UNKNOWN';             // Catch-all

// ============================================
// ProviderError class
// ============================================

export class ProviderError extends Error {
  public readonly provider: ProviderName;
  public readonly code: ProviderErrorCode;
  public readonly originalError: unknown;
  public readonly retryable: boolean;
  public readonly statusCode?: number;

  constructor(params: {
    provider: ProviderName;
    code: ProviderErrorCode;
    message: string;
    originalError?: unknown;
    retryable?: boolean;
    statusCode?: number;
  }) {
    super(params.message);
    this.name = 'ProviderError';
    this.provider = params.provider;
    this.code = params.code;
    this.originalError = params.originalError ?? null;
    this.retryable = params.retryable ?? false;
    this.statusCode = params.statusCode;
  }

  /**
   * Returns a plain object representation for logging/serialization.
   */
  toJSON(): Record<string, any> {
    return {
      name: this.name,
      provider: this.provider,
      code: this.code,
      message: this.message,
      retryable: this.retryable,
      statusCode: this.statusCode,
    };
  }
}

// ============================================
// Error normalization functions
// ============================================

/**
 * Extract HTTP status code from various error shapes.
 * Works with:
 *   - Brevo SDK errors (error.statusCode or error.response?.statusCode)
 *   - Mailchimp SDK errors (error.status)
 *   - Zoho axios errors (error.response?.status)
 *   - Generic Node errors (no status)
 */
function extractStatusCode(error: unknown): number | undefined {
  if (!error || typeof error !== 'object') return undefined;

  const err = error as Record<string, any>;

  // Brevo SDK: error.statusCode
  if (typeof err.statusCode === 'number') return err.statusCode;

  // Mailchimp SDK: error.status
  if (typeof err.status === 'number') return err.status;

  // Axios errors (Zoho): error.response?.status
  if (err.response && typeof err.response === 'object') {
    if (typeof err.response.status === 'number') return err.response.status;
  }

  return undefined;
}

/**
 * Extract error message from various error shapes.
 */
function extractMessage(error: unknown): string {
  if (error instanceof Error) return error.message;
  if (typeof error === 'string') return error;
  if (error && typeof error === 'object') {
    const err = error as Record<string, any>;
    if (typeof err.message === 'string') return err.message;
    if (typeof err.error === 'string') return err.error;
    if (err.response?.data?.message) return String(err.response.data.message);
  }
  return String(error);
}

/**
 * Detect if an error is a network/connection error.
 * No status code, no response — the request never reached the server.
 */
function isNetworkError(error: unknown): boolean {
  if (!error || typeof error !== 'object') return false;
  const err = error as Record<string, any>;

  // Axios network errors have code: 'ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND'
  if (typeof err.code === 'string') {
    const networkCodes = ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'EPIPE', 'ENETUNREACH'];
    if (networkCodes.includes(err.code)) return true;
  }

  // Fetch API network errors
  if (err.name === 'TypeError' && typeof err.message === 'string') {
    if (err.message.includes('fetch') || err.message.includes('network') || err.message.includes('ECONNREFUSED')) {
      return true;
    }
  }

  return false;
}

/**
 * Map an HTTP status code to a ProviderErrorCode.
 * Returns the code and whether it should be retried.
 */
function statusCodeToErrorCode(status: number): { code: ProviderErrorCode; retryable: boolean } {
  if (status === 401 || status === 403) return { code: 'AUTH_FAILED', retryable: false };
  if (status === 404) return { code: 'NOT_FOUND', retryable: false };
  if (status === 429) return { code: 'RATE_LIMITED', retryable: true };
  if (status >= 400 && status < 500) return { code: 'VALIDATION_ERROR', retryable: false };
  if (status >= 500) return { code: 'PROVIDER_ERROR', retryable: true };
  return { code: 'UNKNOWN', retryable: false };
}

/**
 * Normalize any error into a ProviderError.
 *
 * This is the main entry point for error normalization.
 * It inspects the error shape, extracts the HTTP status code,
 * and maps it to the appropriate ProviderErrorCode.
 *
 * Provider-specific heuristics:
 *   - Brevo: checks for duplicate contact errors in the message
 *   - Mailchimp: checks for "already subscribed" / "member exists" messages
 *   - Zoho: checks for Zoho API error format { code, message, status }
 */
export function normalizeProviderError(
  error: unknown,
  provider: ProviderName
): ProviderError {
  // Already a ProviderError — return as-is
  if (error instanceof ProviderError) {
    return error;
  }

  const message = extractMessage(error);
  const statusCode = extractStatusCode(error);

  // Network errors (no response from server)
  if (isNetworkError(error)) {
    return new ProviderError({
      provider,
      code: 'CONNECTION_ERROR',
      message: `Connection error: ${message}`,
      originalError: error,
      retryable: true,
    });
  }

  // No status code — classify by message content
  if (!statusCode) {
    // Check for known error patterns
    const lowerMessage = message.toLowerCase();

    if (lowerMessage.includes('duplicate') || lowerMessage.includes('already exists') ||
        lowerMessage.includes('already subscribed') || lowerMessage.includes('member exists')) {
      return new ProviderError({
        provider,
        code: 'DUPLICATE_CONTACT',
        message: `Contact already exists: ${message}`,
        originalError: error,
        retryable: false,
      });
    }

    if (lowerMessage.includes('not found') || lowerMessage.includes('does not exist')) {
      return new ProviderError({
        provider,
        code: 'NOT_FOUND',
        message,
        originalError: error,
        retryable: false,
      });
    }

    if (lowerMessage.includes('rate limit') || lowerMessage.includes('too many requests')) {
      return new ProviderError({
        provider,
        code: 'RATE_LIMITED',
        message: `Rate limited: ${message}`,
        originalError: error,
        retryable: true,
      });
    }

    if (lowerMessage.includes('unauthorized') || lowerMessage.includes('invalid api key') ||
        lowerMessage.includes('authentication') || lowerMessage.includes('invalid token')) {
      return new ProviderError({
        provider,
        code: 'AUTH_FAILED',
        message: `Authentication failed: ${message}`,
        originalError: error,
        retryable: false,
      });
    }

    // Unknown error with no status code
    return new ProviderError({
      provider,
      code: 'UNKNOWN',
      message,
      originalError: error,
      retryable: false,
    });
  }

  // Has a status code — map it
  const { code, retryable } = statusCodeToErrorCode(statusCode);

  // Additional heuristics based on status code + message
  if (statusCode === 400) {
    const lowerMessage = message.toLowerCase();
    if (lowerMessage.includes('duplicate') || lowerMessage.includes('already exists') ||
        lowerMessage.includes('already subscribed') || lowerMessage.includes('member exists')) {
      return new ProviderError({
        provider,
        code: 'DUPLICATE_CONTACT',
        message: `Contact already exists: ${message}`,
        originalError: error,
        retryable: false,
        statusCode,
      });
    }

    // Brevo-specific: "contact already exists" returns 400
    if (provider === 'brevo' && lowerMessage.includes('contact already exist')) {
      return new ProviderError({
        provider,
        code: 'DUPLICATE_CONTACT',
        message: `Contact already exists: ${message}`,
        originalError: error,
        retryable: false,
        statusCode,
      });
    }

    // Mailchimp-specific: "already subscribed" / "member exists"
    if (provider === 'mailchimp' && (lowerMessage.includes('already subscribed') || lowerMessage.includes('member exists'))) {
      return new ProviderError({
        provider,
        code: 'DUPLICATE_CONTACT',
        message: `Contact already exists: ${message}`,
        originalError: error,
        retryable: false,
        statusCode,
      });
    }
  }

  if (statusCode === 404) {
    const lowerMessage = message.toLowerCase();
    if (lowerMessage.includes('workflow') || lowerMessage.includes('automation')) {
      return new ProviderError({
        provider,
        code: 'WORKFLOW_NOT_FOUND',
        message: `Automation not found: ${message}`,
        originalError: error,
        retryable: false,
        statusCode,
      });
    }

    if (lowerMessage.includes('list') || lowerMessage.includes('audience')) {
      return new ProviderError({
        provider,
        code: 'LIST_NOT_FOUND',
        message: `List not found: ${message}`,
        originalError: error,
        retryable: false,
        statusCode,
      });
    }
  }

  return new ProviderError({
    provider,
    code,
    message,
    originalError: error,
    retryable,
    statusCode,
  });
}