import { Token } from '../../../domain/entities/token';
import { NotFoundError } from '../../../domain/errors/not-found-error';
import { ValidationError } from '../../../domain/errors/validation-error';
import { INetworkRepository } from '../../../domain/repositories/network-repository';
import { ITokenRepository } from '../../../domain/repositories/token-repository';
import { CoinMarketCapClient } from '../../../infrastructure/external/coinmarketcap-client';
import { TransactionRateInput } from '../../dto/transaction.dto';
import { TransactionData, TokenTransferPath } from '../../../domain/value-objects/transaction-data';

export interface AmountRate {
  readonly raw: string | null;
  readonly formatted: string | null;
  readonly tokenSymbol: string | null;
  readonly tokenDecimals: number | null;
  readonly usd: number | null;
}

export interface SwapRate {
  readonly amountIn: AmountRate;
  readonly amountOut: AmountRate;
  readonly minimumSwapValueUsd: number;
  readonly minimumSwapValueFormatted: string | null;
  readonly meetsMinimum: boolean | null;
}

export interface TransactionRateOutput {
  readonly networkId: string;
  readonly method: string;
  readonly fee: AmountRate;
  readonly swap: SwapRate | null;
}

const DEFAULT_MINIMUM_SWAP_VALUE_USD = 10;

function formatRawAmount(raw: string, decimals: number): string | null {
  try {
    const value = BigInt(raw);
    const isNegative = value < 0n;
    const absValue = isNegative ? -value : value;
    const divisor = 10n ** BigInt(decimals);
    const integerPart = absValue / divisor;
    const fractionalPart = absValue % divisor;
    const fractionalStr = fractionalPart.toString().padStart(decimals, '0').replace(/0+$/, '');
    const sign = isNegative ? '-' : '';
    return fractionalStr ? `${sign}${integerPart}.${fractionalStr}` : `${sign}${integerPart}`;
  } catch {
    return null;
  }
}

function calculateFeeRaw(
  fee: string | null | undefined,
  gasPrice: string | null | undefined,
  gasLimit: number | null | undefined,
): string | null {
  if (fee) {
    return fee;
  }

  if (gasPrice && gasLimit != null) {
    return (BigInt(gasPrice) * BigInt(gasLimit)).toString();
  }

  return null;
}

function isTokenTransferPath(path: TransactionData): path is TokenTransferPath {
  return 'fromTokenValue' in path && 'toTokenValue' in path;
}

export class TransactionRateUseCase {
  constructor(
    private readonly networkRepository: INetworkRepository,
    private readonly tokenRepository: ITokenRepository,
    private readonly coinMarketCapClient: CoinMarketCapClient,
  ) {}

  async execute(input: TransactionRateInput): Promise<TransactionRateOutput> {
    const network = await this.networkRepository.findById(input.networkId);
    if (!network) {
      throw new NotFoundError('Network', input.networkId);
    }

    const networkTokens = await this.tokenRepository.findByNetworkId(network.id);
    const nativeToken = networkTokens.find((token) => token.isNative);

    const fee = await this.buildFee(input, nativeToken);
    const swap = input.method.toLowerCase() === 'swap' ? await this.buildSwap(input, networkTokens) : null;

    return {
      networkId: network.id,
      method: input.method,
      fee,
      swap,
    };
  }

  private async buildFee(input: TransactionRateInput, nativeToken: Token | undefined): Promise<AmountRate> {
    const raw = calculateFeeRaw(input.fee, input.gasPrice, input.gasLimit);

    if (!raw) {
      return {
        raw: null,
        formatted: null,
        tokenSymbol: nativeToken?.symbol ?? null,
        tokenDecimals: nativeToken?.decimals ?? null,
        usd: null,
      };
    }

    const formatted = nativeToken ? formatRawAmount(raw, nativeToken.decimals) : null;
    const usd = formatted && nativeToken ? await this.estimateUsd(nativeToken, formatted) : null;

    return {
      raw,
      formatted,
      tokenSymbol: nativeToken?.symbol ?? null,
      tokenDecimals: nativeToken?.decimals ?? null,
      usd,
    };
  }

