import axios, { AxiosInstance } from 'axios';
import { ExternalApiAdapter, TokenMetadata, MarketData } from './base-adapter';
import { preferGruV2OfficialDexPairs } from '../config/gru-v2-deployment-pools';
import { logger } from '../utils/logger';

export interface DexScreenerPair {
  chainId: string;
  dexId: string;
  url: string;
  pairAddress: string;
  baseToken: {
    address: string;
    name: string;
    symbol: string;
  };
  quoteToken: {
    address: string;
    name: string;
    symbol: string;
  };
  priceNative?: string;
  priceUsd?: string;
  txns?: {
    m5?: {
      buys?: number;
      sells?: number;
    };
    h1?: {
      buys?: number;
      sells?: number;
    };
    h6?: {
      buys?: number;
      sells?: number;
    };
    h24?: {
      buys?: number;
      sells?: number;
    };
  };
  volume?: {
    h24?: number;
    h6?: number;
    h1?: number;
    m5?: number;
  };
  priceChange?: {
    m5?: number;
    h1?: number;
    h6?: number;
    h24?: number;
  };
  liquidity?: {
    usd?: number;
    base?: number;
    quote?: number;
  };
  fdv?: number;
  pairCreatedAt?: number;
}

interface DexScreenerResponse {
  schemaVersion?: string;
  pairs?: DexScreenerPair[] | null;
  pair?: DexScreenerPair;
}

/** Current API returns a JSON array of pairs; older clients used `{ pairs: [...] }`. */
export function normalizeDexScreenerTokenPairsPayload(data: unknown): DexScreenerPair[] {
  if (Array.isArray(data)) {
    return data as DexScreenerPair[];
  }
  if (data && typeof data === 'object' && data !== null && 'pairs' in data) {
    const p = (data as DexScreenerResponse).pairs;
    return Array.isArray(p) ? p : [];
  }
  return [];
}

// Chain ID to DexScreener chain identifier mapping
// DexScreener uses chain identifiers like 'ethereum', 'bsc', etc.
const CHAIN_TO_DEXSCREENER_ID: Record<number, string> = {
  1: 'ethereum',
  56: 'bsc',
  137: 'polygon',
  43114: 'avalanche',
  42161: 'arbitrum',
  10: 'optimism',
  8453: 'base',
  100: 'gnosis',
  42220: 'celo',
  25: 'cronos',
  1111: 'wemix',
  // Chain 138 / ALL Mainnet: not listed on public DexScreener API
};

// Reverse mapping for lookup
const DEXSCREENER_ID_TO_CHAIN: Record<string, number> = {};
Object.entries(CHAIN_TO_DEXSCREENER_ID).forEach(([chainId, dexId]) => {
  DEXSCREENER_ID_TO_CHAIN[dexId] = parseInt(chainId, 10);
});

/** Prefer the pair with the most USD liquidity, then highest 24h volume (avoids averaging thin pools). */
function pairLiquidityScore(pair: DexScreenerPair): number {
  const liq = pair.liquidity?.usd ?? 0;
  if (liq > 0) return liq;
  return pair.volume?.h24 ?? 0;
}

/**
 * Aggregates DexScreener token-pairs response into a single {@link MarketData} snapshot.
 * Exported for unit tests.
 */
