import axios from 'axios';
import BigNumber from 'bignumber.js';

import logger from '../logger';
import { getTokenInfo } from './token-address-mapping';
import type { ExchangeRateData, IExchangeRateProvider, ProviderConfig } from './types';
import { SymbolNotSupportedError } from './types';

/**
 * CoinGecko Exchange Rate Provider
 *
 * Fetches token prices from CoinGecko API
 * Uses the /simple/token_price/ethereum endpoint which supports contract addresses
 * Uses Ethereum mainnet (chainId: 1) contract addresses
 */
export class CoinGeckoProvider implements IExchangeRateProvider {
  public readonly name = 'coingecko';

  private baseUrl: string;
  private timeout: number;
  private apiKey?: string;
  private zeroAddress = '0x0000000000000000000000000000000000000000';

  constructor(config: ProviderConfig = {}) {
    this.baseUrl = config.base_url || 'https://api.coingecko.com/api/v3';
    this.timeout = config.timeout || 10000; // 10 seconds default
    this.apiKey = config.api_key;
  }

  async fetch(symbol: string): Promise<ExchangeRateData> {
    try {
      // Get token info from mapping
      const tokenInfo = getTokenInfo(symbol);
      if (!tokenInfo) {
        // Symbol not in mapping - this is a data issue, not a service issue
        throw new SymbolNotSupportedError(symbol, this.name);
      }

      const { address, coinGeckoId } = tokenInfo;
      const addressLower = address?.toLowerCase();
      const useTokenPriceEndpoint = !!address && addressLower !== this.zeroAddress;

      if (!useTokenPriceEndpoint) {
        return await this.fetchByCoinId(symbol, coinGeckoId);
      }

      // Use contract address endpoint for Ethereum tokens
      const url = `${this.baseUrl}/simple/token_price/ethereum`;
      const params: any = {
        contract_addresses: address,
        // IMPORTANT: Must be 'usd' because the entire dynamic pricing system uses USD as the sole base currency
        // All exchange rates are defined as "USD per Token" (e.g., 1 ABT = 0.258 USD)
        // This is a P0 architectural constraint - see plan.md "USD 是唯一价值锚点"
        vs_currencies: 'usd',
        include_last_updated_at: true,
      };

      // Add API key if configured (for rate limit increase)
      if (this.apiKey) {
        params.x_cg_pro_api_key = this.apiKey;
      }

      logger.debug('Fetching exchange rate from CoinGecko', { symbol, address, url });

      const response = await axios.get(url, {
        params,
        timeout: this.timeout,
        headers: {
          'User-Agent': 'PaymentKit/1.0',
          Accept: 'application/json',
        },
      });

      if (!response.data || typeof response.data !== 'object') {
        throw new Error('Invalid response format from CoinGecko');
      }

      // CoinGecko returns data keyed by lowercase address
      const tokenData = response.data[addressLower];

      if (!tokenData) {
        if (coinGeckoId) {
          return await this.fetchByCoinId(symbol, coinGeckoId);
        }
        throw new SymbolNotSupportedError(symbol, this.name);
      }

      return this.buildRateResult(tokenData.usd, tokenData.last_updated_at, tokenInfo);
    } catch (error: any) {
      // Log the error
      logger.error('Failed to fetch exchange rate from CoinGecko', {
        symbol,
        error: error.message,
        response: error.response?.data,
      });

      // Determine error type and throw appropriate error
      // Re-throw SymbolNotSupportedError as-is (don't wrap it)
      if (error instanceof SymbolNotSupportedError) {
        throw error;
      }
      if (error.response) {
        if (error.response.status === 404) {
          throw new SymbolNotSupportedError(symbol, this.name);
        } else if (error.response.status === 429) {
          throw new Error('CoinGecko rate limit exceeded');
        } else if (error.response.status >= 500) {
          throw new Error(`CoinGecko server error: ${error.response.status}`);
        } else {
          throw new Error(`CoinGecko request failed: ${error.response.status}`);
        }
      } else if (error.code === 'ECONNABORTED') {
        throw new Error(`CoinGecko request timeout after ${this.timeout}ms`);
      } else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
        throw new Error(`Cannot connect to CoinGecko: ${error.message}`);
      } else {
        throw new Error(`CoinGecko fetch error: ${error.message}`);
      }
    }
  }

  private async fetchByCoinId(symbol: string, coinGeckoId?: string): Promise<ExchangeRateData> {
    if (!coinGeckoId) {
      throw new SymbolNotSupportedError(symbol, this.name, `CoinGecko ID is missing for ${symbol}`);
    }

    const url = `${this.baseUrl}/simple/price`;
    const params: any = {
      ids: coinGeckoId,
      vs_currencies: 'usd',
      include_last_updated_at: true,
    };

    if (this.apiKey) {
      params.x_cg_pro_api_key = this.apiKey;
    }

    logger.debug('Fetching exchange rate from CoinGecko by coin id', { symbol, coinGeckoId, url });

    const response = await axios.get(url, {
      params,
      timeout: this.timeout,
      headers: {
        'User-Agent': 'PaymentKit/1.0',
        Accept: 'application/json',
      },
    });

    if (!response.data || typeof response.data !== 'object') {
      throw new Error('Invalid response format from CoinGecko');
    }

    const tokenData = response.data[coinGeckoId];
    if (!tokenData) {
      throw new SymbolNotSupportedError(symbol, this.name);
    }

    const tokenInfo = getTokenInfo(symbol);
    if (!tokenInfo) {
      throw new SymbolNotSupportedError(symbol, this.name);
    }

    return this.buildRateResult(tokenData.usd, tokenData.last_updated_at, tokenInfo);
  }

  private buildRateResult(
    usdPrice: number,
    lastUpdatedAt: number | undefined,
    tokenInfo: ReturnType<typeof getTokenInfo>
  ): ExchangeRateData {
    if (!usdPrice || typeof usdPrice !== 'number') {
      throw new Error(`No valid USD price found for ${tokenInfo?.symbol || 'token'}`);
    }

    const rate = new BigNumber(usdPrice);
    if (rate.lte(0) || !rate.isFinite()) {
      throw new Error(`Invalid price value: ${usdPrice}`);
    }

    const timestampMs = lastUpdatedAt ? lastUpdatedAt * 1000 : Date.now();

    return {
      rate: rate.toString(),
      timestamp_ms: timestampMs,
      metadata: {
        source: 'coingecko',
        address: tokenInfo?.address,
        symbol: tokenInfo?.symbol,
        coin_id: tokenInfo?.coinGeckoId,
      },
    };
  }
}
