import { ethers } from 'ethers';
import { getChainConfig } from '../config/chains';
import { getCanonicalPriceUsd } from './canonical-price-oracle';
import { resolveCanonicalQuoteAddress } from '../config/canonical-tokens';
import { TokenRepository } from '../database/repositories/token-repo';
import { PoolRepository, type LiquidityPool } from '../database/repositories/pool-repo';
import { MarketDataRepository } from '../database/repositories/market-data-repo';
import { SwapEventRepository, type SwapEventRecord } from '../database/repositories/swap-event-repo';
import { OHLCVGenerator, type OHLCVInterval } from '../indexer/ohlcv-generator';
import { logger } from '../utils/logger';

const ERC20_METADATA_ABI = [
  'function decimals() view returns (uint8)',
  'function symbol() view returns (string)',
] as const;

const UNISWAP_V2_SWAP_IFACE = new ethers.Interface([
  'event Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to)',
]);

const UNISWAP_V3_SWAP_IFACE = new ethers.Interface([
  'event Swap(address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick)',
]);

const DODO_SWAP_IFACE = new ethers.Interface([
  'event DODOSwap(address fromToken, address toToken, uint256 fromAmount, uint256 toAmount, address trader, address receiver)',
]);

type BackfillOptions = {
  chainId: number;
  days?: number;
  chunkSize?: number;
  poolLimit?: number;
};

type TokenMetadata = {
  address: string;
  decimals: number;
  symbol?: string;
};

type BackfillSummary = {
  chainId: number;
  poolsScanned: number;
  poolsProcessed: number;
  swapsUpserted: number;
  ohlcvTokensGenerated: number;
  fromBlock: number;
  toBlock: number;
  days: number;
};

export class HistoricalPricingBackfillService {
  private tokenRepo = new TokenRepository();
  private poolRepo = new PoolRepository();
  private marketDataRepo = new MarketDataRepository();
  private swapEventRepo = new SwapEventRepository();
  private ohlcvGenerator = new OHLCVGenerator();
  private providerCache = new Map<number, ethers.JsonRpcProvider>();
  private tokenMetadataCache = new Map<string, TokenMetadata>();
  private anchorPriceCache = new Map<string, number | undefined>();
  private blockTimestampCache = new Map<string, Date>();

  async backfillChain(options: BackfillOptions): Promise<BackfillSummary> {
    const chainId = options.chainId;
    const days = Math.max(1, options.days ?? 30);
    const chunkSize = Math.max(100, options.chunkSize ?? 2500);
    const config = getChainConfig(chainId);
    if (!config) {
      throw new Error(`Chain ${chainId} is not configured`);
    }

    const provider = this.getProvider(chainId);
    const currentBlock = await provider.getBlockNumber();
    const blocksPerDay = Math.max(1, Math.ceil((24 * 60 * 60) / Math.max(config.blockTime, 1)));
    const defaultFromBlock = Math.max(0, currentBlock - (blocksPerDay * days));
    const pools = await this.poolRepo.getPoolsByChain(chainId, options.poolLimit ?? 500);
    const tokenAddresses = new Set<string>();

    let poolsProcessed = 0;
    let swapsUpserted = 0;

    for (const pool of pools) {
      tokenAddresses.add(pool.token0Address.toLowerCase());
      tokenAddresses.add(pool.token1Address.toLowerCase());

      if (!this.isSupportedDexType(pool.dexType)) {
        continue;
      }

      const fromBlock = Math.max(defaultFromBlock, Number(pool.createdAtBlock || 0));
      const count = await this.backfillPool(pool, fromBlock, currentBlock, chunkSize);
      if (count > 0) {
        poolsProcessed += 1;
        swapsUpserted += count;
      }
    }

    const now = new Date();
    const from = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
    let ohlcvTokensGenerated = 0;
    const intervals: OHLCVInterval[] = ['5m', '15m', '1h', '4h', '24h'];

    for (const tokenAddress of tokenAddresses) {
      for (const interval of intervals) {
        await this.ohlcvGenerator.generateAndStore(chainId, tokenAddress, interval, from, now);
      }
      ohlcvTokensGenerated += 1;
    }

    return {
      chainId,
      poolsScanned: pools.length,
      poolsProcessed,
      swapsUpserted,
      ohlcvTokensGenerated,
      fromBlock: defaultFromBlock,
      toBlock: currentBlock,
      days,
    };
  }