  private async buildSwap(input: TransactionRateInput, networkTokens: Token[]): Promise<SwapRate | null> {
    const tokenIn = await this.resolveTokenIn(input, networkTokens);
    const tokenOut = await this.resolveTokenOut(input, networkTokens);

    if (!tokenIn || !tokenOut) {
      const details: Record<string, string[]> = {};
      if (!tokenIn) {
        details.tokenIn = ['Token not found'];
      }
      if (!tokenOut) {
        details.tokenOut = ['Token not found'];
      }

      throw new ValidationError(
        'Swap transactions require tokenIn and tokenOut to be specified or derivable from data',
        details,
      );
    }

    const amountInRaw = this.resolveAmountIn(input);
    const amountOutRaw = this.resolveAmountOut(input);

    const amountIn = await this.buildAmount(amountInRaw, tokenIn);
    const amountOut = await this.buildAmount(amountOutRaw, tokenOut);

    const minimumSwapValueUsd = tokenIn.minimumSwapValue ?? DEFAULT_MINIMUM_SWAP_VALUE_USD;
    let minimumSwapValueFormatted: string | null = null;
    if (amountIn.usd != null && amountIn.usd > 0 && amountIn.formatted != null) {
      const amountInFormattedValue = parseFloat(amountIn.formatted);
      minimumSwapValueFormatted = ((minimumSwapValueUsd * amountInFormattedValue) / amountIn.usd).toString();
    }

    const meetsMinimum = amountIn.usd != null ? amountIn.usd >= minimumSwapValueUsd : null;

    return {
      amountIn,
      amountOut,
      minimumSwapValueUsd,
      minimumSwapValueFormatted,
      meetsMinimum,
    };
  }

  private async resolveTokenIn(
    input: TransactionRateInput,
    networkTokens: Token[],
  ): Promise<Token | null> {
    const tokenTransferPath = (input.data ?? []).find(isTokenTransferPath);
    if (tokenTransferPath) {
      const fromToken = await this.tokenRepository.findById(tokenTransferPath.from);
      if (fromToken) {
        return fromToken;
      }
    }

    if (input.tokenIn) {
      return this.findTokenBySymbolOrCode(input.tokenIn, networkTokens);
    }

    return null;
  }

  private async resolveTokenOut(
    input: TransactionRateInput,
    networkTokens: Token[],
  ): Promise<Token | null> {
    const tokenTransferPath = (input.data ?? []).find(isTokenTransferPath);
    if (tokenTransferPath) {
      const toToken = await this.tokenRepository.findById(tokenTransferPath.to);
      if (toToken) {
        return toToken;
      }
    }

    if (input.tokenOut) {
      return this.findTokenBySymbolOrCode(input.tokenOut, networkTokens);
    }

    return null;
  }

  private findTokenBySymbolOrCode(identifier: string, networkTokens: Token[]): Token | null {
    const upperIdentifier = identifier.toUpperCase();

    const bySymbol = networkTokens.find(
      (token) => token.symbol.toUpperCase() === upperIdentifier,
    );
    if (bySymbol) {
      return bySymbol;
    }

    const byCode = networkTokens.find(
      (token) => token.code.toUpperCase() === upperIdentifier,
    );
    return byCode ?? null;
  }

  private resolveAmountIn(input: TransactionRateInput): string | null {
    if (input.amountIn) {
      return input.amountIn;
    }

    const tokenTransferPath = (input.data ?? []).find(isTokenTransferPath);
    return tokenTransferPath ? tokenTransferPath.fromTokenValue : null;
  }

  private resolveAmountOut(input: TransactionRateInput): string | null {
    if (input.amountOut) {
      return input.amountOut;
    }

    const tokenTransferPath = (input.data ?? []).find(isTokenTransferPath);
    return tokenTransferPath ? tokenTransferPath.toTokenValue : null;
  }

  private async buildAmount(raw: string | null, token: Token): Promise<AmountRate> {
    if (!raw) {
      return {
        raw: null,
        formatted: null,
        tokenSymbol: token.symbol,
        tokenDecimals: token.decimals,
        usd: null,
      };
    }

    const formatted = formatRawAmount(raw, token.decimals);
    const usd = formatted ? await this.estimateUsd(token, formatted) : null;

    return {
      raw,
      formatted,
      tokenSymbol: token.symbol,
      tokenDecimals: token.decimals,
      usd,
    };
  }

  private async estimateUsd(token: Token, formattedAmount: string): Promise<number | null> {
    try {
      const coinInfo = await this.coinMarketCapClient.getCoinInfoBySlug(token.code);
      if (!coinInfo) {
        return null;
      }

      const prices = await this.coinMarketCapClient.getPrices([coinInfo.id], 'USD');
      const price = prices.get(coinInfo.id);
      if (price == null) {
        return null;
      }

      return parseFloat(formattedAmount) * price;
    } catch {
      return null;
    }
  }
}
