import {
  getTransferSolInstruction,
  SYSTEM_PROGRAM_ADDRESS,
} from "@solana-program/system";
import {
  ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
  fetchMint,
  getCreateAssociatedTokenInstructionAsync,
  getSyncNativeInstruction,
} from "@solana-program/token";
import {
  type Address,
  address,
  addSignersToTransactionMessage,
  appendTransactionMessageInstructions,
  createNoopSigner,
  createSolanaRpc,
  createTransactionMessage,
  fetchEncodedAccount,
  generateKeyPairSigner,
  getAddressEncoder,
  getProgramDerivedAddress,
  getTransactionCodec,
  type IInstruction,
  partiallySignTransactionMessageWithSigners,
  pipe,
  type ReadonlyUint8Array,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
} from "@solana/kit";
import { ChainID } from "../../chains.js";
import {
  CROSS_CHAIN_GUARD_ADDRESSES,
  NATIVE_SOLANA_TOKEN_ADDRESS,
  SINGLE_CHAIN_GUARD_ADDRESSES,
  WRAPPED_SOL_MINT_ADDRESS,
} from "../../constants.js";
import type { CrossChainOrder } from "../orders/cross-chain.js";
import type { SingleChainOrder } from "../orders/single-chain.js";
import { getDefaultSolanaRPC } from "./client.js";
import { getCreateOrderInstruction } from "./generated/cross-chain/index.js";
import { getCreateLimitOrderInstructionAsync } from "./generated/single-chain/index.js";
import { genSecretHashAndNumber } from "./utils.js";
import { Keypair as UtilsKeypair } from "@nealireverse_dev/utils";
export type SolanaOrderInstructionResult = {
  // Order address
  orderAddress: string;
  // Encoded transaction bytes to be later deserialized into a transaction type
  txBytes: ReadonlyUint8Array;
};

