import axios, { AxiosInstance } from 'axios';
import { ExternalApiAdapter, TokenMetadata, MarketData } from './base-adapter';
import { logger } from '../utils/logger';

interface CoinGeckoPlatform {
  id: string;
  chain_identifier: number;
  name: string;
  shortname: string;
}

interface CoinGeckoCoin {
  id: string;
  symbol: string;
  name: string;
  description?: {
    en?: string;
  };
  image?: {
    large?: string;
    small?: string;
    thumb?: string;
  };
  links?: {
    homepage?: string[];
    twitter_screen_name?: string;
    telegram_channel_identifier?: string;
    subreddit_url?: string;
    repos_url?: {
      github?: string[];
    };
  };
  market_data?: {
    current_price?: {
      usd?: number;
    };
    price_change_percentage_24h?: number;
    total_volume?: {
      usd?: number;
    };
    market_cap?: {
      usd?: number;
    };
  };
}

interface CoinGeckoMarket {
  id: string;
  symbol: string;
  name: string;
  image?: string;
  current_price?: number;
  price_change_percentage_24h?: number;
  total_volume?: number;
  market_cap?: number;
}

interface CoinGeckoTrending {
  coins: Array<{
    item: {
      id: string;
      name: string;
      symbol: string;
      thumb?: string;
      score?: number;
    };
  }>;
}

interface CoinGeckoMarketChartRangeResponse {
  prices?: Array<[number, number]>;
}

// Chain ID to CoinGecko platform ID mapping
const CHAIN_TO_PLATFORM: Record<number, string> = {
  1: 'ethereum',
  56: 'binance-smart-chain',
  137: 'polygon-pos',
  43114: 'avalanche',
  42161: 'arbitrum-one',
  10: 'optimistic-ethereum',
  8453: 'base',
  // Note: 138 and 651940 are likely not supported, will return null gracefully
};

const REFERENCE_SYMBOL_TO_COIN_ID: Record<string, string> = {
  ETH: 'ethereum',
  BTC: 'bitcoin',
  BNB: 'binancecoin',
  POL: 'matic-network',
  AVAX: 'avalanche-2',
  CELO: 'celo',
  CRO: 'crypto-com-chain',
  XDAI: 'xdai',
  LINK: 'chainlink',
};

export class CoinGeckoAdapter implements ExternalApiAdapter {
  private api: AxiosInstance;
  private apiKey?: string;
  private supportedPlatforms: Map<number, string> = new Map();
  private cache: Map<string, { data: unknown; expiresAt: Date }> = new Map();

  constructor() {
    this.apiKey = process.env.COINGECKO_API_KEY;
    const baseURL = this.apiKey
      ? 'https://pro-api.coingecko.com/api/v3'
      : 'https://api.coingecko.com/api/v3';

    this.api = axios.create({
      baseURL,
      timeout: 10000,
      headers: this.apiKey
        ? {
            'x-cg-pro-api-key': this.apiKey,
          }
        : {},
    });
  }

  getProviderName(): string {
    return 'coingecko';
  }

  /**
   * Check if chain is supported by CoinGecko
   */
  async checkChainSupport(chainId: number): Promise<boolean> {
    // Check cache first
    const cacheKey = `chain_support_${chainId}`;
    const cached = this.cache.get(cacheKey);
    if (cached && cached.expiresAt > new Date()) {
      return cached.data as boolean;
    }

    try {
      // Load supported platforms if not already loaded
      if (this.supportedPlatforms.size === 0) {
        await this.loadSupportedPlatforms();
      }

      const supported = this.supportedPlatforms.has(chainId);
      this.cache.set(cacheKey, {
        data: supported,
        expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hour cache
      });
      return supported;
    } catch (error) {
      logger.error(`Error checking CoinGecko chain support for ${chainId}:`, error);
      return false;
    }
  }

  /**
   * Load supported platforms from CoinGecko
   */
  private async loadSupportedPlatforms(): Promise<void> {
    try {
      const response = await this.api.get<CoinGeckoPlatform[]>('/asset_platforms');
      response.data.forEach((platform) => {
        if (platform.chain_identifier) {
          this.supportedPlatforms.set(platform.chain_identifier, platform.id);
        }
      });
    } catch (error) {
      logger.error('Error loading CoinGecko platforms:', error);
      // Fallback to known mappings
      Object.entries(CHAIN_TO_PLATFORM).forEach(([chainId, platformId]) => {
        this.supportedPlatforms.set(parseInt(chainId, 10), platformId);
      });
    }
  }

