/**
 * This file contains code adapted from the Jupiter Protocol's instruction parser
 * Source: https://github.com/jup-ag/instruction-parser
 * Modified for use in XSwap
 */
import { Event, Program, Provider } from "@coral-xyz/anchor";
import { AccountInfo, Connection, PublicKey } from "@solana/web3.js";
import { JUPITER_V6_PROGRAM_ID } from "@src/constants";
import { IDL, Jupiter } from "@src/contracts/idl/jupiter";
import { SwapEvent, TransactionWithMeta } from "@src/models/SolanaTransaction";
import { getEvents } from "@src/services/solana/jupiter/get-events";
import { InstructionParser } from "@src/services/solana/jupiter/instruction-parser";
import { BigNumberUtil } from "@src/services/solana/jupiter/utils";
import BigNumber from "bignumber.js";
import { unpackMint } from "@solana/spl-token";
import { Transaction } from "@src/models";

export const program = new Program<Jupiter>(
  IDL,
  JUPITER_V6_PROGRAM_ID,
  {} as Provider,
);

type AccountInfoMap = Map<string, AccountInfo<Buffer>>;

export type SwapAttributes = Transaction;

const reduceEventData = <T>(events: Event[], name: string) =>
  events.reduce((acc, event) => {
    if (event.name === name) {
      acc.push(event.data as T);
    }
    return acc;
  }, new Array<T>());

export async function extractSolanaSwapData(
  signature: string,
  connection: Connection,
  tx: TransactionWithMeta,
): Promise<SwapAttributes | undefined> {
  const programId = JUPITER_V6_PROGRAM_ID;
  const accountInfosMap: AccountInfoMap = new Map();

  const logMessages = tx.meta?.logMessages;
  if (!logMessages) {
    throw new Error("Missing log messages...");
  }

  const parser = new InstructionParser(programId);
  const events = getEvents(program, tx);

  const swapEvents = reduceEventData<SwapEvent>(events, "SwapEvent");

  if (swapEvents.length === 0) {
    // Not a swap event, for example: https://solscan.io/tx/5ZSozCHmAFmANaqyjRj614zxQY8HDXKyfAs2aAVjZaadS4DbDwVq8cTbxmM5m5VzDcfhysTSqZgKGV1j2A2Hqz1V
    return;
  }

  const accountsToBeFetched = new Array<PublicKey>();
  swapEvents.forEach((swapEvent) => {
    accountsToBeFetched.push(swapEvent.inputMint);
    accountsToBeFetched.push(swapEvent.outputMint);
  });

  const accountInfos = await connection.getMultipleAccountsInfo(
    accountsToBeFetched,
  );
  accountsToBeFetched.forEach((account, index) => {
    const accountInfo = accountInfos[index];
    if (accountInfo) {
      accountInfosMap.set(account.toBase58(), accountInfo);
    }
  });

  const swapData = await parseSwapEvents(accountInfosMap, swapEvents);
  const instructions = parser.getInstructions(tx);
  const positions = parser.getInitialAndFinalSwapPositions(instructions);
  if (!positions) {
    throw new Error("Failed to get initial and final swap positions");
  }

  const [initialPositions, finalPositions] = positions as [number[], number[]];
  const initialPosition = initialPositions[0] as number;
  const finalPosition = finalPositions[0] as number;

  const inMint = swapData[initialPosition]?.inMint as string;
  const inSwapData = swapData.filter(
    (swap, index) =>
      initialPositions?.includes(index) && swap.inMint === inMint,
  );

  const inAmount = inSwapData.reduce((acc, curr) => {
    return acc.plus(new BigNumber(curr.inAmount || "0"));
  }, new BigNumber(0));

  const outMint = swapData[finalPosition]?.outMint;
  const outSwapData = swapData.filter(
    (swap, index) =>
      finalPositions?.includes(index) && swap.outMint === outMint,
  );
  const outAmount = outSwapData.reduce((acc, curr) => {
    return acc.plus(new BigNumber(curr.outAmount || "0"));
  }, new BigNumber(0));

  const isSuccessful = tx.meta?.status?.Ok === null;

  const swap = {} as SwapAttributes;

  swap.hash = signature;
  swap.timestamp = tx.blockTime ?? 0;

  swap.amountWei = inAmount.toString();
  swap.tokenAddress = inMint;

  swap.tokenOutAmount = outAmount.toString();
  swap.tokenOutAddress = outMint;

  swap.sourceChainId = "mainnet-beta";
  swap.targetChainId = "mainnet-beta";

  swap.status = isSuccessful ? "DONE" : "REVERTED";

  return swap;
}

async function parseSwapEvents(
  accountInfosMap: AccountInfoMap,
  swapEvents: SwapEvent[],
) {
  const swapData = await Promise.all(
    swapEvents.map((swapEvent) => extractSwapData(accountInfosMap, swapEvent)),
  );

  return swapData;
}

async function extractSwapData(
  accountInfosMap: AccountInfoMap,
  swapEvent: SwapEvent,
) {
  const inMint = swapEvent.inputMint.toBase58();
  const inAmount = swapEvent.inputAmount.toString();
  const inTokenDecimals = extractMintDecimals(
    accountInfosMap,
    swapEvent.inputMint,
  );
  const inAmountInDecimal = BigNumberUtil.fromBN(
    swapEvent.inputAmount,
    inTokenDecimals,
  );

  const outMint = swapEvent.outputMint.toBase58();
  const outAmount = swapEvent.outputAmount.toString();
  const outTokenDecimals = extractMintDecimals(
    accountInfosMap,
    swapEvent.outputMint,
  );
  const outAmountInDecimal = BigNumberUtil.fromBN(
    swapEvent.outputAmount,
    outTokenDecimals,
  );

  return {
    inMint,
    inAmount,
    inAmountInDecimal,
    outMint,
    outAmount,
    outAmountInDecimal,
  };
}

function extractMintDecimals(accountInfosMap: AccountInfoMap, mint: PublicKey) {
  const mintData = accountInfosMap.get(mint.toBase58());

  if (mintData) {
    const mintInfo = unpackMint(mint, mintData, mintData.owner);
    return mintInfo.decimals;
  }

  return;
}
