import { Logger } from '@nestjs/common';
import axios, {
  AxiosInstance,
  AxiosRequestConfig,
  AxiosResponse,
  AxiosError,
  InternalAxiosRequestConfig,
} from 'axios';
import {
  IntegrationConnectionError,
  IntegrationRateLimitError,
  IntegrationApiError,
  IntegrationAuthError,
} from '../errors';

/**
 * Configuration options for the base connector
 */
export interface BaseConnectorConfig {
  /** Base URL for the API */
  baseUrl: string;
  /** Request timeout in milliseconds (default: 30000) */
  timeout?: number;
  /** Maximum number of retry attempts (default: 3) */
  maxRetries?: number;
  /** Initial delay in milliseconds for retry backoff (default: 1000) */
  retryDelay?: number;
  /** Maximum delay in milliseconds for retry backoff (default: 30000) */
  maxRetryDelay?: number;
  /** Enable request/response logging (default: true in development) */
  enableLogging?: boolean;
}

/**
 * Retry configuration for individual requests
 */
export interface RetryConfig {
  /** Maximum number of retry attempts */
  maxRetries?: number;
  /** Initial delay in milliseconds */
  initialDelay?: number;
  /** Maximum delay in milliseconds */
  maxDelay?: number;
  /** Retry on these status codes (default: 429, 500, 502, 503, 504) */
  retryOnStatuses?: number[];
}

/**
 * Connection test result
 */
export interface ConnectionTestResult {
  success: boolean;
  message: string;
  latencyMs?: number;
  details?: Record<string, unknown>;
}

/**
 * Default retry status codes
 */
const DEFAULT_RETRY_STATUSES = [429, 500, 502, 503, 504];

/**
 * Abstract base class for all API connectors.
 * Provides common functionality for HTTP requests, retry logic, rate limiting, and error handling.
 */
export abstract class BaseConnector {
  /** Name of the integration for logging and error reporting */
  protected abstract readonly name: string;

  /** Logger instance */
  protected readonly logger: Logger;

  /** Axios HTTP client instance */
  protected httpClient: AxiosInstance;

  /** Connector configuration */
  protected config: Required<BaseConnectorConfig>;

  constructor(config?: BaseConnectorConfig) {
    // Logger will be initialized with the concrete class name
    this.logger = new Logger(this.constructor.name);

    if (config?.baseUrl) {
      this.reconfigure(config);
    }
  }

  /**
   * Create or recreate the HTTP client with the given configuration.
   * Call this when credentials are loaded from DB or updated.
   */
  protected reconfigure(config: BaseConnectorConfig): void {
    this.config = {
      timeout: 30000,
      maxRetries: 3,
      retryDelay: 1000,
      maxRetryDelay: 30000,
      enableLogging: process.env.NODE_ENV === 'development',
      ...config,
    };

    // Create axios instance with base configuration
    this.httpClient = axios.create({
      baseURL: this.config.baseUrl,
      timeout: this.config.timeout,
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
      },
    });

