/**
 * Base Carrier Connector
 *
 * Abstract base class for carrier API integrations
 * Provides common functionality: HTTP client, retry logic, circuit breaker, logging
 */

import { Logger } from '@nestjs/common';
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import CircuitBreaker from 'opossum';
import {
  CarrierConnectorPort,
  CarrierRateSearchInput,
  CarrierAvailabilityInput,
} from '@domain/ports/out/carrier-connector.port';
import { RateQuote } from '@domain/entities/rate-quote.entity';
import { CarrierTimeoutException } from '@domain/exceptions/carrier-timeout.exception';
import { CarrierUnavailableException } from '@domain/exceptions/carrier-unavailable.exception';

export interface CarrierConfig {
  name: string;
  code: string;
  baseUrl: string;
  timeout: number; // milliseconds
  maxRetries: number;
  circuitBreakerThreshold: number; // failure threshold before opening circuit
  circuitBreakerTimeout: number; // milliseconds to wait before half-open
}

export abstract class BaseCarrierConnector implements CarrierConnectorPort {
  protected readonly logger: Logger;
  protected readonly httpClient: AxiosInstance;
  protected readonly circuitBreaker: CircuitBreaker;

  constructor(protected readonly config: CarrierConfig) {
    this.logger = new Logger(`${config.name}Connector`);

    // Create HTTP client
    this.httpClient = axios.create({
      baseURL: config.baseUrl,
      timeout: config.timeout,
      headers: {
        'Content-Type': 'application/json',
        'User-Agent': 'Xpeditis/1.0',
      },
    });

    // Request interceptor
    this.httpClient.interceptors.request.use(
      request => {
        this.logger.debug(
          `Request: ${request.method?.toUpperCase()} ${request.url}`,
          request.data ? JSON.stringify(request.data).substring(0, 200) : ''
        );
        return request;
      },
      error => {
        this.logger.error(`Request error: ${error?.message || 'Unknown error'}`);
        return Promise.reject(error);
      }
    );

    // Response interceptor
    this.httpClient.interceptors.response.use(
      response => {
        this.logger.debug(`Response: ${response.status} ${response.statusText}`);
        return response;
      },
      error => {
        if (error?.code === 'ECONNABORTED') {
          this.logger.warn(`Request timeout after ${config.timeout}ms`);
          throw new CarrierTimeoutException(config.name, config.timeout);
        }
        this.logger.error(`Response error: ${error?.message || 'Unknown error'}`);
        return Promise.reject(error);
      }
    );

    // Circuit breaker
    this.circuitBreaker = new CircuitBreaker(this.makeRequest.bind(this), {
      timeout: config.timeout,
      errorThresholdPercentage: config.circuitBreakerThreshold,
      resetTimeout: config.circuitBreakerTimeout,
      name: `${config.name}-circuit-breaker`,
    });

    this.circuitBreaker.on('open', () =>
      this.logger.warn('Circuit breaker opened - carrier unavailable')
    );
    this.circuitBreaker.on('halfOpen', () =>
      this.logger.log('Circuit breaker half-open - testing carrier availability')
    );
    this.circuitBreaker.on('close', () =>
      this.logger.log('Circuit breaker closed - carrier available')
    );
  }

  getCarrierName(): string {
    return this.config.name;
  }

  getCarrierCode(): string {
    return this.config.code;
  }

  protected async makeRequest<T>(
    config: AxiosRequestConfig,
    retries = this.config.maxRetries
  ): Promise<AxiosResponse<T>> {
    try {
      return await this.httpClient.request<T>(config);
    } catch (error: any) {
      if (retries > 0 && this.isRetryableError(error)) {
        const delay = this.calculateRetryDelay(this.config.maxRetries - retries);
        this.logger.warn(`Request failed, retrying in ${delay}ms (${retries} retries left)`);
        await this.sleep(delay);
        return this.makeRequest<T>(config, retries - 1);
      }
      throw error;
    }
  }

  protected isRetryableError(error: any): boolean {
    if (error.code === 'ECONNABORTED') return false;
    if (error.code === 'ENOTFOUND') return false;
    if (error.response) {
      const status = error.response.status;
      return status >= 500 && status < 600;
    }
    return true;
  }

  protected calculateRetryDelay(attempt: number): number {
    const baseDelay = 1000;
    const maxDelay = 5000;
    const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
    return delay + Math.random() * 1000; // jitter
  }

  protected sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  protected async requestWithCircuitBreaker<T>(
    config: AxiosRequestConfig
  ): Promise<AxiosResponse<T>> {
    try {
      return (await this.circuitBreaker.fire(config)) as AxiosResponse<T>;
    } catch (error: any) {
      if (error?.message === 'Breaker is open') {
        throw new CarrierUnavailableException(this.config.name, 'Circuit breaker is open');
      }
      throw error;
    }
  }

  async healthCheck(): Promise<boolean> {
    try {
      await this.requestWithCircuitBreaker({ method: 'GET', url: '/health', timeout: 5000 });
      return true;
    } catch (error: any) {
      this.logger.warn(`Health check failed: ${error?.message || 'Unknown error'}`);
      return false;
    }
  }

  abstract searchRates(input: CarrierRateSearchInput): Promise<RateQuote[]>;
  abstract checkAvailability(input: CarrierAvailabilityInput): Promise<number>;
}
