import { sha256, toBytes, type TypedDataDefinition } from 'viem';
import { isEvmChain, ChainID, chainIdToChainTypeMap, type SupportedChain } from '../../chains.js';
import { ValidationError } from '../../errors/index.js';
import {
  type ChainPreparedData,
  type CrossChainUserIntentRequest,
  type ExecutionDetails,
  type Hash,
  type SourceChainData,
} from '../../types/intent.js';
import { CrossChainOrderValidator } from '../../utils/order-validator.js';
import { Parsers } from '../../utils/parsers.js';
import { type ExtraTransfer } from './common.js';
import type { ApiResponse } from '../../types/api.js';
import { BaseSDK } from '../sdk.js';
import { getEVMCrossChainOrderTypedData } from '../evm/order-signature.js';
import { getSolanaCrossChainOrderInstructions } from '../solana/order-instructions.js';
import { getSuiOrderTransaction } from '../sui/order-transaction.js';
import { QuoteProvider } from '../../utils/quote/aggregator.js';

export type CreateCrossChainOrderParams = {
  /** Source chain ID where tokens will be sent from */
  sourceChainId: SupportedChain;
  /** Token address on the source chain to be swapped */
  sourceTokenAddress: string;
  /** Amount of source tokens to swap with decimals */
  sourceTokenAmount: bigint;
  /** Destination chain ID where tokens will be received */
  destinationChainId: SupportedChain;
  /** Token address on the destination chain to receive */
  destinationTokenAddress: string;
  /** Minimum amount of destination tokens to receive */
  destinationTokenMinAmount?: bigint;
  /** Recipient wallet address on the destination chain */
  destinationAddress: string;
  /** Minimum amount of stablecoins in the intermediate swap */
  minStablecoinAmount?: bigint;
  /** Timestamp (in seconds) after which the order expires */
  deadline: number;
  /** Extra transfers to be made */
  extraTransfers?: ExtraTransfer[];

  /** Stop loss max out */
  stopLossMaxOut?: bigint;
  /** Take profit min out */
  takeProfitMinOut?: bigint;
};

/**
 * Represents a X-chain swap order
 * Contains all the information needed to execute the order on both source and destination chains
 */
export class CrossChainOrder {
  /** User's wallet address that initiates the order */
  public user: string;
  /** Source chain ID where tokens will be sent from */
  public sourceChainId: SupportedChain;
  /** Token address on the source chain to be swapped */
  public sourceTokenAddress: string;
  /** Amount of source tokens to swap */
  public sourceTokenAmount: bigint;
  /** Destination chain ID where tokens will be received */
  public destinationChainId: SupportedChain;
  /** Token address on the destination chain to receive */
  public destinationTokenAddress: string;
  /** Minimum amount of destination tokens to receive */
  public destinationTokenMinAmount: bigint;
  /** Recipient wallet address on the destination chain */
  public destinationAddress: string;
  /** Minimum amount of stablecoins in the intermediate swap */
  public minStablecoinAmount: bigint;
  /** Timestamp (in seconds) after which the order expires */
  public deadline: number;
  /** Extra transfers to be made */
  public extraTransfers?: ExtraTransfer[];
  /** Stop loss max out */
  public stopLossMaxOut?: bigint;
  /** Take profit min out */
  public takeProfitMinOut?: bigint;

  private constructor(params: CreateCrossChainOrderParams & { user: string }) {
    this.user = params.user;
    this.sourceChainId = params.sourceChainId;
    this.sourceTokenAddress = params.sourceTokenAddress;
    this.sourceTokenAmount = params.sourceTokenAmount;
    this.destinationChainId = params.destinationChainId;
    this.destinationTokenAddress = params.destinationTokenAddress;
    this.destinationTokenMinAmount = params.destinationTokenMinAmount ?? 1n;
    this.destinationAddress = params.destinationAddress;
    this.minStablecoinAmount = params.minStablecoinAmount ?? 1n;
    this.deadline = params.deadline;
    this.extraTransfers = params.extraTransfers;
    this.stopLossMaxOut = params.stopLossMaxOut;
    this.takeProfitMinOut = params.takeProfitMinOut;
  }

