import { Address } from '../entities/address';
import { Token } from '../entities/token';
import { Transaction } from '../entities/transaction';
import { isContractTokenTransferPath } from '../value-objects/transaction-data';

export interface TokenBalance {
  readonly tokenId: string;
  readonly balance: string;
}

export class WalletBalanceService {
  calculate(
    addresses: Address[],
    transactions: Transaction[],
    tokens: Token[],
  ): TokenBalance[] {
    // Build a lookup that matches either the internal address id or the raw
    // blockchain address. Third-party integrations sometimes store the raw
    // address in transaction fields, so we treat both identifiers as known.
    const knownAddresses = new Set<string>();
    for (const address of addresses) {
      knownAddresses.add(address.id);
      knownAddresses.add(address.address.toLowerCase());
    }

    const isKnownAddress = (value: string | null | undefined): boolean => {
      if (!value) {
        return false;
      }
      return knownAddresses.has(value) || knownAddresses.has(value.toLowerCase());
    };

    const nativeTokenByNetwork = new Map<string, Token>();
    for (const token of tokens) {
      if (token.isNative) {
        nativeTokenByNetwork.set(token.networkId, token);
      }
    }

    const balances = new Map<string, bigint>();

    const add = (tokenId: string, delta: bigint): void => {
      const current = balances.get(tokenId) ?? 0n;
      balances.set(tokenId, current + delta);
    };

    for (const transaction of transactions) {
      if (transaction.status !== 'confirmed') {
        continue;
      }

      const nativeToken = nativeTokenByNetwork.get(transaction.networkId);

      if (transaction.interactedWith) {
        // Smart-contract token transfer: `value` is 0 and the movement is recorded in `data`.
        for (const path of transaction.data) {
          if (isContractTokenTransferPath(path)) {
            const amount = BigInt(path.amount);
            if (transaction.from && isKnownAddress(transaction.from)) {
              add(transaction.interactedWith, -amount);
            }
            if (path.to && isKnownAddress(path.to)) {
              add(transaction.interactedWith, amount);
            }
          }
        }
      } else {
        // Layer-1 / native token transfer.
        if (nativeToken) {
          if (transaction.from && isKnownAddress(transaction.from) && transaction.to) {
            add(nativeToken.id, -BigInt(transaction.value));
          }
          if (transaction.to && isKnownAddress(transaction.to)) {
            add(nativeToken.id, BigInt(transaction.value));
          }
        }
      }

      // Swap data paths where token IDs are stored in `from` / `to`.
      // These apply both to legacy layer-1 swaps and to swaps that set
      // `interactedWith` to the output token.
      for (const path of transaction.data) {
        if ('fromTokenValue' in path && 'toTokenValue' in path) {
          add(path.from, -BigInt(path.fromTokenValue));
          add(path.to, BigInt(path.toTokenValue));
        }
      }

      if (nativeToken && transaction.fee && transaction.from && isKnownAddress(transaction.from)) {
        add(nativeToken.id, -BigInt(transaction.fee));
      }
    }

    return Array.from(balances.entries()).map(([tokenId, balance]) => ({
      tokenId,
      balance: balance.toString(),
    }));
  }
}
