// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import {
  PublicKey,
  VersionedTransaction,
  Connection,
  AccountMeta,
  SystemProgram,
  TransactionInstruction,
  TransactionMessage,
  AddressLookupTableAccount,
} from "@solana/web3.js";
import {
  getAssociatedTokenAddress,
  TOKEN_PROGRAM_ID,
  NATIVE_MINT,
  TOKEN_2022_PROGRAM_ID,
  ASSOCIATED_TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { BN } from "@coral-xyz/anchor";
import { Logger } from "../../utils/logger";
import { createErrorEnhancer } from "../../utils/errors";
import {
  CCIPContext,
  CCIPSendRequest,
  CCIPSendOptions,
  CCIPCoreConfig,
} from "../models";
import { CCIPAccountReader } from "./accounts";
import {
  ccipSend,
  CcipSendAccounts,
  CcipSendArgs,
} from "../../bindings/instructions/ccipSend";
import {
  findConfigPDA,
  findDestChainStatePDA,
  findNoncePDA,
  findFeeBillingSignerPDA,
  findFqConfigPDA,
  findFqDestChainPDA,
  findFqBillingTokenConfigPDA,
  findFqPerChainPerTokenConfigPDA,
  findRMNRemoteConfigPDA,
  findRMNRemoteCursesPDA,
  findTokenPoolChainConfigPDA,
} from "../../utils/pdas";

/**
 * Sends a CCIP message
 *
 * @param context SDK context with provider, config and logger
 * @param request Send request parameters
 * @param accountReader Account reader instance
 * @param computeBudgetInstruction Optional compute budget instruction
 * @param sendOptions Optional send options (skipPreflight, etc.)
 * @returns Transaction signature
 */
export async function sendCCIPMessage(
  context: CCIPContext,
  request: CCIPSendRequest,
  accountReader: CCIPAccountReader,
  computeBudgetInstruction?: TransactionInstruction,
  sendOptions?: CCIPSendOptions,
): Promise<VersionedTransaction> {
  if (!context.logger) {
    throw new Error("Logger is required for sendCCIPMessage");
  }

  const logger = context.logger;
  const config = context.config;
  const connection = context.provider.connection;
  const enhanceError = createErrorEnhancer(logger);

  // Determine if we're using native SOL
  const isNativeSol = request.feeToken.equals(PublicKey.default);

  // For native SOL, we use NATIVE_MINT as the token mint
  const feeTokenMint = isNativeSol ? NATIVE_MINT : request.feeToken;

  // Determine the correct fee token program ID
  let feeTokenProgramId = TOKEN_PROGRAM_ID;
  if (!isNativeSol) {
    try {
      const feeTokenMintInfo = await connection.getAccountInfo(feeTokenMint);
      if (feeTokenMintInfo) {
        feeTokenProgramId = feeTokenMintInfo.owner;
      } else {
        feeTokenProgramId = TOKEN_2022_PROGRAM_ID;
      }
    } catch (error) {
      feeTokenProgramId = TOKEN_2022_PROGRAM_ID;
    }
  }

  const selectorBigInt = BigInt(request.destChainSelector.toString());
  const signerPublicKey = context.provider.getAddress();

  // Build the accounts for the ccipSend instruction
  const accounts = await buildCCIPSendAccounts(
    config,
    selectorBigInt,
    request,
    feeTokenMint,
    feeTokenProgramId,
    isNativeSol,
    signerPublicKey,
    logger,
  );

  // Build token indexes and accounts
  const { tokenIndexes, remainingAccounts, lookupTableList } =
    await buildTokenAccountsForSend(
      request,
      connection,
      feeTokenProgramId,
      accountReader,
      logger,
      config,
      signerPublicKey,
    );

  // Create the args for the ccipSend instruction
  const args: CcipSendArgs = {
    destChainSelector: request.destChainSelector,
    message: {
      receiver: request.receiver,
      data: request.data,
      tokenAmounts: request.tokenAmounts,
      feeToken: request.feeToken,
      extraArgs: request.extraArgs,
    },
    tokenIndexes: new Uint8Array(tokenIndexes),
  };

  // Create the ccipSend instruction
  const instruction = ccipSend(args, accounts, config.ccipRouterProgramId);

  // Add remaining accounts to the instruction
  if (remainingAccounts.length > 0) {
    instruction.keys.push(...remainingAccounts);
  }

  const { blockhash, lastValidBlockHeight } =
    await connection.getLatestBlockhash({
      commitment: "finalized", // Using finalized for longer validity
    });

  // Create the transaction instructions array
  const instructions: TransactionInstruction[] = [];

  // Add compute budget instruction if provided
  if (computeBudgetInstruction) {
    instructions.push(computeBudgetInstruction);
  }

  // Add the ccipSend instruction
  instructions.push(instruction);

  // Create the transaction
  const messageV0 = new TransactionMessage({
    payerKey: signerPublicKey,
    recentBlockhash: blockhash,
    instructions,
  }).compileToV0Message(lookupTableList);

  const tx = new VersionedTransaction(messageV0);
  const signedTx = await context.provider.signTransaction(tx);

  return signedTx;
}

/**
 * Build accounts required for the ccipSend instruction
 */
async function buildCCIPSendAccounts(
  config: CCIPCoreConfig,
  selectorBigInt: bigint,
  request: CCIPSendRequest,
  feeTokenMint: PublicKey,
  feeTokenProgramId: PublicKey,
  isNativeSol: boolean,
  signerPublicKey: PublicKey,
  logger: Logger,
): Promise<CcipSendAccounts> {
  const enhanceError = createErrorEnhancer(logger);

  try {
    logger.info(
      `Building accounts for CCIP send to chain ${selectorBigInt.toString()}`,
    );
    logger.debug(
      `Fee token: ${feeTokenMint.toString()} (${
        isNativeSol ? "Native SOL" : "SPL Token"
      })`,
    );

    // Find all the PDAs needed for the ccipSend instruction
    const [configPDA] = findConfigPDA(config.ccipRouterProgramId);
    const [destChainState] = findDestChainStatePDA(
      selectorBigInt,
      config.ccipRouterProgramId,
    );
    const [nonce] = findNoncePDA(
      selectorBigInt,
      signerPublicKey,
      config.ccipRouterProgramId,
    );
    const [feeBillingSigner] = findFeeBillingSignerPDA(
      config.ccipRouterProgramId,
    );
    const [feeQuoterConfig] = findFqConfigPDA(config.feeQuoterProgramId);
    const [fqDestChain] = findFqDestChainPDA(
      selectorBigInt,
      config.feeQuoterProgramId,
    );
    const [fqBillingTokenConfig] = findFqBillingTokenConfigPDA(
      feeTokenMint,
      config.feeQuoterProgramId,
    );
    const [fqLinkBillingTokenConfig] = findFqBillingTokenConfigPDA(
      config.linkTokenMint,
      config.feeQuoterProgramId,
    );
    const [rmnRemoteCurses] = findRMNRemoteCursesPDA(config.rmnRemoteProgramId);
    const [rmnRemoteConfig] = findRMNRemoteConfigPDA(config.rmnRemoteProgramId);

    // Get the associated token accounts for the user and fee billing signer
    logger.debug(
      `Deriving token accounts for fee token: ${feeTokenMint.toString()}`,
    );

    const userFeeTokenAccount = isNativeSol
      ? PublicKey.default // For native SOL we use the default public key
      : await getAssociatedTokenAddress(
          feeTokenMint,
          signerPublicKey,
          true,
          feeTokenProgramId,
          ASSOCIATED_TOKEN_PROGRAM_ID,
        );

    const feeBillingSignerFeeTokenAccount = await getAssociatedTokenAddress(
      feeTokenMint,
      feeBillingSigner,
      true,
      feeTokenProgramId,
      ASSOCIATED_TOKEN_PROGRAM_ID,
    );

    return {
      authority: signerPublicKey,
      config: configPDA,
      destChainState: destChainState,
      nonce: nonce,
      systemProgram: SystemProgram.programId,
      feeTokenProgram: feeTokenProgramId,
      feeTokenMint: feeTokenMint,
      feeTokenUserAssociatedAccount: userFeeTokenAccount,
      feeTokenReceiver: feeBillingSignerFeeTokenAccount,
      feeBillingSigner: feeBillingSigner,
      feeQuoter: config.feeQuoterProgramId,
      feeQuoterConfig: feeQuoterConfig,
      feeQuoterDestChain: fqDestChain,
      feeQuoterBillingTokenConfig: fqBillingTokenConfig,
      feeQuoterLinkTokenConfig: fqLinkBillingTokenConfig,
      rmnRemote: config.rmnRemoteProgramId,
      rmnRemoteCurses: rmnRemoteCurses,
      rmnRemoteConfig: rmnRemoteConfig,
    };
  } catch (error) {
    // Use enhanceError to add context and properly log the error
    throw enhanceError(error, {
      operation: "buildCCIPSendAccounts",
      destChainSelector: selectorBigInt.toString(),
      feeToken: feeTokenMint.toString(),
      isNativeSol: isNativeSol,
    });
  }
}

/**
 * Build token accounts and indexes for CCIP send
 */
async function buildTokenAccountsForSend(
  request: CCIPSendRequest,
  connection: Connection,
  feeTokenProgramId: PublicKey,
  accountReader: CCIPAccountReader,
  logger: Logger,
  config: CCIPCoreConfig,
  signerPublicKey: PublicKey,
): Promise<{
  tokenIndexes: number[];
  remainingAccounts: AccountMeta[];
  lookupTableList: AddressLookupTableAccount[];
}> {
  const enhanceError = createErrorEnhancer(logger);
  logger.debug(
    `Building token accounts for ${request.tokenAmounts.length} tokens`,
  );

  // Setup token accounts
  const tokenIndexes: number[] = [];
  const remainingAccounts: AccountMeta[] = [];
  const lookupTableList: AddressLookupTableAccount[] = [];
  let lastIndex = 0;

  // Process each token amount
  for (const tokenAmount of request.tokenAmounts) {
    try {
      const tokenMint = tokenAmount.token;
      logger.debug(
        `Processing token: ${tokenMint.toString()}, amount: ${tokenAmount.amount.toString()}`,
      );

      // Determine token program from token mint
      let tokenProgram = feeTokenProgramId;
      try {
        const tokenMintInfo = await connection.getAccountInfo(tokenMint);
        if (tokenMintInfo) {
          tokenProgram = tokenMintInfo.owner;
          logger.debug(
            `Auto-detected token program: ${tokenProgram.toString()} for token: ${tokenMint.toString()}`,
          );
        } else {
          logger.warn(
            `Token mint info not found for ${tokenMint.toString()}, using fallback token program`,
          );
        }
      } catch (error) {
        logger.warn(
          `Error determining token program, using fallback: ${
            error instanceof Error ? error.message : String(error)
          }`,
        );
      }

      // Get token admin registry for this token to access lookup table
      const tokenAdminRegistry = await accountReader.getTokenAdminRegistry(
        tokenMint,
      );
      logger.debug(
        `Retrieved token admin registry for ${tokenMint.toString()}`,
      );

      // Get lookup table for this token
      const lookupTable = await getLookupTableAccount(
        connection,
        tokenAdminRegistry.lookupTable,
        logger,
      );
      lookupTableList.push(lookupTable);

      // Get the lookup table addresses
      const lookupTableAddresses = lookupTable.state.addresses;

      // Extract pool program from lookup table
      const poolProgram = getPoolProgram(lookupTableAddresses, logger);

      // Get user token account - use the signer public key
      const userTokenAccount = await getAssociatedTokenAddress(
        tokenMint,
        signerPublicKey,
        true,
        tokenProgram,
        ASSOCIATED_TOKEN_PROGRAM_ID,
      );

      // Get token chain config
      const [tokenBillingConfig] = findFqPerChainPerTokenConfigPDA(
        BigInt(request.destChainSelector.toString()),
        tokenMint,
        config.feeQuoterProgramId,
      );

      // Get pool chain config
      const [poolChainConfig] = findTokenPoolChainConfigPDA(
        BigInt(request.destChainSelector.toString()),
        tokenMint,
        poolProgram,
      );

      // Build token accounts using lookup table
      const tokenAccounts = buildTokenLookupAccounts(
        userTokenAccount,
        tokenBillingConfig,
        poolChainConfig,
        lookupTableAddresses,
        tokenAdminRegistry.writableIndexes,
        logger,
      );

      tokenIndexes.push(lastIndex);
      const currentLen = tokenAccounts.length;
      lastIndex += currentLen;
      remainingAccounts.push(...tokenAccounts);

      logger.debug(
        `Added ${currentLen} token-specific accounts for ${tokenMint.toString()}`,
      );
    } catch (error) {
      throw enhanceError(error, {
        operation: "buildTokenAccountsForSend",
        token: tokenAmount.token.toString(),
        amount: tokenAmount.amount.toString(),
      });
    }
  }

  return { tokenIndexes, remainingAccounts, lookupTableList };
}

/**
 * Gets an address lookup table account
 */
async function getLookupTableAccount(
  connection: Connection,
  lookupTableAddress: PublicKey,
  logger: Logger,
): Promise<AddressLookupTableAccount> {
  const enhanceError = createErrorEnhancer(logger);
  logger.debug(`Fetching lookup table: ${lookupTableAddress.toString()}`);

  const { value: lookupTableAccount } = await connection.getAddressLookupTable(
    lookupTableAddress,
  );

  if (!lookupTableAccount) {
    throw enhanceError(
      new Error(`Lookup table not found: ${lookupTableAddress.toString()}`),
      {
        operation: "getLookupTableAccount",
        lookupTableAddress: lookupTableAddress.toString(),
      },
    );
  }

  if (lookupTableAccount.state.addresses.length < 7) {
    throw enhanceError(
      new Error(
        `Lookup table has insufficient accounts: ${lookupTableAccount.state.addresses.length} (needs at least 7)`,
      ),
      {
        operation: "getLookupTableAccount",
        lookupTableAddress: lookupTableAddress.toString(),
        addressCount: lookupTableAccount.state.addresses.length,
      },
    );
  }

  logger.trace(
    `Lookup table fetched with ${lookupTableAccount.state.addresses.length} addresses`,
  );
  return lookupTableAccount;
}

/**
 * Extracts the pool program from lookup table addresses
 */
function getPoolProgram(
  lookupTableAddresses: PublicKey[],
  logger: Logger,
): PublicKey {
  const enhanceError = createErrorEnhancer(logger);
  // The pool program is at index 2 in the lookup table
  if (lookupTableAddresses.length <= 2) {
    throw enhanceError(
      new Error(
        "Lookup table doesn't have enough entries to determine pool program",
      ),
      {
        operation: "getPoolProgram",
        addressCount: lookupTableAddresses.length,
      },
    );
  }

  const poolProgram = lookupTableAddresses[2];
  logger.debug(
    `Using pool program: ${poolProgram.toString()} (index 2 in lookup table)`,
  );

  return poolProgram;
}

/**
 * Build token accounts using lookup table
 */
function buildTokenLookupAccounts(
  userTokenAccount: PublicKey,
  tokenBillingConfig: PublicKey,
  poolChainConfig: PublicKey,
  lookupTableEntries: Array<PublicKey>,
  writableIndexes: BN[],
  logger: Logger,
): Array<AccountMeta> {
  // First entry is the lookup table itself
  const lookupTable = lookupTableEntries[0];

  logger.trace("Building token lookup accounts", {
    userTokenAccount: userTokenAccount.toString(),
    tokenBillingConfig: tokenBillingConfig.toString(),
    poolChainConfig: poolChainConfig.toString(),
    lookupTableAddress: lookupTable.toString(),
    entriesCount: lookupTableEntries.length,
  });

  // Build the token accounts with the correct writable flags
  const accounts = [
    { pubkey: userTokenAccount, isSigner: false, isWritable: true },
    { pubkey: tokenBillingConfig, isSigner: false, isWritable: false },
    { pubkey: poolChainConfig, isSigner: false, isWritable: true },

    // First account is the lookup table - must be non-writable
    { pubkey: lookupTable, isSigner: false, isWritable: false },
  ];

  // Add the remaining lookup table entries with correct writable flags
  const remainingAccounts = lookupTableEntries.slice(1).map((pubkey, index) => {
    const isWrit = isWritable(index + 1, writableIndexes, logger);
    return {
      pubkey,
      isSigner: false,
      isWritable: isWrit,
    };
  });

  return [...accounts, ...remainingAccounts];
}

/**
 * Checks if an account should be writable based on writable indexes bitmap
 */
function isWritable(
  index: number,
  writableIndexes: BN[],
  logger?: Logger,
): boolean {
  // For the lookup table access, index 0 is determined by the program requirements
  // The lookup table itself must be NON-writable
  if (index === 0) {
    return false;
  }

  // For other accounts, check the writable indexes bitmap
  // Each BN in writableIndexes represents a 256-bit mask
  const bnIndex = Math.floor(index / 128);

  // In the Rust code, bits are set from left to right
  const bitPosition = bnIndex === 0 ? 127 - (index % 128) : 255 - (index % 128);

  if (bnIndex < writableIndexes.length) {
    // Create a BN with the bit at the position we want to check
    const mask = new BN(1).shln(bitPosition);

    // Check if the bit is set using bitwise AND
    const result = writableIndexes[bnIndex].and(mask);

    // If the result is not zero, the bit is set
    return !result.isZero();
  }

  // Default to non-writable if index is out of bounds
  return false;
}
