import {
  address,
  appendTransactionMessageInstructions,
  compileTransactionMessage,
  createNoopSigner,
  createSolanaRpc,
  createTransactionMessage,
  fetchEncodedAccount,
  getAddressEncoder,
  getCompiledTransactionMessageEncoder,
  getProgramDerivedAddress,
  pipe,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
  type Address,
  type IInstruction,
  type ReadonlyUint8Array,
  type TransactionMessageBytes,
} from '@solana/kit';
import { getDefaultSolanaRPC } from './client.js';
import { fetchMaybeLimitOrder, getCancelLimitOrderInstruction } from './generated/single-chain/index.js';
import {
  ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
  fetchMint,
  getCreateAssociatedTokenInstructionAsync,
} from '@solana-program/token';
import { CROSS_CHAIN_GUARD_ADDRESSES, SINGLE_CHAIN_GUARD_ADDRESSES, SOLANA_MINT_TOKEN } from '../../constants.js';
import { ChainID } from '../../chains.js';
import { fetchMaybeOrder, getCancelOrderInstruction } from './generated/cross-chain/index.js';

export async function cancelSingleChainOrderInstructions(
  orderAddress: string,
  options?: { rpcUrl?: string },
): Promise<IInstruction[]> {
  const rpc = options?.rpcUrl ? createSolanaRpc(options.rpcUrl) : getDefaultSolanaRPC();

  const orderId = address(orderAddress);
  const chainOrder = await fetchMaybeLimitOrder(rpc, orderId);

  if (!chainOrder.exists) {
    throw new Error(`Order with address ${orderAddress} not found`);
  }

  const instructions: IInstruction[] = [];
  const orderUserAddress = chainOrder.data.user;

  const tokenInMint = chainOrder.data.tokenInMint;

  const tokenMintProgram = await fetchMint(rpc, tokenInMint);

  const addressEncoder = getAddressEncoder();

  const [tokenInProgramAccount] = await getProgramDerivedAddress({
    programAddress: ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
    seeds: [
      addressEncoder.encode(orderUserAddress),
      addressEncoder.encode(tokenMintProgram.programAddress),
      addressEncoder.encode(tokenInMint),
    ],
  });

  const guardAddress = SINGLE_CHAIN_GUARD_ADDRESSES[ChainID.Solana] as Address;
  const [guardProgramAccount] = await getProgramDerivedAddress({
    programAddress: ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
    seeds: [
      // Owner
      addressEncoder.encode(guardAddress),
      // Token program
      addressEncoder.encode(tokenMintProgram.programAddress),
      // mint address
      addressEncoder.encode(tokenInMint),
    ],
  });

  const userTokenInAccount = await fetchEncodedAccount(rpc, tokenInProgramAccount);

  if (!userTokenInAccount.exists) {
    const createTokenIx = await getCreateAssociatedTokenInstructionAsync({
      mint: tokenInMint,
      owner: orderUserAddress,
      payer: createNoopSigner(orderUserAddress),
      tokenProgram: tokenMintProgram.programAddress,
    });

    instructions.push(createTokenIx);
  }

  const cancelLimitOrderIx = getCancelLimitOrderInstruction({
    user: createNoopSigner(orderUserAddress),
    order: orderId,
    guard: guardAddress,
    tokenInMint: chainOrder.data.tokenInMint,
    userTokenInAccount: userTokenInAccount.address,
    guardTokenInAccount: guardProgramAccount,
    tokenInProgram: tokenMintProgram.programAddress,
  });

  instructions.push(cancelLimitOrderIx);

  return instructions;
}

export async function cancelCrossChainOrderInstructionsAsBytes(
  orderAddress: string,
  signerAddress: string,
  options?: { rpcUrl?: string },
): Promise<{ versionedMessageBytes: ReadonlyUint8Array }> {
  const rpc = options?.rpcUrl ? createSolanaRpc(options.rpcUrl) : getDefaultSolanaRPC();
  const instructions = await cancelCrossChainOrderInstructions(orderAddress, options);
  const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: 'confirmed' }).send();

  const txMessage = pipe(
    createTransactionMessage({ version: 0 }),
    (tx) => setTransactionMessageFeePayerSigner(createNoopSigner(address(signerAddress)), tx),
    (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
    (tx) => appendTransactionMessageInstructions(instructions, tx),
  );

  const compiledTxMessage = compileTransactionMessage(txMessage);

  const txBytes = getCompiledTransactionMessageEncoder().encode(compiledTxMessage);

  return { versionedMessageBytes: txBytes };
}

export async function cancelSingleChainOrderInstructionsAsBytes(
  orderAddress: string,
  signerAddress: string,
  options?: { rpcUrl?: string },
): Promise<{ versionedMessageBytes: ReadonlyUint8Array }> {
  const rpc = options?.rpcUrl ? createSolanaRpc(options.rpcUrl) : getDefaultSolanaRPC();
  const instructions = await cancelSingleChainOrderInstructions(orderAddress, options);
  const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: 'confirmed' }).send();

  const txMessage = pipe(
    createTransactionMessage({ version: 0 }),
    (tx) => setTransactionMessageFeePayerSigner(createNoopSigner(address(signerAddress)), tx),
    (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
    (tx) => appendTransactionMessageInstructions(instructions, tx),
  );

  const compiledTxMessage = compileTransactionMessage(txMessage);

  const txBytes = getCompiledTransactionMessageEncoder().encode(compiledTxMessage) as TransactionMessageBytes;

  return { versionedMessageBytes: txBytes };
}

