import 'dotenv/config';
import 'reflect-metadata';
import { v4 as uuidv4 } from 'uuid';
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 { TransactionEntity } from '../infrastructure/persistence/entities/transaction.entity';
import { TokenEntity } from '../infrastructure/persistence/entities/token.entity';
import { Wallet } from '../domain/entities/wallet';
import { Network } from '../domain/entities/network';
import { NetworkStatus } from '../domain/value-objects/network-status';
import {
  getDeterministicAddress,
  getEvmAddress,
} from '../infrastructure/external/deterministic-address-generator';
import { isTokenTransferPath } from '../domain/value-objects/transaction-data';

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

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 as NetworkStatus,
    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 transactionRepository = AppDataSource.getRepository(TransactionEntity);
  const tokenRepository = AppDataSource.getRepository(TokenEntity);

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

  const activeNetworks = networkEntities.filter((network) => network.status === 'active');
  const activeAddressTypes = new Set(activeNetworks.map((network) => network.addressType));

  // Pick a representative active network for each address type so the
  // deterministic generator can produce a format-appropriate address string.
  const representativeNetworkByType = new Map<string, Network>();
  for (const network of activeNetworks) {
    if (!representativeNetworkByType.has(network.addressType)) {
      representativeNetworkByType.set(network.addressType, toDomainNetwork(network));
    }
  }

  const addressesByWallet = new Map<string, AddressEntity[]>();
  for (const address of allAddresses) {
    const list = addressesByWallet.get(address.walletId) ?? [];
    list.push(address);
    addressesByWallet.set(address.walletId, list);
  }

  logger.info('Starting address refactor healing', {
    dryRun: DRY_RUN,
    wallets: walletEntities.length,
    activeNetworks: activeNetworks.length,
    activeAddressTypes: Array.from(activeAddressTypes),
    addresses: allAddresses.length,
  });

  let addressesHealed = 0;
  let addressesCreated = 0;
  let walletsSkipped = 0;

  for (const walletEntity of walletEntities) {
    const wallet = toDomainWallet(walletEntity);

    let phrase: string;
    try {
      phrase = encryptionService.decrypt(walletEntity.encryptedPhrase);
    } catch (error) {
      logger.error('Failed to decrypt wallet phrase, skipping wallet', {
        walletId: wallet.id,
        error,
      });
      walletsSkipped++;
      continue;
    }

    const walletAddresses = addressesByWallet.get(wallet.id) ?? [];
    const addressesByType = new Map<string, AddressEntity>();
    const duplicateAddresses: AddressEntity[] = [];

    for (const address of walletAddresses) {
      const existing = addressesByType.get(address.type);
      if (!existing) {
        addressesByType.set(address.type, address);
      } else {
        // The migration should already have collapsed duplicates, but if this
        // script is run against data that still has duplicates we resolve them
        // deterministically: keep the oldest record.
        if (address.createdAt < existing.createdAt) {
          duplicateAddresses.push(existing);
          addressesByType.set(address.type, address);
        } else {
          duplicateAddresses.push(address);
        }
      }
    }

    for (const addressType of activeAddressTypes) {
      const expectedAddress =
        addressType === 'evm'
          ? getEvmAddress(phrase)
          : getDeterministicAddress(
              representativeNetworkByType.get(addressType) ?? toDomainNetwork(networkEntities[0]),
              wallet,
            );

      const existing = addressesByType.get(addressType);

      if (!existing) {
        if (DRY_RUN) {
          logger.info('[DRY RUN] Would create address', {
            walletId: wallet.id,
            addressType,
            address: expectedAddress,
          });
        } else {
          const entity = addressRepository.create({
            id: uuidv4(),
            address: expectedAddress,
            type: addressType,
            walletId: wallet.id,
            isPrimary: false,
            createdAt: new Date(),
            updatedAt: new Date(),
          });
          await addressRepository.save(entity);
          logger.info('Created address', {
            id: entity.id,
            walletId: wallet.id,
            addressType,
            address: expectedAddress,
          });
        }
        addressesCreated++;
        continue;
      }

      if (existing.address.toLowerCase() !== expectedAddress.toLowerCase()) {
        const oldAddress = existing.address;
        if (DRY_RUN) {
          logger.info('[DRY RUN] Would heal address', {
            id: existing.id,
            walletId: wallet.id,
            addressType,
            oldAddress,
            newAddress: expectedAddress,
          });
        } else {
          existing.address = expectedAddress;
          existing.updatedAt = new Date();
          await addressRepository.save(existing);
          logger.info('Healed address', {
            id: existing.id,
            walletId: wallet.id,
            addressType,
            oldAddress,
            newAddress: expectedAddress,
          });
        }
        addressesHealed++;
      }
    }

    // Remove any duplicate records that were not already collapsed by the
    // migration. Transaction references are redirected to the survivor.
    if (duplicateAddresses.length > 0) {
      const survivorByType = new Map<string, AddressEntity>();
      for (const [type, address] of addressesByType) {
        survivorByType.set(type, address);
      }

      for (const duplicate of duplicateAddresses) {
        const survivor = survivorByType.get(duplicate.type);
        if (!survivor) {
          continue;
        }

        if (DRY_RUN) {
          logger.info('[DRY RUN] Would collapse duplicate address', {
            duplicateId: duplicate.id,
            survivorId: survivor.id,
            walletId: wallet.id,
            addressType: duplicate.type,
          });
          continue;
        }

        await transactionRepository
          .createQueryBuilder()
          .update(TransactionEntity)
          .set({ fromAddressId: survivor.id })
          .where('fromAddressId = :oldId', { oldId: duplicate.id })
          .execute();

        await transactionRepository
          .createQueryBuilder()
          .update(TransactionEntity)
          .set({ toAddressId: survivor.id })
          .where('toAddressId = :oldId', { oldId: duplicate.id })
          .execute();

        await addressRepository.remove(duplicate);
        logger.info('Collapsed duplicate address', {
          duplicateId: duplicate.id,
          survivorId: survivor.id,
          walletId: wallet.id,
          addressType: duplicate.type,
        });
      }
    }
  }

  // Heal transaction network_id from the token(s) referenced by the
  // transaction. This fixes cases such as USDT (BSC) transactions being linked
  // to the wrong network because they share the same EVM address as USDT (ETH).
  logger.info('Starting transaction network healing');

  const tokenById = new Map(tokenEntities.map((t) => [t.id, t]));

  const resolveExpectedNetworkId = (entity: TransactionEntity): string | null => {
    if (entity.interactedWith) {
      const token = tokenById.get(entity.interactedWith);
      if (token) {
        return token.networkId;
      }
    }

    for (const path of entity.data) {
      // Swap-style token transfer paths carry token ids in from/to.
      if (isTokenTransferPath(path)) {
        const token = tokenById.get(path.from) ?? tokenById.get(path.to);
        if (token) {
          return token.networkId;
        }
      }
    }

    return null;
  };

  let txOffset = 0;
  let txHealed = 0;
  let txSkipped = 0;
  let txFailed = 0;
  let txUndetermined = 0;

  // eslint-disable-next-line no-constant-condition
  while (true) {
    const transactions = await transactionRepository.find({
      order: { createdAt: 'ASC' },
      take: TX_BATCH_SIZE,
      skip: txOffset,
    });

    if (transactions.length === 0) {
      break;
    }

    txOffset += transactions.length;

    for (const entity of transactions) {
      const expectedNetworkId = resolveExpectedNetworkId(entity);

      if (!expectedNetworkId) {
        txUndetermined++;
        continue;
      }

      if (entity.networkId === expectedNetworkId) {
        txSkipped++;
        continue;
      }

      const oldNetworkId = entity.networkId;

      if (DRY_RUN) {
        logger.info('[DRY RUN] Would heal transaction network', {
          id: entity.id,
          hash: entity.hash,
          oldNetworkId,
          newNetworkId: expectedNetworkId,
        });
        txHealed++;
        continue;
      }

      entity.networkId = expectedNetworkId;
      entity.updatedAt = new Date();

      try {
        await transactionRepository.save(entity);
        logger.info('Healed transaction network', {
          id: entity.id,
          hash: entity.hash,
          oldNetworkId,
          newNetworkId: expectedNetworkId,
        });
        txHealed++;
      } catch (error) {
        logger.error('Failed to heal transaction network', {
          id: entity.id,
          hash: entity.hash,
          oldNetworkId,
          newNetworkId: expectedNetworkId,
          error,
        });
        txFailed++;
        entity.networkId = oldNetworkId;
      }
    }
  }

  logger.info('Address refactor healing complete', {
    dryRun: DRY_RUN,
    wallets: walletEntities.length,
    walletsSkipped,
    addressesHealed,
    addressesCreated,
    transactionsHealed: txHealed,
    transactionsSkipped: txSkipped,
    transactionsFailed: txFailed,
    transactionsUndetermined: txUndetermined,
  });

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

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