  private isSupportedDexType(dexType: string): boolean {
    return ['uniswap_v2', 'sushiswap', 'uniswap_v3', 'dodo'].includes(dexType);
  }

  private getProvider(chainId: number): ethers.JsonRpcProvider {
    const cached = this.providerCache.get(chainId);
    if (cached) {
      return cached;
    }

    const config = getChainConfig(chainId);
    if (!config) {
      throw new Error(`Chain ${chainId} is not configured`);
    }

    const provider = new ethers.JsonRpcProvider(config.rpcUrl);
    this.providerCache.set(chainId, provider);
    return provider;
  }

  private async backfillPool(
    pool: LiquidityPool,
    fromBlock: number,
    toBlock: number,
    chunkSize: number
  ): Promise<number> {
    let count = 0;
    const provider = this.getProvider(pool.chainId);

    for (let start = fromBlock; start <= toBlock; start += chunkSize) {
      const end = Math.min(toBlock, start + chunkSize - 1);
      const logs = await this.fetchPoolLogs(pool, provider, start, end);

      for (const log of logs) {
        const event = await this.toSwapEventRecord(pool, log, provider);
        if (!event) {
          continue;
        }

        await this.swapEventRepo.upsertSwapEvent(event);
        count += 1;
      }
    }

    logger.info('Historical pricing pool backfill complete', {
      chainId: pool.chainId,
      poolAddress: pool.poolAddress,
      dexType: pool.dexType,
      fromBlock,
      toBlock,
      swapsUpserted: count,
    });

    return count;
  }

  private async fetchPoolLogs(
    pool: LiquidityPool,
    provider: ethers.JsonRpcProvider,
    fromBlock: number,
    toBlock: number
  ): Promise<ethers.Log[]> {
    let iface: ethers.Interface;
    let topic: string;

    switch (pool.dexType) {
      case 'uniswap_v2':
      case 'sushiswap':
        iface = UNISWAP_V2_SWAP_IFACE;
        topic = UNISWAP_V2_SWAP_IFACE.getEvent('Swap')!.topicHash;
        break;
      case 'uniswap_v3':
        iface = UNISWAP_V3_SWAP_IFACE;
        topic = UNISWAP_V3_SWAP_IFACE.getEvent('Swap')!.topicHash;
        break;
      case 'dodo':
        iface = DODO_SWAP_IFACE;
        topic = DODO_SWAP_IFACE.getEvent('DODOSwap')!.topicHash;
        break;
      default:
        return [];
    }

    try {
      return await provider.getLogs({
        address: pool.poolAddress,
        fromBlock,
        toBlock,
        topics: [topic],
      });
    } catch (error) {
      logger.warn('Historical pricing log fetch failed for pool chunk', {
        chainId: pool.chainId,
        poolAddress: pool.poolAddress,
        dexType: pool.dexType,
        fromBlock,
        toBlock,
        error,
      });
      return [];
    }
  }