    // Setup interceptors on the new client
    this.setupRequestInterceptor();
    this.setupResponseInterceptor();
  }

  /**
   * Test the connection to the external service
   * Must be implemented by concrete connectors
   */
  abstract testConnection(): Promise<ConnectionTestResult>;

  /**
   * Get authentication headers for requests
   * Must be implemented by concrete connectors
   */
  abstract getAuthHeaders(): Promise<Record<string, string>>;

  /**
   * Check if the connector is properly configured
   */
  abstract isConfigured(): boolean;

  /**
   * Setup request interceptor for logging and authentication
   */
  protected setupRequestInterceptor(): void {
    this.httpClient.interceptors.request.use(
      async (config: InternalAxiosRequestConfig) => {
        // Add authentication headers
        try {
          const authHeaders = await this.getAuthHeaders();
          Object.assign(config.headers, authHeaders);
        } catch (error) {
          this.logger.error('Failed to get auth headers', error);
          throw error;
        }

        // Log outgoing request
        if (this.config.enableLogging) {
          this.logger.debug(
            `[${this.name}] ${config.method?.toUpperCase()} ${config.url}`,
          );
        }

        // Add request timestamp for latency calculation
        (config as InternalAxiosRequestConfig & { metadata?: { startTime: number } }).metadata = {
          startTime: Date.now(),
        };

        return config;
      },
      (error: AxiosError) => {
        this.logger.error(`[${this.name}] Request setup failed`, error.message);
        return Promise.reject(error);
      },
    );
  }

  /**
   * Setup response interceptor for logging and error transformation
   */
  protected setupResponseInterceptor(): void {
    this.httpClient.interceptors.response.use(
      (response: AxiosResponse) => {
        const config = response.config as InternalAxiosRequestConfig & { metadata?: { startTime: number } };
        const latency = config.metadata
          ? Date.now() - config.metadata.startTime
          : 0;

        if (this.config.enableLogging) {
          this.logger.debug(
            `[${this.name}] Response ${response.status} (${latency}ms)`,
          );
        }

        return response;
      },
      (error: AxiosError) => {
        return this.handleResponseError(error);
      },
    );
  }

  /**
   * Transform axios errors into integration-specific errors
   */
  protected handleResponseError(error: AxiosError): Promise<never> {
    const status = error.response?.status;
    const message = this.extractErrorMessage(error);

    // Connection errors (no response)
    if (!error.response) {
      if (error.code === 'ECONNREFUSED') {
        throw new IntegrationConnectionError(
          this.name,
          'Connection refused',
          error,
        );
      }
      if (error.code === 'ETIMEDOUT' || error.code === 'ECONNABORTED') {
        throw new IntegrationConnectionError(
          this.name,
          'Request timeout',
          error,
        );
      }
      if (error.code === 'ENOTFOUND') {
        throw new IntegrationConnectionError(
          this.name,
          'Host not found',
          error,
        );
      }
      throw new IntegrationConnectionError(
        this.name,
        error.message || 'Unknown connection error',
        error,
      );
    }

    // Authentication errors
    if (status === 401) {
      throw new IntegrationAuthError(this.name, message, error);
    }

    // Rate limiting
    if (status === 429) {
      const retryAfter = this.extractRetryAfter(error);
      throw new IntegrationRateLimitError(this.name, message, retryAfter, error);
    }

    // General API errors
    throw new IntegrationApiError(
      this.name,
      message,
      status,
      this.extractErrorCode(error),
      error,
    );
  }

  /**
   * Extract error message from axios error response
   */
  protected extractErrorMessage(error: AxiosError): string {
    const data = error.response?.data as Record<string, unknown> | undefined;
    if (data?.message) {
      return String(data.message);
    }
    if (data?.error) {
      return String(data.error);
    }
    if (data?.error_description) {
      return String(data.error_description);
    }
    return error.message || 'Unknown error';
  }

  /**
   * Extract error code from axios error response
   */
  protected extractErrorCode(error: AxiosError): string | undefined {
    const data = error.response?.data as Record<string, unknown> | undefined;
    if (data?.code) {
      return String(data.code);
    }
    if (data?.error_code) {
      return String(data.error_code);
    }
    return undefined;
  }

  /**
   * Extract retry-after header value in seconds
   */
  protected extractRetryAfter(error: AxiosError): number | undefined {
    const retryAfter = error.response?.headers?.['retry-after'];
    if (!retryAfter) return undefined;

    const seconds = parseInt(retryAfter, 10);
    if (!isNaN(seconds)) return seconds;

    // Try to parse as date
    const date = new Date(retryAfter);
    if (!isNaN(date.getTime())) {
      return Math.ceil((date.getTime() - Date.now()) / 1000);
    }

    return undefined;
  }

  /**
   * Execute a request with automatic retry logic
   */
  protected async executeWithRetry<T>(
    requestFn: () => Promise<AxiosResponse<T>>,
    retryConfig?: RetryConfig,
  ): Promise<T> {
    const config: Required<RetryConfig> = {
      maxRetries: this.config.maxRetries,
      initialDelay: this.config.retryDelay,
      maxDelay: this.config.maxRetryDelay,
      retryOnStatuses: DEFAULT_RETRY_STATUSES,
      ...retryConfig,
    };

    let lastError: Error | null = null;

    for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
      try {
        const response = await requestFn();
        return response.data;
      } catch (error) {
        lastError = error as Error;

        // Check if we should retry
        if (!this.shouldRetry(error, attempt, config)) {
          throw error;
        }

        // Calculate delay with exponential backoff and jitter
        const delay = this.calculateRetryDelay(attempt, config);

        this.logger.warn(
          `[${this.name}] Request failed (attempt ${attempt + 1}/${config.maxRetries + 1}). ` +
          `Retrying in ${delay}ms...`,
        );

        await this.sleep(delay);
      }
    }

    throw lastError;
  }

  /**
   * Determine if a request should be retried
   */
  private shouldRetry(
    error: unknown,
    attempt: number,
    config: Required<RetryConfig>,
  ): boolean {
    if (attempt >= config.maxRetries) {
      return false;
    }

    // Check if error is retryable
    if (error instanceof IntegrationRateLimitError) {
      return true;
    }

    if (error instanceof IntegrationConnectionError) {
      return true;
    }

    if (error instanceof IntegrationApiError) {
      return config.retryOnStatuses.includes(error.apiStatusCode || 0);
    }

    // Check axios error status
    if (axios.isAxiosError(error) && error.response?.status) {
      return config.retryOnStatuses.includes(error.response.status);
    }

    return false;
  }

  /**
   * Calculate retry delay with exponential backoff and jitter
   */
  private calculateRetryDelay(
    attempt: number,
    config: Required<RetryConfig>,
  ): number {
    // Exponential backoff: initialDelay * 2^attempt
    const exponentialDelay = config.initialDelay * Math.pow(2, attempt);

    // Apply jitter (0.5 to 1.5 multiplier)
    const jitter = 0.5 + Math.random();
    const delayWithJitter = exponentialDelay * jitter;

    // Cap at max delay
    return Math.min(delayWithJitter, config.maxDelay);
  }

  /**
   * Sleep for specified milliseconds
   */
  protected sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  /**
   * Make a GET request with retry logic
   */
  protected async get<T>(
    url: string,
    config?: AxiosRequestConfig,
    retryConfig?: RetryConfig,
  ): Promise<T> {
    return this.executeWithRetry(
      () => this.httpClient.get<T>(url, config),
      retryConfig,
    );
  }

  /**
   * Make a POST request with retry logic
   */
  protected async post<T>(
    url: string,
    data?: unknown,
    config?: AxiosRequestConfig,
    retryConfig?: RetryConfig,
  ): Promise<T> {
    return this.executeWithRetry(
      () => this.httpClient.post<T>(url, data, config),
      retryConfig,
    );
  }

  /**
   * Make a PUT request with retry logic
   */
  protected async put<T>(
    url: string,
    data?: unknown,
    config?: AxiosRequestConfig,
    retryConfig?: RetryConfig,
  ): Promise<T> {
    return this.executeWithRetry(
      () => this.httpClient.put<T>(url, data, config),
      retryConfig,
    );
  }

  /**
   * Make a PATCH request with retry logic
   */
  protected async patch<T>(
    url: string,
    data?: unknown,
    config?: AxiosRequestConfig,
    retryConfig?: RetryConfig,
  ): Promise<T> {
    return this.executeWithRetry(
      () => this.httpClient.patch<T>(url, data, config),
      retryConfig,
    );
  }

  /**
   * Make a DELETE request with retry logic
   */
  protected async delete<T>(
    url: string,
    config?: AxiosRequestConfig,
    retryConfig?: RetryConfig,
  ): Promise<T> {
    return this.executeWithRetry(
      () => this.httpClient.delete<T>(url, config),
      retryConfig,
    );
  }
}