  /**
   * Get token by contract address
   */
  async getTokenByContract(chainId: number, address: string): Promise<TokenMetadata | null> {
    const platformId = this.supportedPlatforms.get(chainId);
    if (!platformId) {
      return null; // Chain not supported
    }

    const cacheKey = `token_${chainId}_${address.toLowerCase()}`;
    const cached = this.cache.get(cacheKey);
    if (cached && cached.expiresAt > new Date()) {
      return cached.data as TokenMetadata;
    }

    try {
      const response = await this.api.get<CoinGeckoCoin>(
        `/coins/${platformId}/contract/${address.toLowerCase()}`
      );

      const metadata: TokenMetadata = {
        id: response.data.id,
        name: response.data.name,
        symbol: response.data.symbol,
        description: response.data.description?.en,
        logoUrl: response.data.image?.large || response.data.image?.small,
        websiteUrl: response.data.links?.homepage?.[0],
        socialLinks: {
          twitter: response.data.links?.twitter_screen_name
            ? `https://twitter.com/${response.data.links.twitter_screen_name}`
            : undefined,
          telegram: response.data.links?.telegram_channel_identifier
            ? `https://t.me/${response.data.links.telegram_channel_identifier}`
            : undefined,
          github: response.data.links?.repos_url?.github?.[0],
        },
      };

      // Cache for 1 hour
      this.cache.set(cacheKey, {
        data: metadata,
        expiresAt: new Date(Date.now() + 60 * 60 * 1000),
      });

      return metadata;
    } catch (error: unknown) {
      const err = error as { response?: { status?: number } };
      if (err.response?.status === 404) {
        return null; // Token not found
      }
      logger.error(`Error fetching CoinGecko token ${address} on chain ${chainId}:`, error);
      return null;
    }
  }

  /**
   * Get market data for a token
   */
  async getMarketData(chainId: number, address: string): Promise<MarketData | null> {
    const platformId = this.supportedPlatforms.get(chainId);
    if (!platformId) {
      return null;
    }

    const cacheKey = `market_${chainId}_${address.toLowerCase()}`;
    const cached = this.cache.get(cacheKey);
    if (cached && cached.expiresAt > new Date()) {
      return cached.data as MarketData;
    }

    try {
      const response = await this.api.get<CoinGeckoCoin>(
        `/coins/${platformId}/contract/${address.toLowerCase()}`
      );

      const marketData: MarketData = {
        priceUsd: response.data.market_data?.current_price?.usd,
        priceChange24h: response.data.market_data?.price_change_percentage_24h,
        volume24h: response.data.market_data?.total_volume?.usd,
        marketCapUsd: response.data.market_data?.market_cap?.usd,
        lastUpdated: new Date(),
      };

      // Cache for 5 minutes
      this.cache.set(cacheKey, {
        data: marketData,
        expiresAt: new Date(Date.now() + 5 * 60 * 1000),
      });

      return marketData;
    } catch (error: unknown) {
      const err = error as { response?: { status?: number } };
      if (err.response?.status === 404) {
        return null;
      }
      logger.error(`Error fetching CoinGecko market data for ${address} on chain ${chainId}:`, error);
      return null;
    }
  }

  /**
   * Get trending tokens
   */
  async getTrending(): Promise<Array<{ id: string; name: string; symbol: string; score: number }>> {
    const cacheKey = 'trending';
    const cached = this.cache.get(cacheKey);
    if (cached && cached.expiresAt > new Date()) {
      return cached.data as Array<{ id: string; name: string; symbol: string; score: number }>;
    }

    try {
      const response = await this.api.get<CoinGeckoTrending>('/search/trending');
      const trending = response.data.coins.map((coin) => ({
        id: coin.item.id,
        name: coin.item.name,
        symbol: coin.item.symbol,
        score: coin.item.score || 0,
      }));

      // Cache for 10 minutes
      this.cache.set(cacheKey, {
        data: trending,
        expiresAt: new Date(Date.now() + 10 * 60 * 1000),
      });

      return trending;
    } catch (error) {
      logger.error('Error fetching CoinGecko trending:', error);
      return [];
    }
  }

