import { privateKeyToAccount } from 'viem/accounts';
import type { EVMConfig } from '../../config.js';
import { BaseSDK } from '../sdk.js';
import { EVMIntentProvider } from './intent-provider.js';
import { type Address, type Hex } from 'viem';
import {
  PERMIT2_ADDRESS,
  CROSS_CHAIN_GUARD_ADDRESSES,
  SINGLE_CHAIN_GUARD_ADDRESSES,
  MAX_UINT_256,
} from '../../constants.js';
import type { ExtraTransfer } from '../orders/common.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';
import type { ApiUserOrders } from '../../types/api.js';
import { fetchJWTToken, fetchSiweMessage } from '../../auth/siwe.js';
import { fetchUserOrders } from '../orders/api/fetch.js';

type CancelSingleChainOrderParams = {
  orderId: string;
  user: string;
  tokenIn: string;
  amountIn: bigint;
  requestedOutput: ExtraTransfer;
  extraTransfers: ExtraTransfer[];
  encodedExternalCallData: string;
  deadline: number;
  nonce: bigint;
};

type CancelCrossChainOrderParams = {
  orderId: string;
  user: string;
  tokenIn: string;
  amountIn: bigint;
  srcChainId: number;
  deadline: number;
  minStablecoinsAmount: bigint;
  executionDetailsHash: string;
  nonce: bigint;
};

/**
 * Handles EVM-specific aspects of cross-chain swaps for Ethereum-compatible chains:
 */
export class EVMSDK extends BaseSDK {
  private readonly config: EVMConfig;
  private readonly evmIntentProvider: EVMIntentProvider;
  private token?: string;

  constructor(config: EVMConfig) {
    super();
    this.config = config;
    this.evmIntentProvider = new EVMIntentProvider(config);
  }

  public async cancelCrossChainOrder(params: CancelCrossChainOrderParams): Promise<string> {
    const chainId = this.config.chainId;
    const auctioneerAddress = CROSS_CHAIN_GUARD_ADDRESSES[chainId] as Hex;
    const userAddress = (await this.getUserAddress()) as Hex;

    const auctioneerContract = this.evmIntentProvider.provider.getCrossChainAuctioneerContract(auctioneerAddress);
    const permit2Contract = this.evmIntentProvider.provider.getPermit2Contract(PERMIT2_ADDRESS[chainId]);

    const orderIdHex = params.orderId as Hex;
    let [initialized, deactivated] = await auctioneerContract.read.orderData([orderIdHex]);
    let txHash: Hex;

    if (initialized) {
      if (deactivated) {
        throw new Error('Order is already deactivated');
      }

      const orderInfo = {
        user: params.user as Hex,
        tokenIn: params.tokenIn as Hex,
        srcChainId: params.srcChainId,
        deadline: params.deadline,
        amountIn: BigInt(params.amountIn),
        minStablecoinsAmount: BigInt(params.minStablecoinsAmount),
        executionDetailsHash: params.executionDetailsHash as Hex,
        nonce: BigInt(params.nonce!),
      };

      const tx = await auctioneerContract.write.cancelOrder([orderInfo]);
      txHash = tx;
    } else {
      const nonce = BigInt(params.nonce);
      let nonceWordPos = nonce >> 8n;
      let nonceBitPos = nonce - nonceWordPos * 256n;
      let mask = 1n << nonceBitPos;

      const currentNonceBitmap = await permit2Contract.read.nonceBitmap([userAddress, nonceWordPos]);
      if ((currentNonceBitmap & (1n << nonceBitPos)) !== 0n) {
        throw new Error('Nonce is already invalidated');
      }

      const tx = await permit2Contract.write.invalidateUnorderedNonces([nonceWordPos, mask]);
      txHash = tx;
    }

    return txHash;
  }

