import { type Address } from 'viem';
import { type SupportedEvmChain } from '../../chains.js';
import type { EVMConfig } from '../../config.js';
import { SINGLE_CHAIN_GUARD_ADDRESSES, CROSS_CHAIN_GUARD_ADDRESSES, MAX_UINT_32 } from '../../constants.js';
import { getEVMCrossChainOrderTypedData, getEVMSingleChainOrderTypedData } from './order-signature.js';
import { type CrossChainPermitTransferFrom, type SingleChainPermitTransferFrom, type TransferData } from './permit2.js';
import { ChainProvider } from './chain-provider.js';
import type { CrossChainOrder } from '../orders/cross-chain.js';
import type { SingleChainOrder } from '../orders/single-chain.js';
import type { CrossChainOrderPrepared, SingleChainOrderPrepared } from '../../types/intent.js';

export class EVMIntentProvider {
  provider: ChainProvider;

  constructor(config: EVMConfig) {
    this.provider = new ChainProvider(config);
  }

  async prepareSingleChainOrder(order: SingleChainOrder): Promise<SingleChainOrderPrepared> {
    const { orderTypedData: dataToSign, nonce } = await getEVMSingleChainOrderTypedData(order);

    const signature = await this.provider.walletClient.signTypedData({
      message: dataToSign.message,
      primaryType: dataToSign.primaryType,
      types: dataToSign.types,
      domain: dataToSign.domain,
      account: this.provider.getAccount(),
    });

    return {
      order,
      preparedData: {
        nonce: nonce.toString(),
        signature: signature.slice(2),
      },
    };
  }

  async prepareCrossChainOrder(order: CrossChainOrder): Promise<CrossChainOrderPrepared> {
    const { orderTypedData: dataToSign, nonce } = await getEVMCrossChainOrderTypedData(order);

    const signature = await this.provider.walletClient.signTypedData({
      message: dataToSign.message,
      primaryType: dataToSign.primaryType,
      types: dataToSign.types,
      domain: dataToSign.domain,
      account: this.provider.getAccount(),
    });

    return {
      order,
      preparedData: {
        nonce: nonce.toString(),
        signature: signature.slice(2),
      },
    };
  }

  static getCrossChainPermissionMessage(order: CrossChainOrder, nonce: bigint): CrossChainPermitTransferFrom {
    const spender = CROSS_CHAIN_GUARD_ADDRESSES[order.sourceChainId as SupportedEvmChain] as Address;

    return {
      permitted: {
        token: order.sourceTokenAddress as Address,
        amount: order.sourceTokenAmount,
      },
      spender,
      nonce,
      deadline: BigInt(order.deadline),
      witness: {
        user: order.user as Address,
        tokenIn: order.sourceTokenAddress as Address,
        srcChainId: order.sourceChainId,
        deadline: order.deadline,
        amountIn: order.sourceTokenAmount,
        minStablecoinsAmount: order.minStablecoinAmount,
        nonce,
        executionDetailsHash: order.getExecutionDetailsHash(),
      },
    };
  }

  static getSingleChainLimitPermissionMessage(order: SingleChainOrder, nonce: bigint): SingleChainPermitTransferFrom {
    const spender = SINGLE_CHAIN_GUARD_ADDRESSES[order.chainId as SupportedEvmChain] as Address;

    const requestedOutput: TransferData = {
      token: order.tokenOut as Address,
      receiver: order.destinationAddress as Address,
      amount: order.amountOutMin,
    };

    const extraTransfers: TransferData[] =
      order.extraTransfers?.map((transfer) => ({
        token: transfer.token as Address,
        receiver: transfer.receiver as Address,
        amount: transfer.amount,
      })) || [];

    return {
      permitted: {
        token: order.tokenIn as Address,
        amount: order.amountIn,
      },
      spender,
      nonce,
      deadline: BigInt(order.deadline),
      witness: {
        user: order.user as Address,
        tokenIn: order.tokenIn as Address,
        amountIn: order.amountIn,
        requestedOutput,
        encodedExternalCallData: '0x', // TODO: Update when external call data is supported
        extraTransfers,
        deadline: order.deadline,
        nonce,
      },
    };
  }
  static getRandomNonce(): bigint {
    return BigInt(Math.floor(Math.random() * Number(MAX_UINT_32)));
  }
}