  /**
   * Get market data for multiple tokens
   */
  async getMarkets(coinIds: string[]): Promise<CoinGeckoMarket[]> {
    if (coinIds.length === 0) return [];

    const cacheKey = `markets_${coinIds.join(',')}`;
    const cached = this.cache.get(cacheKey);
    if (cached && cached.expiresAt > new Date()) {
      return cached.data as CoinGeckoMarket[];
    }

    try {
      const response = await this.api.get<CoinGeckoMarket[]>('/coins/markets', {
        params: {
          vs_currency: 'usd',
          ids: coinIds.join(','),
          order: 'market_cap_desc',
          per_page: coinIds.length,
          page: 1,
        },
      });

      // Cache for 5 minutes
      this.cache.set(cacheKey, {
        data: response.data,
        expiresAt: new Date(Date.now() + 5 * 60 * 1000),
      });

      return response.data;
    } catch (error) {
      logger.error('Error fetching CoinGecko markets:', error);
      return [];
    }
  }

  /**
   * Spot USD price for canonical reference symbols (ETH, BTC, …) when the chain
   * is not on CoinGecko's contract API (e.g. Chain 138 native ETH / WETH proxy).
   */
  async getReferenceMarketData(referenceSymbol: string): Promise<MarketData | null> {
    const symbol = referenceSymbol.trim().toUpperCase();
    if (symbol === 'USD') {
      return { priceUsd: 1, lastUpdated: new Date() };
    }

    const coinId = REFERENCE_SYMBOL_TO_COIN_ID[symbol];
    if (!coinId) {
      return null;
    }

    const cacheKey = `reference_market_${coinId}`;
    const cached = this.cache.get(cacheKey);
    if (cached && cached.expiresAt > new Date()) {
      return cached.data as MarketData;
    }

    try {
      const response = await this.api.get<Record<string, { usd?: number }>>('/simple/price', {
        params: {
          ids: coinId,
          vs_currencies: 'usd',
          include_last_updated_at: true,
        },
      });

      const usd = response.data?.[coinId]?.usd;
      if (usd === undefined || !Number.isFinite(usd) || usd <= 0) {
        return null;
      }

      const marketData: MarketData = {
        priceUsd: usd,
        lastUpdated: new Date(),
      };

      this.cache.set(cacheKey, {
        data: marketData,
        expiresAt: new Date(Date.now() + 5 * 60 * 1000),
      });

      return marketData;
    } catch (error) {
      logger.error(`Error fetching CoinGecko reference market data for ${referenceSymbol}:`, error);
      return null;
    }
  }

  async getHistoricalReferencePrice(
    referenceSymbol: string,
    timestamp: Date
  ): Promise<{ priceUsd: number; effectiveTimestamp: Date } | null> {
    const symbol = referenceSymbol.trim().toUpperCase();
    const coinId = REFERENCE_SYMBOL_TO_COIN_ID[symbol];
    if (!coinId) {
      return null;
    }

    const from = Math.floor((timestamp.getTime() - (6 * 60 * 60 * 1000)) / 1000);
    const to = Math.floor((timestamp.getTime() + (6 * 60 * 60 * 1000)) / 1000);
    const cacheKey = `history_${coinId}_${from}_${to}`;
    const cached = this.cache.get(cacheKey);
    if (cached && cached.expiresAt > new Date()) {
      return cached.data as { priceUsd: number; effectiveTimestamp: Date } | null;
    }

    try {
      const response = await this.api.get<CoinGeckoMarketChartRangeResponse>(
        `/coins/${coinId}/market_chart/range`,
        {
          params: {
            vs_currency: 'usd',
            from,
            to,
          },
        }
      );

      const prices = response.data.prices || [];
      if (prices.length === 0) {
        return null;
      }

      const targetMs = timestamp.getTime();
      let bestPoint: [number, number] | null = null;
      let bestDistance = Number.POSITIVE_INFINITY;

      for (const point of prices) {
        if (point[0] > targetMs) {
          continue;
        }
        const distance = Math.abs(point[0] - targetMs);
        if (distance < bestDistance) {
          bestDistance = distance;
          bestPoint = point;
        }
      }

      if (!bestPoint || !Number.isFinite(bestPoint[1])) {
        return null;
      }

      const resolved = {
        priceUsd: bestPoint[1],
        effectiveTimestamp: new Date(bestPoint[0]),
      };
      this.cache.set(cacheKey, {
        data: resolved,
        expiresAt: new Date(Date.now() + 60 * 60 * 1000),
      });
      return resolved;
    } catch (error) {
      logger.error(`Error fetching CoinGecko historical reference price for ${referenceSymbol}:`, error);
      return null;
    }
  }
}