export async function cancelCrossChainOrderInstructions(
  orderAddress: string,
  options?: { rpcUrl?: string },
): Promise<IInstruction[]> {
  const rpc = options?.rpcUrl ? createSolanaRpc(options.rpcUrl) : getDefaultSolanaRPC();
  const orderId = address(orderAddress);

  const chainOrder = await fetchMaybeOrder(rpc, orderId);
  if (!chainOrder.exists) {
    throw new Error(`Order with address ${orderAddress} not found`);
  }

  const instructions: IInstruction[] = [];
  const orderUserAddress = chainOrder.data.user;
  const guardAddress = CROSS_CHAIN_GUARD_ADDRESSES[ChainID.Solana] as Address;
  const addressEncoder = getAddressEncoder();

  const isRecoveringTokenIn = chainOrder.data.lockedStablecoins === 0n;
  const recoverTokenMint = isRecoveringTokenIn ? chainOrder.data.tokenInMint : address(SOLANA_MINT_TOKEN.mint); // TODO: Use USDC for production

  const recoverTokenMintProgram = await fetchMint(rpc, recoverTokenMint);

  const [userRecoveredTokenAccount] = await getProgramDerivedAddress({
    programAddress: ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
    seeds: [
      addressEncoder.encode(orderUserAddress),
      addressEncoder.encode(recoverTokenMintProgram.programAddress),
      addressEncoder.encode(recoverTokenMint),
    ],
  });

  const [guardRecoveredTokenAccount] = await getProgramDerivedAddress({
    programAddress: ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
    seeds: [
      addressEncoder.encode(guardAddress),
      addressEncoder.encode(recoverTokenMintProgram.programAddress),
      addressEncoder.encode(recoverTokenMint),
    ],
  });

  // Check if user's recovered token account exists, create if needed
  const userRecoveredTokenAccountInfo = await fetchEncodedAccount(rpc, userRecoveredTokenAccount);
  if (!userRecoveredTokenAccountInfo.exists) {
    const createRecoveredTokenAccountIx = await getCreateAssociatedTokenInstructionAsync({
      payer: createNoopSigner(orderUserAddress),
      ata: userRecoveredTokenAccount,
      owner: orderUserAddress,
      mint: recoverTokenMint,
      tokenProgram: recoverTokenMintProgram.programAddress,
    });
    instructions.push(createRecoveredTokenAccountIx);
  }

  let userCollateralTokenAccount: Address | undefined;
  const willClaimCollateral = chainOrder.data.lockedCollateral > 0n;

  if (willClaimCollateral) {
    // Using SOLANA_DEFAULT_STABLECOIN as collateral mint for now
    const collateralTokenMint = address(SOLANA_MINT_TOKEN.mint);
    const collateralTokenMintProgram = await fetchMint(rpc, collateralTokenMint);

    const [userCollateralAccount] = await getProgramDerivedAddress({
      programAddress: ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
      seeds: [
        addressEncoder.encode(orderUserAddress),
        addressEncoder.encode(collateralTokenMintProgram.programAddress),
        addressEncoder.encode(collateralTokenMint),
      ],
    });

    userCollateralTokenAccount = userCollateralAccount;

    // Check if user's collateral token account exists, create if needed
    const userCollateralTokenAccountInfo = await fetchEncodedAccount(rpc, userCollateralTokenAccount);
    if (!userCollateralTokenAccountInfo.exists) {
      const createCollateralTokenAccountIx = await getCreateAssociatedTokenInstructionAsync({
        payer: createNoopSigner(orderUserAddress),
        ata: userCollateralTokenAccount,
        owner: orderUserAddress,
        mint: collateralTokenMint,
        tokenProgram: collateralTokenMintProgram.programAddress,
      });
      instructions.push(createCollateralTokenAccountIx);
    }
  }

  const collateralTokenMint = address(SOLANA_MINT_TOKEN.mint);
  const collateralTokenMintProgram = await fetchMint(rpc, collateralTokenMint);

  const [guardCollateralTokenAccount] = await getProgramDerivedAddress({
    programAddress: ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
    seeds: [
      addressEncoder.encode(guardAddress),
      addressEncoder.encode(collateralTokenMintProgram.programAddress),
      addressEncoder.encode(collateralTokenMint),
    ],
  });

  // Create the cancel order instruction
  const cancelOrderIx = getCancelOrderInstruction({
    user: createNoopSigner(orderUserAddress),
    order: orderId,
    guard: guardAddress,
    recoveredTokenMint: recoverTokenMint,
    userRecoveredTokenAccount: userRecoveredTokenAccount,
    guardRecoveredTokenAccount: guardRecoveredTokenAccount,
    recoveredTokenProgram: recoverTokenMintProgram.programAddress,
    collateralTokenMint: collateralTokenMint,
    userCollateralTokenAccount: userCollateralTokenAccount,
    guardCollateralTokenAccount: guardCollateralTokenAccount,
    collateralTokenProgram: collateralTokenMintProgram.programAddress,
  });

  instructions.push(cancelOrderIx);

  return instructions;
}
