import { ChainID, chainIdToChainTypeMap, isEvmChain, type SupportedChain } from '../../chains.js';
import { ValidationError } from '../../errors/index.js';
import type { ApiResponse } from '../../types/api.js';
import type { SingleChainPreparedData, SingleChainUserIntentRequest } from '../../types/intent.js';
import { SingleChainOrderValidator } from '../../utils/order-validator.js';
import { QuoteProvider } from '../../utils/quote/aggregator.js';
import { getEVMSingleChainOrderTypedData } from '../evm/order-signature.js';
import { BaseSDK } from '../sdk.js';
import { getSolanaSingleChainOrderInstructions } from '../solana/order-instructions.js';
import { type ExtraTransfer } from './common.js';

export type CreateSingleChainOrderParams = {
  chainId: SupportedChain;
  amountIn: bigint;
  tokenIn: string;
  tokenOut: string;
  amountOutMin?: bigint;
  destinationAddress: string;
  extraTransfers?: ExtraTransfer[];
  deadline: number;

  stopLossMaxOut?: bigint;
  takeProfitMinOut?: bigint;
};

type OrderScenario = 'QUOTE_REQUIRED' | 'USE_PROVIDED_AMOUNT' | 'STOP_LOSS_ONLY' | 'BOTH_PROVIDED';

export class SingleChainOrder {
  public user: string;
  public chainId: SupportedChain;
  public amountIn: bigint;
  public tokenIn: string;
  public tokenOut: string;
  public amountOutMin: bigint;
  public destinationAddress: string;
  public extraTransfers?: ExtraTransfer[];
  public deadline: number;
  public stopLossMaxOut?: bigint;
  public takeProfitMinOut?: bigint;

  private constructor(params: CreateSingleChainOrderParams & { user: string }) {
    this.user = params.user;
    this.chainId = params.chainId;
    this.amountIn = params.amountIn;
    this.tokenIn = params.tokenIn;
    this.tokenOut = params.tokenOut;
    this.amountOutMin = params.amountOutMin ?? 1n;
    this.destinationAddress = params.destinationAddress;
    this.extraTransfers = params.extraTransfers;
    this.deadline = params.deadline;
    this.stopLossMaxOut = params.stopLossMaxOut;
    this.takeProfitMinOut = params.takeProfitMinOut;
  }

  public static async create(input: CreateSingleChainOrderParams & { user: string }): Promise<SingleChainOrder> {
    new SingleChainOrderValidator().validateOrder(input);

    const amountOutMin = await this.calculateAmountOutMin(input);

    const order = new SingleChainOrder({
      ...input,
      amountOutMin,
      user: input.user,
    });

    const preparedRandomData = order.getRandomPreparedData();
    const intentRequest = order.toIntentRequest(preparedRandomData);
    await BaseSDK.validateSingleChainOrder(intentRequest);

    return order;
  }

  /// This is needed because API requires the prepared data to be send
  /// 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(): SingleChainPreparedData {
    const chainId = this.chainId;
    const chainType = chainIdToChainTypeMap[chainId];

    const randomNumber = String(Math.floor(Math.random() * 10000000));

    switch (chainType) {
      case 'EVM': {
        return { nonce: randomNumber, signature: '0x0000000000000000000000000000000000000000000000000000000000000000' };
      }
      case 'Solana': {
        return { secretNumber: randomNumber, orderPubkey: 'DFNAjFAvS4GF98Tp1kiyLvEHM3wjGXibCfF86nnmhuVc' };
      }
      case 'Sui': {
        const digest = 'FQWndwYJhNQUoHyvR8UuhGURC2EKx9eWErFm9Tc2DggF';
        return { transactionHash: digest };
      }

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

  private static async calculateAmountOutMin(input: CreateSingleChainOrderParams): Promise<bigint> {
    const { amountOutMin, stopLossMaxOut } = input;

    const scenario = this.getSingleChainOrderScenario({
      hasAmountOutMin: !!amountOutMin,
      hasStopLoss: !!stopLossMaxOut,
    });

    switch (scenario) {
      case 'QUOTE_REQUIRED':
        const quote = await QuoteProvider.getSingleChainQuote({
          tokenIn: input.tokenIn,
          amount: input.amountIn,
          chainId: input.chainId,
          tokenOut: input.tokenOut,
        });
        return (quote.amountOut * 93n) / 100n; // Add 7% reduced to cover fees

      case 'USE_PROVIDED_AMOUNT':
        return amountOutMin!;

      case 'STOP_LOSS_ONLY':
      case 'BOTH_PROVIDED':
        // When stop loss is involved, amountOutMin should be 1
        return 1n;
    }
  }

  private static getSingleChainOrderScenario({
    hasAmountOutMin,
    hasStopLoss,
  }: {
    hasAmountOutMin: boolean;
    hasStopLoss: boolean;
  }): OrderScenario {
    if (!hasAmountOutMin && !hasStopLoss) return 'QUOTE_REQUIRED';
    if (hasAmountOutMin && !hasStopLoss) return 'USE_PROVIDED_AMOUNT';
    if (!hasAmountOutMin && hasStopLoss) return 'STOP_LOSS_ONLY';
    return 'BOTH_PROVIDED';
  }

  public toIntentRequest(preparedData: SingleChainPreparedData): SingleChainUserIntentRequest {
    const sourceChainType = chainIdToChainTypeMap[this.chainId];

    return {
      genericData: this,
      chainSpecificData: {
        [sourceChainType]: preparedData,
      },
    };
  }

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

  public async toEVMTypedData() {
    if (!isEvmChain(this.chainId)) {
      throw new ValidationError('Chain id is not an Ethereum compatible chain');
    }

    return getEVMSingleChainOrderTypedData(this);
  }

  public async toSolanaInstructionsByteArray() {
    if (this.chainId !== ChainID.Solana) {
      throw new ValidationError('Chain id is not Solana');
    }

    return getSolanaSingleChainOrderInstructions(this);
  }
}
