import { ethers } from 'ethers';
import { TokenRepository, Token } from '../database/repositories/token-repo';
import { logger } from '../utils/logger';

// ERC20 ABI for token metadata
const ERC20_ABI = [
  'function name() view returns (string)',
  'function symbol() view returns (string)',
  'function decimals() view returns (uint8)',
  'function totalSupply() view returns (uint256)',
];

export class TokenIndexer {
  private provider: ethers.JsonRpcProvider;
  private tokenRepo: TokenRepository;
  private chainId: number;

  constructor(chainId: number, rpcUrl: string) {
    this.chainId = chainId;
    this.provider = new ethers.JsonRpcProvider(rpcUrl);
    this.tokenRepo = new TokenRepository();
  }

  /**
   * Discover and index a token by address
   */
  async indexToken(address: string): Promise<Token | null> {
    try {
      const tokenContract = new ethers.Contract(address, ERC20_ABI, this.provider);

      // Fetch token metadata in parallel
      const [name, symbol, decimals, totalSupply] = await Promise.allSettled([
        tokenContract.name(),
        tokenContract.symbol(),
        tokenContract.decimals(),
        tokenContract.totalSupply(),
      ]);

      const token: Token = {
        chainId: this.chainId,
        address: address.toLowerCase(),
        name: name.status === 'fulfilled' ? name.value : undefined,
        symbol: symbol.status === 'fulfilled' ? symbol.value : undefined,
        decimals: decimals.status === 'fulfilled' ? decimals.value : undefined,
        totalSupply:
          totalSupply.status === 'fulfilled' ? totalSupply.value.toString() : undefined,
        verified: false, // Can be updated later via verification process
      };

      // Save to database
      await this.tokenRepo.upsertToken(token);

      return token;
    } catch (error) {
      logger.error(`Error indexing token ${address} on chain ${this.chainId}:`, error);
      return null;
    }
  }

  /**
   * Index multiple tokens
   */
  async indexTokens(addresses: string[]): Promise<Token[]> {
    const results = await Promise.allSettled(
      addresses.map((address) => this.indexToken(address))
    );

    return results
      .filter((result) => result.status === 'fulfilled' && result.value !== null)
      .map((result) => (result as PromiseFulfilledResult<Token>).value);
  }

  /**
   * Discover tokens from Transfer events
   */
  async discoverTokensFromTransfers(
    fromBlock: number,
    toBlock: number,
    batchSize: number = 1000
  ): Promise<string[]> {
    const discoveredAddresses = new Set<string>();

    // ERC20 Transfer event signature
    const transferTopic = ethers.id('Transfer(address,address,uint256)');

    try {
      for (let start = fromBlock; start <= toBlock; start += batchSize) {
        const end = Math.min(start + batchSize - 1, toBlock);

        const logs = await this.provider.getLogs({
          fromBlock: start,
          toBlock: end,
          topics: [transferTopic],
        });

        // Extract token addresses from logs
        logs.forEach((log) => {
          discoveredAddresses.add(log.address.toLowerCase());
        });

        logger.info(
          `Discovered ${discoveredAddresses.size} unique tokens from blocks ${start}-${end}`
        );
      }
    } catch (error) {
      logger.error(`Error discovering tokens from transfers:`, error);
    }

    return Array.from(discoveredAddresses);
  }

  /**
   * Update token metadata (useful for refreshing data)
   */
  async updateTokenMetadata(address: string): Promise<Token | null> {
    return this.indexToken(address);
  }

  /**
   * Get token from database or index if not found
   */
  async getOrIndexToken(address: string): Promise<Token | null> {
    // Try to get from database first
    const existing = await this.tokenRepo.getToken(this.chainId, address);
    if (existing) {
      return existing;
    }

    // Index if not found
    return this.indexToken(address);
  }
}
