/**
 * Transaction Interceptor - Intercepts eth_sendTransaction and converts to eth_sendRawTransaction
 */

import { BesuClient } from '../clients/besu-client';
import { Web3SignerClient } from '../clients/web3signer-client';
import { NonceManager } from '../services/nonce-manager';
import { ethers } from 'ethers';

export interface TxPolicy {
  walletAllowlist: string[];
  maxGasLimit: bigint;
  maxGasPriceWei: bigint;
  minGasPriceWei: bigint;
}

export interface TransactionObject {
  from: string;
  to?: string;
  value?: string;
  gas?: string;
  gasPrice?: string;
  maxFeePerGas?: string;
  maxPriorityFeePerGas?: string;
  data?: string;
  nonce?: string;
  chainId?: string | number;
}

export class TxInterceptor {
  private besuClient: BesuClient;
  private web3SignerClient: Web3SignerClient;
  private nonceManager: NonceManager;
  private chainId: number;
  private policy: TxPolicy;

  constructor(
    besuClient: BesuClient,
    web3SignerClient: Web3SignerClient,
    nonceManager: NonceManager,
    chainId: number,
    policy: TxPolicy
  ) {
    this.besuClient = besuClient;
    this.web3SignerClient = web3SignerClient;
    this.nonceManager = nonceManager;
    this.chainId = chainId;
    this.policy = policy;
  }

  private validateAddress(address: string): string {
    if (!address || !ethers.isAddress(address)) {
      throw new Error(`Invalid address: ${address}`);
    }
    return ethers.getAddress(address); // Normalize to checksum
  }

  private validateTx(tx: TransactionObject): void {
    if (!tx.from) {
      throw new Error('Transaction missing required field: from');
    }

    const from = this.validateAddress(tx.from);

    // Check allowlist (only if allowlist is configured)
    // Note: With smart interception, addresses without keys in Web3Signer
    // are passed through before reaching here, so this validation only
    // applies to service wallets that have keys in Web3Signer
    if (this.policy.walletAllowlist.length > 0) {
      const normalizedAllowlist = this.policy.walletAllowlist.map(addr => 
        ethers.getAddress(addr.toLowerCase())
      );
      if (!normalizedAllowlist.includes(from)) {
        throw new Error(`Address ${from} is not in the wallet allowlist`);
      }
    }

    // Validate chainId
    if (tx.chainId !== undefined) {
      const txChainId = typeof tx.chainId === 'string' 
        ? parseInt(tx.chainId, tx.chainId.startsWith('0x') ? 16 : 10)
        : tx.chainId;
      if (txChainId !== this.chainId) {
        throw new Error(`Chain ID mismatch: expected ${this.chainId}, got ${txChainId}`);
      }
    }
  }

  private async fillTxDefaults(tx: TransactionObject): Promise<TransactionObject> {
    const filledTx: TransactionObject = { ...tx };

    // Normalize from address
    filledTx.from = this.validateAddress(tx.from);

    // Set chainId
    filledTx.chainId = `0x${this.chainId.toString(16)}`;

    // Get nonce if not provided
    if (!filledTx.nonce) {
      const nonce = await this.nonceManager.getNextNonce(filledTx.from);
      filledTx.nonce = `0x${nonce.toString(16)}`;
    }

    // Set value default
    if (!filledTx.value) {
      filledTx.value = '0x0';
    }

    // Estimate gas if not provided
    if (!filledTx.gas) {
      try {
        const gasEstimateHex = await this.besuClient.callRpc('eth_estimateGas', [filledTx]);
        const gasEstimate = BigInt(gasEstimateHex);
        
        // Apply max gas limit
        const maxGas = this.policy.maxGasLimit;
        filledTx.gas = `0x${(gasEstimate > maxGas ? maxGas : gasEstimate).toString(16)}`;
      } catch (error) {
        // If estimation fails, use a default
        filledTx.gas = `0x${this.policy.maxGasLimit.toString(16)}`;
      }
    } else {
      // Validate gas limit
      const gas = BigInt(filledTx.gas);
      if (gas > this.policy.maxGasLimit) {
        throw new Error(`Gas limit ${gas} exceeds maximum ${this.policy.maxGasLimit}`);
      }
    }

    // Handle fees (EIP-1559 or legacy)
    const hasEip1559Fields = filledTx.maxFeePerGas || filledTx.maxPriorityFeePerGas;
    
    if (hasEip1559Fields) {
      // EIP-1559: validate and set defaults
      if (!filledTx.maxFeePerGas) {
        const gasPrice = await this.besuClient.callRpc('eth_gasPrice', []);
        filledTx.maxFeePerGas = gasPrice;
      }
      if (!filledTx.maxPriorityFeePerGas) {
        filledTx.maxPriorityFeePerGas = '0x0'; // Zero baseFee, so priority can be 0
      }

      // Validate caps
      const maxFee = BigInt(filledTx.maxFeePerGas!);
      const maxPriorityFee = BigInt(filledTx.maxPriorityFeePerGas!);
      
      if (maxFee > this.policy.maxGasPriceWei) {
        throw new Error(`maxFeePerGas ${maxFee} exceeds maximum ${this.policy.maxGasPriceWei}`);
      }
      if (maxPriorityFee > this.policy.maxGasPriceWei) {
        throw new Error(`maxPriorityFeePerGas ${maxPriorityFee} exceeds maximum ${this.policy.maxGasPriceWei}`);
      }
    } else {
      // Legacy: set gasPrice
      if (!filledTx.gasPrice) {
        const gasPrice = await this.besuClient.callRpc('eth_gasPrice', []);
        filledTx.gasPrice = gasPrice;
      }

      // Validate gas price
      const gasPrice = BigInt(filledTx.gasPrice!);
      if (gasPrice > this.policy.maxGasPriceWei) {
        throw new Error(`gasPrice ${gasPrice} exceeds maximum ${this.policy.maxGasPriceWei}`);
      }
      if (gasPrice < this.policy.minGasPriceWei) {
        throw new Error(`gasPrice ${gasPrice} is below minimum ${this.policy.minGasPriceWei}`);
      }
    }

    return filledTx;
  }

  async interceptAndSign(tx: TransactionObject): Promise<string> {
    // Validate transaction
    this.validateTx(tx);

    // Fill defaults
    const filledTx = await this.fillTxDefaults(tx);

    // Prepare transaction for signing
    const signTxData = {
      from: filledTx.from,
      to: filledTx.to,
      gas: filledTx.gas,
      gasPrice: filledTx.gasPrice,
      maxFeePerGas: filledTx.maxFeePerGas,
      maxPriorityFeePerGas: filledTx.maxPriorityFeePerGas,
      value: filledTx.value!,
      data: filledTx.data || '0x',
      nonce: filledTx.nonce!,
    };

    // Sign transaction via Web3Signer
    // Web3Signer returns the complete signed transaction as a hex string
    const signedTxHex = await this.web3SignerClient.signTransaction(filledTx.from, signTxData);

    // Validate that we received a signed transaction hex string
    if (!signedTxHex || !signedTxHex.startsWith('0x')) {
      throw new Error(`Invalid signed transaction from Web3Signer: ${signedTxHex}`);
    }

    // Web3Signer should return the complete raw signed transaction
    const rawTransaction = signedTxHex;

    // Submit to Besu
    const txHash = await this.besuClient.sendRawTransaction(rawTransaction);

    // Commit nonce after successful submission
    const nonce = parseInt(filledTx.nonce!, 16);
    await this.nonceManager.commitNonce(filledTx.from, nonce);

    return txHash;
  }
}