export async function getSolanaSingleChainOrderInstructions(
  order: SingleChainOrder,
  options?: { rpcUrl?: string }
): Promise<SolanaOrderInstructionResult & { secretNumber: string }> {
  const rpc = options?.rpcUrl
    ? createSolanaRpc(options.rpcUrl)
    : getDefaultSolanaRPC();

  const orderSigner = await generateKeyPairSigner();
  const signer = createNoopSigner(order.user as Address);
  UtilsKeypair.from(orderSigner);

  let tokenInMint = address(order.tokenIn);

  const { secretHash, secretNumber } = genSecretHashAndNumber(order);

  const orderUserAddress = address(order.user);
  const spendingNative = tokenInMint === NATIVE_SOLANA_TOKEN_ADDRESS;

  if (spendingNative) {
    tokenInMint = WRAPPED_SOL_MINT_ADDRESS;
    order.tokenIn = WRAPPED_SOL_MINT_ADDRESS;
  }

  const tokenMintProgram = await fetchMint(rpc, tokenInMint);

  const guardAddress = address(SINGLE_CHAIN_GUARD_ADDRESSES[ChainID.Solana]);
  const addressEncoder = getAddressEncoder();
  const instructions: Array<IInstruction> = [];

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

  const userTokenInAccount = await fetchEncodedAccount(
    rpc,
    tokenInProgramAccount
  );

  if (!userTokenInAccount.exists) {
    const createAccountIx = await getCreateAssociatedTokenInstructionAsync({
      payer: signer,
      ata: tokenInProgramAccount,
      owner: orderUserAddress,
      mint: tokenMintProgram.address,
      tokenProgram: tokenMintProgram.programAddress,
    });

    instructions.push(createAccountIx);
  }

  if (spendingNative) {
    const transferIx = getTransferSolInstruction({
      amount: order.amountIn,
      destination: userTokenInAccount.address,
      source: signer,
    });

    instructions.push(transferIx);

    const syncNativeIx = getSyncNativeInstruction({
      account: userTokenInAccount.address,
    });

    instructions.push(syncNativeIx);
  }

  const createSingleChainLimitOrderIx =
    await getCreateLimitOrderInstructionAsync({
      user: signer,
      order: orderSigner,
      guard: guardAddress,
      tokenInMint: tokenInMint,
      userTokenInAccount: userTokenInAccount.address,
      tokenInProgram: tokenMintProgram.programAddress,
      amountIn: order.amountIn,
      deadline: order.deadline,
      amountOutMin: order.amountOutMin,
      secretHash: Uint8Array.from(secretHash),
      extraTransfersAmounts: [], // TODO
    });

  instructions.push(createSingleChainLimitOrderIx);

  const { value: latestBlockhash } = await rpc
    .getLatestBlockhash({ commitment: "confirmed" })
    .send();

  const txMessage = pipe(
    createTransactionMessage({ version: 0 }),
    (tx) => setTransactionMessageFeePayerSigner(signer, tx),
    (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
    (tx) => appendTransactionMessageInstructions(instructions, tx),
    (tx) => addSignersToTransactionMessage([orderSigner], tx)
  );

  const partiallySignedTransaction =
    await partiallySignTransactionMessageWithSigners(txMessage);

  const txBytes = getTransactionCodec().encode(partiallySignedTransaction);

  return {
    orderAddress: orderSigner.address,
    txBytes,
    secretNumber,
  };
}

export async function getSolanaCrossChainOrderInstructions(
  order: CrossChainOrder,
  options?: { rpcUrl?: string }
): Promise<SolanaOrderInstructionResult> {
  const rpc = options?.rpcUrl
    ? createSolanaRpc(options.rpcUrl)
    : getDefaultSolanaRPC();
  const orderSigner = await generateKeyPairSigner();
  const signer = createNoopSigner(order.user as Address);

  let tokenInMint = address(order.sourceTokenAddress);

  const orderUserAddress = address(order.user);
  const spendingNative = tokenInMint === NATIVE_SOLANA_TOKEN_ADDRESS;

  if (spendingNative) {
    tokenInMint = WRAPPED_SOL_MINT_ADDRESS;
  }

  const tokenMintProgram = await fetchMint(rpc, tokenInMint);

  const guardAddress = address(CROSS_CHAIN_GUARD_ADDRESSES[ChainID.Solana]);

  const addressEncoder = getAddressEncoder();

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

  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 instructions: Array<IInstruction> = [];

  const userTokenInAccount = await fetchEncodedAccount(
    rpc,
    tokenInProgramAccount
  );

  if (spendingNative) {
    order.sourceTokenAddress = WRAPPED_SOL_MINT_ADDRESS;

    if (!userTokenInAccount.exists) {
      const createAccountIx = await getCreateAssociatedTokenInstructionAsync({
        payer: signer,
        ata: tokenInProgramAccount,
        owner: orderUserAddress,
        mint: tokenMintProgram.address,
        tokenProgram: tokenMintProgram.programAddress,
      });

      instructions.push(createAccountIx);
    }

    const transferIx = getTransferSolInstruction({
      source: signer,
      destination: userTokenInAccount.address,
      amount: order.sourceTokenAmount,
    });

    const syncNativeIx = getSyncNativeInstruction({
      account: userTokenInAccount.address,
    });

    instructions.push(transferIx);
    instructions.push(syncNativeIx);
  }

  const executionHashUint8Array = order.executionDetailsHashToBytes();

  const createOrderIx = getCreateOrderInstruction({
    user: signer,
    order: orderSigner,
    guard: address(CROSS_CHAIN_GUARD_ADDRESSES[ChainID.Solana]),
    systemProgram: SYSTEM_PROGRAM_ADDRESS,
    associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
    tokenInMint: tokenInMint,
    userTokenInAccount: userTokenInAccount.address,
    guardTokenInAccount: guardProgramAccount,
    tokenInProgram: tokenMintProgram.programAddress,
    amountIn: order.sourceTokenAmount,
    deadline: order.deadline,
    executionDetailsHash: executionHashUint8Array,
    minStablecoinsAmount: order.minStablecoinAmount,
  });

  instructions.push(createOrderIx);

  const { value: latestBlockhash } = await rpc
    .getLatestBlockhash({ commitment: "confirmed" })
    .send();

  const txMessage = pipe(
    createTransactionMessage({ version: 0 }),
    (tx) => setTransactionMessageFeePayerSigner(signer, tx),
    (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
    (tx) => appendTransactionMessageInstructions(instructions, tx),
    (tx) => addSignersToTransactionMessage([orderSigner], tx)
  );

  const partiallySignedTransaction =
    await partiallySignTransactionMessageWithSigners(txMessage);

  const txBytes = getTransactionCodec().encode(partiallySignedTransaction);

  return {
    orderAddress: orderSigner.address,
    txBytes: Uint8Array.from(txBytes),
  };
}
