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 isMissingInfo = (info: string | null): boolean => {
  if (!info) {
    return true;
  }
  return info.trim().length === 0;
};

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

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

  const tokens = await repositories.tokenRepository.findAll();
  const missing = tokens.filter((token) => isMissingInfo(token.info));

  logger.info('Starting token info healing', {
    totalTokens: tokens.length,
    missingInfoTokens: missing.length,
  });

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

  const coinMap = await coinMarketCapClient.getCoinMap();
  const slugToId = new Map<string, number>();
  for (const entry of coinMap) {
    if (!slugToId.has(entry.slug.toLowerCase())) {
      slugToId.set(entry.slug.toLowerCase(), entry.id);
    }
  }

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

  for (const token of missing) {
    const id = slugToId.get(token.code.toLowerCase());
    if (!id) {
      unmatched.push(token);
      continue;
    }
    ids.push(id);
    const list = tokensById.get(id) ?? [];
    list.push(token);
    tokensById.set(id, list);
  }

  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 })),
    });
  }

  let healed = 0;
  let failed = 0;
  let skipped = unmatched.length;

  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 metadata batch', { batch: i / BATCH_SIZE + 1, ids: batch.length });

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

      for (const coin of coins) {
        foundIds.add(coin.id);
        const tokensForCoin = tokensById.get(coin.id) ?? [];
        for (const token of tokensForCoin) {
          if (!coin.description || coin.description.trim().length === 0) {
            logger.warn('CoinMarketCap description is empty for token', {
              id: token.id,
              code: token.code,
              cmcId: coin.id,
            });
            skipped++;
            continue;
          }

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

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

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

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

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