import 'dotenv/config';
import 'reflect-metadata';
import { initializeDatabase } from '../infrastructure/persistence/data-source';
import { redisClient } from '../infrastructure/cache/redis-client';
import { createRepositories } from '../main/factories/repositories.factory';
import { createServices } from '../main/factories/services.factory';
import { Token } from '../domain/entities/token';

const BATCH_SIZE = 100;

const run = async (): Promise<void> => {
  await initializeDatabase();

  const repositories = createRepositories();
  const services = createServices(redisClient);
  const { logger, coinMarketCapClient } = services;

  const [tokens, networks] = await Promise.all([
    repositories.tokenRepository.findAll(),
    repositories.networkRepository.findAll(),
  ]);

  const networkNameById = new Map<string, string>();
  for (const network of networks) {
    networkNameById.set(network.id, network.name);
  }

  logger.info('Starting token market cap healing', {
    totalTokens: tokens.length,
    totalNetworks: networks.length,
  });

  if (tokens.length === 0) {
    logger.info('No tokens require market cap healing');
    await redisClient.quit();
    process.exit(0);
  }

  const tokensWithNetwork = tokens.map((token) => ({
    id: token.id,
    name: token.name,
    symbol: token.symbol,
    address: token.address,
    networkName: networkNameById.get(token.networkId) ?? token.networkId,
    token,
  }));

  const coinEntries = await coinMarketCapClient.getTokenCoinIdsByNetwork(tokensWithNetwork);

  const ids: number[] = [];
  const tokensById = new Map<number, Token[]>();
  const unmatched: Token[] = [];

  for (const { token } of tokensWithNetwork) {
    const entry = coinEntries.get(token.id) ?? null;
    if (!entry) {
      unmatched.push(token);
      continue;
    }
    ids.push(entry.id);
    const list = tokensById.get(entry.id) ?? [];
    list.push(token);
    tokensById.set(entry.id, list);
  }

  let healed = 0;
  let failed = 0;
  let skipped = 0;
  let zeroed = 0;

  if (unmatched.length > 0) {
    logger.warn('No CoinMarketCap mapping found for tokens', {
      count: unmatched.length,
      tokens: unmatched.map((t) => ({
        id: t.id,
        code: t.code,
        name: t.name,
        networkId: t.networkId,
      })),
    });
  }

  for (const token of unmatched) {
    const updated = new Token(
      token.id,
      token.isNative,
      token.address,
      token.networkId,
      token.thumbnail,
      token.name,
      token.symbol,
      token.decimals,
      token.code,
      token.info,
      token.isActive,
      token.minimumSwapValue,
      0,
      token.createdAt,
      new Date(),
    );
    await repositories.tokenRepository.update(updated);
    zeroed++;
    logger.info('Set token market cap to zero (no platform match)', {
      id: token.id,
      code: token.code,
      name: token.name,
      networkId: token.networkId,
    });
  }

  const uniqueIds = Array.from(new Set(ids));

  for (let i = 0; i < uniqueIds.length; i += BATCH_SIZE) {
    const batch = uniqueIds.slice(i, i + BATCH_SIZE);
    logger.info('Fetching token market cap batch', { batch: i / BATCH_SIZE + 1, ids: batch.length });

    try {
      const marketCaps = await coinMarketCapClient.getMarketCaps(batch);
      const foundIds = new Set<number>();

      for (const [id, marketCap] of marketCaps.entries()) {
        foundIds.add(id);
        const tokensForCoin = tokensById.get(id) ?? [];
        for (const token of tokensForCoin) {
          if (marketCap == null || marketCap <= 0) {
            logger.warn('CoinMarketCap market cap is missing or invalid for token', {
              id: token.id,
              code: token.code,
              cmcId: id,
              marketCap,
            });
            skipped++;
            continue;
          }

          const updated = new Token(
            token.id,
            token.isNative,
            token.address,
            token.networkId,
            token.thumbnail,
            token.name,
            token.symbol,
            token.decimals,
            token.code,
            token.info,
            token.isActive,
            token.minimumSwapValue,
            marketCap,
            token.createdAt,
            new Date(),
          );
          await repositories.tokenRepository.update(updated);
          logger.info('Healed token market cap', {
            id: token.id,
            code: token.code,
            name: token.name,
            cmcId: id,
            marketCap,
          });
          healed++;
        }
      }

      for (const id of batch) {
        if (!foundIds.has(id)) {
          const tokensForId = tokensById.get(id) ?? [];
          failed += tokensForId.length;
          logger.warn('CoinMarketCap market cap missing for ids', {
            cmcId: id,
            tokens: tokensForId.map((t) => ({ id: t.id, code: t.code })),
          });
        }
      }
    } catch (error) {
      logger.error('Failed to heal token market cap batch', { batch: i / BATCH_SIZE + 1, error });
      for (const id of batch) {
        failed += (tokensById.get(id) ?? []).length;
      }
    }
  }

  logger.info('Token market cap healing complete', {
    totalTokens: tokens.length,
    healed,
    failed,
    skipped,
    zeroed,
  });

  await redisClient.quit();
  process.exit(failed > 0 ? 1 : 0);
};

run().catch((error) => {
  // eslint-disable-next-line no-console
  console.error('Token market cap healing failed', error);
  process.exit(1);
});