  /**
   * Factory method to create and validate a new Order instance
   * @param input Order parameters
   * @returns Validated Order instance
   * @throws {ValidationError} If order validation fails
   */
  public static async create(input: CreateCrossChainOrderParams & { user: string }): Promise<CrossChainOrder> {
    // Validate first on creation
    await new CrossChainOrderValidator().validateOrder({ ...input, user: input.user });

    const { minStablecoinAmount, destinationTokenMinAmount } = await this.calculateAmountOutMin(input);
    const order = new CrossChainOrder({
      ...input,
      minStablecoinAmount: minStablecoinAmount,
      destinationTokenMinAmount: destinationTokenMinAmount,
      user: input.user,
    });

    const randomPreparedData = order.getRandomPreparedData();
    const intentRequest = order.toIntentRequest(randomPreparedData);
    await BaseSDK.validateCrossChainOrder(intentRequest);

    return order;
  }

  /// This is needed because API requires the prepared data to be sent
  /// In the cases of Solana and Sui, if we want the real data, we must send the order on-chain before validating.
  /// And that is something we cannot do before validating the order on the API side.
  private getRandomPreparedData(): ChainPreparedData {
    const chainId = this.sourceChainId;
    const chainType = chainIdToChainTypeMap[chainId];

    switch (chainType) {
      case 'EVM': {
        return {
          nonce: String(Math.floor(Math.random() * 10000000)),
          signature: '0x0000000000000000000000000000000000000000000000000000000000000000',
        };
      }
      case 'Solana': {
        return { orderPubkey: 'DFNAjFAvS4GF98Tp1kiyLvEHM3wjGXibCfF86nnmhuVc' };
      }
      case 'Sui': {
        const digest = 'FQWndwYJhNQUoHyvR8UuhGURC2EKx9eWErFm9Tc2DggF';
        return { transactionHash: digest };
      }

      default: {
        throw new Error('Chain type not supported');
      }
    }
  }

  private static async calculateAmountOutMin(
    input: CreateCrossChainOrderParams,
  ): Promise<{ minStablecoinAmount: bigint; destinationTokenMinAmount: bigint }> {
    const { destinationTokenMinAmount, stopLossMaxOut, minStablecoinAmount } = input;

    if (stopLossMaxOut !== undefined) {
      return {
        destinationTokenMinAmount: 1n,
        minStablecoinAmount: 1n,
      };
    }

    if (!minStablecoinAmount || !destinationTokenMinAmount) {
      const quote = await QuoteProvider.getQuote({
        sourceChainId: input.sourceChainId,
        tokenIn: input.sourceTokenAddress,
        amount: input.sourceTokenAmount,
        destChainId: input.destinationChainId,
        tokenOut: input.destinationTokenAddress,
      });

      return {
        destinationTokenMinAmount: quote.estimatedAmountOutReduced,
        minStablecoinAmount: quote.estimatedAmountInAsMinStablecoinAmount,
      };
    }

    return {
      destinationTokenMinAmount,
      minStablecoinAmount,
    };
  }

  /**
   * Gets the execution details for the destination chain
   * These details are used to complete the order on the destination chain
   * @returns Structured execution details object
   */
  public getExecutionDetails(): ExecutionDetails {
    return {
      destChainId: this.destinationChainId,
      tokenOut: this.destinationTokenAddress,
      destinationAddress: this.destinationAddress,
      amountOutMin: this.destinationTokenMinAmount,
      extraTransfers: this.extraTransfers,
    };
  }

  public executionDetailsHashToBytes(): Uint8Array {
    const executionDetailsHash = this.getExecutionDetailsHash().slice(2);

    const executionHashByteSlice = Buffer.from(executionDetailsHash, 'hex');

    return new Uint8Array(executionHashByteSlice);
  }