  public async cancelSingleChainOrder(params: CancelSingleChainOrderParams): Promise<string> {
    const chainId = this.config.chainId;
    const auctioneerAddress = SINGLE_CHAIN_GUARD_ADDRESSES[chainId] as Hex;
    const userAddress = (await this.getUserAddress()) as Hex;

    const auctioneerContract = this.evmIntentProvider.provider.getSingleChainAuctioneerContract(auctioneerAddress);

    const orderHash = params.orderId as Hex;
    const wasManuallyInitialized = await auctioneerContract.read.orderManuallyInitialized([orderHash]);

    let txHash: Hex;

    if (wasManuallyInitialized) {
      txHash = await auctioneerContract.write.cancelManuallyCreatedOrder([
        {
          amountIn: params.amountIn,
          tokenIn: params.tokenIn as Hex,
          deadline: params.deadline as number,
          nonce: params.nonce,
          encodedExternalCallData: params.encodedExternalCallData as Hex,
          extraTransfers: params.extraTransfers.map((transfer) => {
            return {
              amount: transfer.amount,
              receiver: transfer.receiver as Hex,
              token: transfer.token as Hex,
            };
          }),
          requestedOutput: {
            amount: params.requestedOutput.amount,
            receiver: params.requestedOutput.receiver as Hex,
            token: params.requestedOutput.token as Hex,
          },
          user: params.user as Hex,
        },
      ]);
    } else {
      const permit2Contract = this.evmIntentProvider.provider.getPermit2Contract(PERMIT2_ADDRESS[chainId]);

      const nonce = BigInt(params.nonce);
      const nonceWordPos = nonce >> 8n;
      const nonceBitPos = nonce - nonceWordPos * 256n;
      const mask = 1n << nonceBitPos;

      // Check if nonce is already invalidated
      const currentNonceBitmap = await permit2Contract.read.nonceBitmap([userAddress, nonceWordPos]);
      if ((currentNonceBitmap & (1n << nonceBitPos)) !== 0n) {
        throw new Error('Nonce is already invalidated');
      }

      txHash = await permit2Contract.write.invalidateUnorderedNonces([nonceWordPos, mask]);
    }

    return txHash;
  }

  public async authenticate(token?: string): Promise<string> {
    const chainId = this.config.chainId;
    const wallet = await this.getUserAddress();

    const response = await fetchSiweMessage({
      chainId,
      wallet,
    });

    const message = response.data!;

    // Sign the message using the wallet client
    const signature = await this.evmIntentProvider.provider.walletClient.signMessage({
      message,
    });

    const jwt = await fetchJWTToken(
      {
        message,
        signature,
      },
      token,
    );

    const newToken = jwt.data!;

    return newToken;
  }

  public setToken(token: string) {
    this.token = token;

    return this;
  }

  public override async getOrders(): Promise<ApiUserOrders> {
    if (!this.token) {
      throw new Error('No token provided');
    }

    const orders = await fetchUserOrders(this.token);

    return orders;
  }

  /**
   * Gets the user's Ethereum address derived from their private key
   * @returns Promise resolving to the user's Ethereum address as a 0x-prefixed string
   */
  public async getUserAddress(): Promise<string> {
    return privateKeyToAccount(this.config.privateKey).address;
  }

  protected async approveAllowanceIfNeeded(tokenAddress: Address, amount: bigint): Promise<void> {
    const permit2Address = PERMIT2_ADDRESS[this.config.chainId];
    const ERC20Contract = this.evmIntentProvider.provider.getERC20Contract(tokenAddress);

    const userAddress = (await this.getUserAddress()) as Address;

    const allowance = await ERC20Contract.read.allowance([userAddress, permit2Address]);

    if (allowance < amount) {
      await ERC20Contract.write.approve([permit2Address, MAX_UINT_256]);
    }
  }

  /**
   * Prepares an EVM order for submission
   *
   * This method:
   * 1. Validates token balances and allowances
   * 2. Creates and signs the EIP-712 typed data for Permit2
   * 3. Prepares the order for submission to the auctioneer
   *
   * @param order The validated order to prepare
   * @returns Promise resolving to a prepared order with EVM-specific signature data
   */
  protected override async prepareCrossChainOrder(order: CrossChainOrder): Promise<CrossChainOrderPrepared> {
    await this.approveAllowanceIfNeeded(order.sourceTokenAddress as Address, order.sourceTokenAmount);
    return this.evmIntentProvider.prepareCrossChainOrder(order);
  }

  protected async prepareSingleChainOrder(order: SingleChainOrder): Promise<SingleChainOrderPrepared> {
    await this.approveAllowanceIfNeeded(order.tokenIn as Address, order.amountIn);
    return this.evmIntentProvider.prepareSingleChainOrder(order);
  }
}