export function aggregateDexScreenerPairsToMarketData(pairs: DexScreenerPair[] | null | undefined): MarketData | null {
  if (!pairs || pairs.length === 0) {
    return null;
  }

  let totalVolume24h = 0;
  let totalLiquidity = 0;
  for (const pair of pairs) {
    if (pair.volume?.h24) totalVolume24h += pair.volume.h24;
    if (pair.liquidity?.usd) totalLiquidity += pair.liquidity.usd;
  }

  const priced = pairs.filter((p) => p.priceUsd && !Number.isNaN(parseFloat(p.priceUsd)));
  if (priced.length === 0) {
    return null;
  }

  let best = priced[0]!;
  let bestScore = pairLiquidityScore(best);
  for (let i = 1; i < priced.length; i++) {
    const p = priced[i]!;
    const s = pairLiquidityScore(p);
    if (s > bestScore) {
      best = p;
      bestScore = s;
    }
  }

  const priceUsd = parseFloat(best.priceUsd!);

  return {
    priceUsd: Number.isFinite(priceUsd) ? priceUsd : undefined,
    volume24h: totalVolume24h > 0 ? totalVolume24h : undefined,
    liquidityUsd: totalLiquidity > 0 ? totalLiquidity : undefined,
    lastUpdated: new Date(),
  };
}

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

  constructor() {
    this.apiKey = process.env.DEXSCREENER_API_KEY;
    this.api = axios.create({
      baseURL: 'https://api.dexscreener.com',
      timeout: 10000,
      headers: this.apiKey
        ? {
            Authorization: `Bearer ${this.apiKey}`,
          }
        : {},
    });
  }

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

  /**
   * Check if chain is supported by DexScreener
   */
  async checkChainSupport(chainId: number): Promise<boolean> {
    const dexId = CHAIN_TO_DEXSCREENER_ID[chainId];
    if (!dexId) {
      return false;
    }

    // Check cache
    const cacheKey = `chain_support_${chainId}`;
    const cached = this.cache.get(cacheKey);
    if (cached && cached.expiresAt > new Date()) {
      return cached.data as boolean;
    }

    // Try a test request to verify support
    try {
      // Use a known token address for testing (e.g., WETH on Ethereum)
      const testAddress = chainId === 1 ? '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' : '';
      if (!testAddress) {
        // For unknown chains, assume not supported unless proven otherwise
        return false;
      }

      const response = await this.api.get<unknown>(`/token-pairs/v1/${dexId}/${testAddress}`);

      const pairs = normalizeDexScreenerTokenPairsPayload(response.data);
      const supported = response.status === 200 && pairs.length > 0;
      this.cache.set(cacheKey, {
        data: supported,
        expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hour cache
      });

      if (supported) {
        this.supportedChains.add(chainId);
      }

      return supported;
    } catch (error: unknown) {
      const err = error as { response?: { status?: number } };
      if (err.response?.status === 404 || err.response?.status === 400) {
        return false;
      }
      logger.error(`Error checking DexScreener chain support for ${chainId}:`, error);
      return false;
    }
  }

  /**
   * Get token by contract address (DexScreener doesn't provide token metadata)
   */
  // eslint-disable-next-line @typescript-eslint/no-unused-vars -- interface requires (chainId, address)
  async getTokenByContract(_chainId: number, _address: string): Promise<TokenMetadata | null> {
    // DexScreener doesn't provide token metadata, only pair data
    return null;
  }

  /**
   * Get market data via token pairs
   */
  async getMarketData(chainId: number, address: string): Promise<MarketData | null> {
    const dexId = CHAIN_TO_DEXSCREENER_ID[chainId];
    if (!dexId) {
      return null; // Chain not supported
    }

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

    try {
      const response = await this.api.get<unknown>(`/token-pairs/v1/${dexId}/${address.toLowerCase()}`);

      const pairsRaw = normalizeDexScreenerTokenPairsPayload(response.data);
      if (pairsRaw.length === 0) {
        return null;
      }
      const pairs = preferGruV2OfficialDexPairs(chainId, address.toLowerCase(), pairsRaw);
      const marketData = aggregateDexScreenerPairsToMarketData(pairs);
      if (!marketData) {
        return null;
      }

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

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

  /**
   * Get all pairs for a token
   */
  async getTokenPairs(chainId: number, tokenAddress: string): Promise<DexScreenerPair[]> {
    const dexId = CHAIN_TO_DEXSCREENER_ID[chainId];
    if (!dexId) {
      return [];
    }

    try {
      const response = await this.api.get<unknown>(`/token-pairs/v1/${dexId}/${tokenAddress.toLowerCase()}`);

      const raw = normalizeDexScreenerTokenPairsPayload(response.data);
      return preferGruV2OfficialDexPairs(chainId, tokenAddress.toLowerCase(), raw);
    } catch (error) {
      logger.error(`Error fetching DexScreener pairs for ${tokenAddress} on chain ${chainId}:`, error);
      return [];
    }
  }

  /**
   * Get pair data by address
   */
  async getPairData(chainId: number, pairAddress: string): Promise<DexScreenerPair | null> {
    const dexId = CHAIN_TO_DEXSCREENER_ID[chainId];
    if (!dexId) {
      return null;
    }

    try {
      const response = await this.api.get<{ pair?: DexScreenerPair }>(
        `/latest/dex/pairs/${dexId}/${pairAddress.toLowerCase()}`
      );

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

  /**
   * Get pairs for multiple tokens (up to 30)
   */
  async getMultipleTokenPairs(
    chainId: number,
    tokenAddresses: string[]
  ): Promise<Record<string, DexScreenerPair[]>> {
    const dexId = CHAIN_TO_DEXSCREENER_ID[chainId];
    if (!dexId || tokenAddresses.length === 0) {
      return {};
    }

    // DexScreener limits to 30 addresses per request
    const chunks = [];
    for (let i = 0; i < tokenAddresses.length; i += 30) {
      chunks.push(tokenAddresses.slice(i, i + 30));
    }

    const results: Record<string, DexScreenerPair[]> = {};

    for (const chunk of chunks) {
      try {
        const response = await this.api.get<unknown>(
          `/tokens/v1/${dexId}/${chunk.map((addr) => addr.toLowerCase()).join(',')}`
        );

        const batchPairs = normalizeDexScreenerTokenPairsPayload(response.data);
        if (batchPairs.length > 0) {
          batchPairs.forEach((pair) => {
            const baseAddr = pair.baseToken.address.toLowerCase();
            const quoteAddr = pair.quoteToken.address.toLowerCase();

            if (chunk.includes(baseAddr)) {
              if (!results[baseAddr]) results[baseAddr] = [];
              results[baseAddr].push(pair);
            }
            if (chunk.includes(quoteAddr)) {
              if (!results[quoteAddr]) results[quoteAddr] = [];
              results[quoteAddr].push(pair);
            }
          });
        }
      } catch (error) {
        logger.error(`Error fetching DexScreener pairs for chunk on chain ${chainId}:`, error);
      }
    }

    return results;
  }
}