  /**
   * Generates a cryptographic hash of the execution details
   * This hash is used to verify order integrity across chains
   * @returns SHA-256 hash of the execution details as a 0x-prefixed hex string
   * @throws {ValidationError} If hash generation fails
   */
  public getExecutionDetailsHash(): Hash {
    try {
      const executionDetails = this.getExecutionDetails();
      const executionDetailsString = JSON.stringify(executionDetails, Parsers.bigIntReplacer);

      const bytes = toBytes(executionDetailsString);
      return sha256(bytes);
    } catch (error) {
      throw new ValidationError(
        'Failed to generate execution details hash',
        error instanceof Error ? error : new Error(String(error)),
      );
    }
  }

  public toIntentRequest(preparedData: ChainPreparedData): CrossChainUserIntentRequest {
    const sourceChain = this.sourceChainId;
    const sourceChainType = chainIdToChainTypeMap[sourceChain];
    const executionDetails = JSON.stringify(this.getExecutionDetails(), Parsers.bigIntReplacer);

    return {
      genericData: this.toSourceChainData(),
      executionDetails,
      chainSpecificData: {
        [sourceChainType]: preparedData,
      },
    };
  }

  /**
   * Converts the order to the format required for source chain processing
   * Used when sending the order to the auctioneer for execution
   * @returns Object containing source chain-specific data
   */
  public toSourceChainData() {
    let data: SourceChainData = {
      user: this.user,
      srcChainId: this.sourceChainId,
      tokenIn: this.sourceTokenAddress,
      amountIn: this.sourceTokenAmount,
      minStablecoinsAmount: this.minStablecoinAmount,
      deadline: this.deadline,
      executionDetailsHash: this.getExecutionDetailsHash(),
      extraTransfers: this.extraTransfers,
    };

    if (this.stopLossMaxOut !== undefined) {
      data.stopLossMaxOut = this.stopLossMaxOut;
    }

    if (this.takeProfitMinOut !== undefined) {
      data.takeProfitMinOut = this.takeProfitMinOut;
    }

    return data;
  }

  /**
   * Serializes the order to a JSON-compatible object
   * Converts bigint values to strings to ensure proper JSON serialization
   * @returns JSON-serializable representation of the order
   */
  public toJSON() {
    return {
      user: this.user,
      sourceChainId: this.sourceChainId as number,
      sourceTokenAddress: this.sourceTokenAddress,
      sourceTokenAmount: this.sourceTokenAmount.toString(),
      destinationChainId: this.destinationChainId as number,
      destinationTokenAddress: this.destinationTokenAddress,
      destinationTokenMinAmount: this.destinationTokenMinAmount.toString(),
      destinationAddress: this.destinationAddress,
      minStablecoinAmount: this.minStablecoinAmount.toString(),
      deadline: this.deadline,
      executionDetailsHash: this.getExecutionDetailsHash(),
      extraTransfers: this.extraTransfers,
      stopLossMaxOut: this.stopLossMaxOut,
      takeProfitMinOut: this.takeProfitMinOut,
    };
  }

  public sendToAuctioneer(preparedData: ChainPreparedData): Promise<ApiResponse> {
    return BaseSDK.sendCrossChainOrder({
      order: this,
      preparedData,
    });
  }

  public async toEVMTypedData(): Promise<{ orderTypedData: TypedDataDefinition; nonce: bigint }> {
    if (!isEvmChain(this.sourceChainId)) {
      throw new ValidationError('Source chain is not EVM');
    }

    return getEVMCrossChainOrderTypedData(this);
  }

  public async toSolanaInstructionsByteArray() {
    if (this.sourceChainId !== ChainID.Solana) {
      throw new ValidationError('Source chain is not Solana');
    }

    return getSolanaCrossChainOrderInstructions(this);
  }

  public async toSuiTransaction() {
    if (this.sourceChainId !== ChainID.Sui) {
      throw new ValidationError('Source chain is not Sui');
    }

    return getSuiOrderTransaction(this);
  }
}