  private async toSwapEventRecord(
    pool: LiquidityPool,
    log: ethers.Log,
    provider: ethers.JsonRpcProvider
  ): Promise<SwapEventRecord | null> {
    try {
      if (pool.dexType === 'uniswap_v2' || pool.dexType === 'sushiswap') {
        const parsed = UNISWAP_V2_SWAP_IFACE.parseLog(log)!;
        const amount0In = BigInt(parsed.args.amount0In.toString());
        const amount1In = BigInt(parsed.args.amount1In.toString());
        const amount0Out = BigInt(parsed.args.amount0Out.toString());
        const amount1Out = BigInt(parsed.args.amount1Out.toString());
        return await this.buildSwapEventRecord(pool, log, provider, {
          amount0In,
          amount1In,
          amount0Out,
          amount1Out,
          sender: String(parsed.args.sender),
          toAddress: String(parsed.args.to),
        });
      }

      if (pool.dexType === 'uniswap_v3') {
        const parsed = UNISWAP_V3_SWAP_IFACE.parseLog(log)!;
        const amount0Signed = BigInt(parsed.args.amount0.toString());
        const amount1Signed = BigInt(parsed.args.amount1.toString());
        return await this.buildSwapEventRecord(pool, log, provider, {
          amount0In: amount0Signed > 0n ? amount0Signed : 0n,
          amount1In: amount1Signed > 0n ? amount1Signed : 0n,
          amount0Out: amount0Signed < 0n ? -amount0Signed : 0n,
          amount1Out: amount1Signed < 0n ? -amount1Signed : 0n,
          sender: String(parsed.args.sender),
          toAddress: String(parsed.args.recipient),
        });
      }

      if (pool.dexType === 'dodo') {
        const parsed = DODO_SWAP_IFACE.parseLog(log)!;
        const fromToken = String(parsed.args.fromToken).toLowerCase();
        const toToken = String(parsed.args.toToken).toLowerCase();
        const fromAmount = BigInt(parsed.args.fromAmount.toString());
        const toAmount = BigInt(parsed.args.toAmount.toString());

        if (fromToken === pool.token0Address.toLowerCase() && toToken === pool.token1Address.toLowerCase()) {
          return await this.buildSwapEventRecord(pool, log, provider, {
            amount0In: fromAmount,
            amount1In: 0n,
            amount0Out: 0n,
            amount1Out: toAmount,
            sender: String(parsed.args.trader),
            toAddress: String(parsed.args.receiver),
          });
        }

        if (fromToken === pool.token1Address.toLowerCase() && toToken === pool.token0Address.toLowerCase()) {
          return await this.buildSwapEventRecord(pool, log, provider, {
            amount0In: 0n,
            amount1In: fromAmount,
            amount0Out: toAmount,
            amount1Out: 0n,
            sender: String(parsed.args.trader),
            toAddress: String(parsed.args.receiver),
          });
        }
      }
    } catch (error) {
      logger.warn('Historical pricing log parse failed', {
        chainId: pool.chainId,
        poolAddress: pool.poolAddress,
        dexType: pool.dexType,
        txHash: log.transactionHash,
        error,
      });
    }

    return null;
  }

  private async buildSwapEventRecord(
    pool: LiquidityPool,
    log: ethers.Log,
    provider: ethers.JsonRpcProvider,
    amounts: {
      amount0In: bigint;
      amount1In: bigint;
      amount0Out: bigint;
      amount1Out: bigint;
      sender?: string;
      toAddress?: string;
    }
  ): Promise<SwapEventRecord> {
    const moved0 = amounts.amount0In > 0n ? amounts.amount0In : amounts.amount0Out;
    const moved1 = amounts.amount1In > 0n ? amounts.amount1In : amounts.amount1Out;
    const valuation = await this.deriveSwapValuation(pool.chainId, pool.token0Address, pool.token1Address, moved0, moved1);

    return {
      chainId: pool.chainId,
      poolAddress: pool.poolAddress,
      transactionHash: log.transactionHash,
      blockNumber: Number(log.blockNumber),
      logIndex: log.index,
      token0Address: pool.token0Address,
      token1Address: pool.token1Address,
      amount0In: amounts.amount0In.toString(),
      amount1In: amounts.amount1In.toString(),
      amount0Out: amounts.amount0Out.toString(),
      amount1Out: amounts.amount1Out.toString(),
      amountUsd: valuation.amountUsd,
      priceUsd: valuation.priceUsd,
      token0PriceUsd: valuation.token0PriceUsd,
      token1PriceUsd: valuation.token1PriceUsd,
      sender: amounts.sender,
      toAddress: amounts.toAddress,
      timestamp: await this.getBlockTimestamp(provider, pool.chainId, Number(log.blockNumber)),
    };
  }

