import 'dotenv/config';
import 'reflect-metadata';
import { initializeDatabase, AppDataSource } from '../infrastructure/persistence/data-source';
import { PinoLogger } from '../infrastructure/logging/pino-logger';
import { AesGcmEncryptionService } from '../infrastructure/security/aes-gcm-encryption-service';
import { WalletEntity } from '../infrastructure/persistence/entities/wallet.entity';
import { NetworkEntity } from '../infrastructure/persistence/entities/network.entity';
import { AddressEntity } from '../infrastructure/persistence/entities/address.entity';
import { Network } from '../domain/entities/network';
import { Wallet } from '../domain/entities/wallet';
import {
  getLegacyAddress,
  getDeterministicAddress,
  getEvmAddress,
} from '../infrastructure/external/deterministic-address-generator';

const UTXO_ADDRESS_TYPES = new Set(['bitcoin']);
const EVM_ADDRESS_TYPE = 'evm';

const DRY_RUN = process.env.DRY_RUN === 'true';

const toDomainWallet = (entity: WalletEntity): Wallet =>
  new Wallet(
    entity.id,
    entity.name,
    entity.syncType,
    entity.syncedAt,
    entity.encryptedPhrase,
    entity.deviceIds,
    entity.isAdmin,
    entity.isActive,
    entity.lastAccessedAt,
    entity.version,
    entity.createdAt,
    entity.updatedAt,
  );

const toDomainNetwork = (entity: NetworkEntity): Network =>
  new Network(
    entity.id,
    entity.name,
    entity.code,
    entity.addressType,
    entity.thumbnail,
    entity.explorer,
    entity.chainId,
    entity.status,
    entity.mcap,
    entity.createdAt,
    entity.updatedAt,
  );

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

  const logger = new PinoLogger();
  const encryptionService = new AesGcmEncryptionService();

  const walletRepository = AppDataSource.getRepository(WalletEntity);
  const networkRepository = AppDataSource.getRepository(NetworkEntity);
  const addressRepository = AppDataSource.getRepository(AddressEntity);

  const [walletEntities, networkEntities, addressEntities] = await Promise.all([
    walletRepository.find(),
    networkRepository.find(),
    addressRepository.find(),
  ]);

  const wallets = walletEntities.map(toDomainWallet);
  const networks = networkEntities.map(toDomainNetwork);
  const utxoNetworks = networks.filter((network) =>
    UTXO_ADDRESS_TYPES.has(network.addressType),
  );
  const evmNetworks = networks.filter((network) => network.addressType === EVM_ADDRESS_TYPE);

  logger.info('Starting address healing', {
    dryRun: DRY_RUN,
    wallets: wallets.length,
    networks: networks.length,
    utxoNetworks: utxoNetworks.length,
    evmNetworks: evmNetworks.length,
    addresses: addressEntities.length,
  });

  type HealEntry = {
    networkCode: string;
    networkName: string;
    newAddress: string;
  };

  const healMap = new Map<string, HealEntry>();

  for (const wallet of wallets) {
    for (const network of utxoNetworks) {
      const oldAddress = getLegacyAddress(network, wallet);
      const newAddress = getDeterministicAddress(network, wallet);

      if (oldAddress === newAddress) {
        continue;
      }

      const key = `${wallet.id}:${oldAddress}`;
      if (healMap.has(key)) {
        logger.warn('Ambiguous address mapping, skipping', {
          walletId: wallet.id,
          networkName: network.name,
          oldAddress,
        });
        continue;
      }

      healMap.set(key, {
        networkCode: network.code,
        networkName: network.name,
        newAddress,
      });
    }

    if (evmNetworks.length === 0) {
      continue;
    }

    let evmAddress: string;
    try {
      const phrase = encryptionService.decrypt(wallet.encryptedPhrase);
      evmAddress = getEvmAddress(phrase);
    } catch (error) {
      logger.error('Failed to decrypt phrase or derive EVM address, skipping wallet', {
        walletId: wallet.id,
        error,
      });
      continue;
    }

    for (const network of evmNetworks) {
      const oldAddress = getDeterministicAddress(network, wallet);

      if (oldAddress.toLowerCase() === evmAddress.toLowerCase()) {
        continue;
      }

      const key = `${wallet.id}:${oldAddress}`;
      if (healMap.has(key)) {
        logger.warn('Ambiguous address mapping, skipping', {
          walletId: wallet.id,
          networkName: network.name,
          oldAddress,
        });
        continue;
      }

      healMap.set(key, {
        networkCode: network.code,
        networkName: network.name,
        newAddress: evmAddress,
      });
    }
  }

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

  for (const entity of addressEntities) {
    const key = `${entity.walletId}:${entity.address}`;
    const entry = healMap.get(key);

    if (!entry) {
      skipped++;
      continue;
    }

    if (entity.address.toLowerCase() === entry.newAddress.toLowerCase()) {
      skipped++;
      continue;
    }

    const oldAddress = entity.address;

    if (DRY_RUN) {
      logger.info('[DRY RUN] Would heal address', {
        id: entity.id,
        walletId: entity.walletId,
        networkCode: entry.networkCode,
        networkName: entry.networkName,
        oldAddress,
        newAddress: entry.newAddress,
      });
      healed++;
      continue;
    }

    entity.address = entry.newAddress;
    entity.updatedAt = new Date();

    try {
      await addressRepository.save(entity);
      logger.info('Healed address', {
        id: entity.id,
        walletId: entity.walletId,
        networkCode: entry.networkCode,
        networkName: entry.networkName,
        oldAddress,
        newAddress: entry.newAddress,
      });
      healed++;
    } catch (error) {
      logger.error('Failed to heal address', {
        id: entity.id,
        walletId: entity.walletId,
        networkCode: entry.networkCode,
        networkName: entry.networkName,
        oldAddress,
        newAddress: entry.newAddress,
        error,
      });
      failed++;
      entity.address = oldAddress;
    }
  }

  logger.info('Address healing complete', {
    dryRun: DRY_RUN,
    wallets: wallets.length,
    networks: networks.length,
    utxoNetworks: utxoNetworks.length,
    evmNetworks: evmNetworks.length,
    addresses: addressEntities.length,
    healed,
    skipped,
    failed,
  });

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

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