  private async deriveSwapValuation(
    chainId: number,
    token0Address: string,
    token1Address: string,
    amount0Raw: bigint,
    amount1Raw: bigint
  ): Promise<{
    amountUsd?: number;
    priceUsd?: number;
    token0PriceUsd?: number;
    token1PriceUsd?: number;
  }> {
    if (amount0Raw <= 0n || amount1Raw <= 0n) {
      return {};
    }

    const [token0Meta, token1Meta] = await Promise.all([
      this.getTokenMetadata(chainId, token0Address),
      this.getTokenMetadata(chainId, token1Address),
    ]);

    const amount0 = Number(ethers.formatUnits(amount0Raw, token0Meta.decimals));
    const amount1 = Number(ethers.formatUnits(amount1Raw, token1Meta.decimals));

    if (!Number.isFinite(amount0) || !Number.isFinite(amount1) || amount0 <= 0 || amount1 <= 0) {
      return {};
    }

    const [anchor0, anchor1] = await Promise.all([
      this.resolveAnchorUsdPrice(chainId, token0Address),
      this.resolveAnchorUsdPrice(chainId, token1Address),
    ]);

    let token0PriceUsd = anchor0;
    let token1PriceUsd = anchor1;

    if (anchor1 != null && anchor1 > 0) {
      token0PriceUsd = (amount1 * anchor1) / amount0;
    }

    if (anchor0 != null && anchor0 > 0) {
      token1PriceUsd = (amount0 * anchor0) / amount1;
    }

    if ((token0PriceUsd == null || token0PriceUsd <= 0) && token1PriceUsd != null && token1PriceUsd > 0) {
      token0PriceUsd = (amount1 * token1PriceUsd) / amount0;
    }

    if ((token1PriceUsd == null || token1PriceUsd <= 0) && token0PriceUsd != null && token0PriceUsd > 0) {
      token1PriceUsd = (amount0 * token0PriceUsd) / amount1;
    }

    const amountUsd =
      token0PriceUsd != null && token0PriceUsd > 0
        ? amount0 * token0PriceUsd
        : token1PriceUsd != null && token1PriceUsd > 0
          ? amount1 * token1PriceUsd
          : undefined;

    return {
      amountUsd,
      priceUsd: token0PriceUsd ?? token1PriceUsd,
      token0PriceUsd,
      token1PriceUsd,
    };
  }

  private async resolveAnchorUsdPrice(chainId: number, address: string): Promise<number | undefined> {
    const normalized = `${chainId}:${address.toLowerCase()}`;
    if (this.anchorPriceCache.has(normalized)) {
      return this.anchorPriceCache.get(normalized);
    }

    const canonical = getCanonicalPriceUsd(chainId, address);
    if (canonical != null && canonical > 0) {
      this.anchorPriceCache.set(normalized, canonical);
      return canonical;
    }

    const resolution = resolveCanonicalQuoteAddress(chainId, address.toLowerCase());
    const marketData = await this.marketDataRepo.getMarketData(chainId, resolution.lookupAddress);
    const price = marketData?.priceUsd;
    this.anchorPriceCache.set(normalized, price);
    return price;
  }

  private async getTokenMetadata(chainId: number, address: string): Promise<TokenMetadata> {
    const normalized = `${chainId}:${address.toLowerCase()}`;
    const cached = this.tokenMetadataCache.get(normalized);
    if (cached) {
      return cached;
    }

    const token = await this.tokenRepo.getToken(chainId, address.toLowerCase());
    if (token?.decimals != null) {
      const metadata = {
        address: address.toLowerCase(),
        decimals: token.decimals,
        symbol: token.symbol,
      };
      this.tokenMetadataCache.set(normalized, metadata);
      return metadata;
    }

    const provider = this.getProvider(chainId);
    const contract = new ethers.Contract(address, ERC20_METADATA_ABI, provider);

    try {
      const [decimals, symbol] = await Promise.all([
        contract.decimals().catch(() => 18),
        contract.symbol().catch(() => undefined),
      ]);

      const metadata = {
        address: address.toLowerCase(),
        decimals: Number(decimals),
        symbol: typeof symbol === 'string' ? symbol : undefined,
      };
      this.tokenMetadataCache.set(normalized, metadata);
      return metadata;
    } catch {
      const metadata = {
        address: address.toLowerCase(),
        decimals: 18,
      };
      this.tokenMetadataCache.set(normalized, metadata);
      return metadata;
    }
  }

  private async getBlockTimestamp(
    provider: ethers.JsonRpcProvider,
    chainId: number,
    blockNumber: number
  ): Promise<Date> {
    const key = `${chainId}:${blockNumber}`;
    const cached = this.blockTimestampCache.get(key);
    if (cached) {
      return cached;
    }

    const block = await provider.getBlock(blockNumber);
    const timestamp = new Date(Number(block?.timestamp || 0) * 1000);
    this.blockTimestampCache.set(key, timestamp);
    return timestamp;
  }
}
