import { parseVaa, postVaaSolana } from "@certusone/wormhole-sdk";
import { signTransactionFactory } from "@certusone/wormhole-sdk/lib/cjs/solana";
import { derivePostedVaaKey } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole";
import { AnchorProvider, BN, Program } from "@coral-xyz/anchor";
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
import { AccountType, parseProductData } from "@pythnetwork/client";
import {
  getPythClusterApiUrl,
  PythCluster,
} from "@pythnetwork/client/lib/cluster";
import {
  AccountMeta,
  Commitment,
  ComputeBudgetProgram,
  Connection,
  Keypair,
  PublicKey,
  TransactionInstruction,
} from "@solana/web3.js";
import * as fs from "fs";
import {
  decodeGovernancePayload,
  ExecutePostedVaa,
  getCreateAccountWithSeedInstruction,
  MultisigParser,
  PythMultisigInstruction,
  WORMHOLE_ADDRESS,
  WORMHOLE_API_ENDPOINT,
  CLAIM_RECORD_SEED,
  mapKey,
  REMOTE_EXECUTOR_ADDRESS,
  envOrErr,
} from "@pythnetwork/xc-admin-common";

const CLUSTER: PythCluster = envOrErr("CLUSTER") as PythCluster;
const EMITTER: PublicKey = new PublicKey(envOrErr("EMITTER"));
const KEYPAIR: Keypair = Keypair.fromSecretKey(
  Uint8Array.from(JSON.parse(fs.readFileSync(envOrErr("WALLET"), "ascii")))
);
const OFFSET: number = Number(process.env.OFFSET ?? "-1");
const SKIP_FAILED_REMOTE_INSTRUCTIONS: boolean =
  process.env.SKIP_FAILED_REMOTE_INSTRUCTIONS == "true";
const COMMITMENT: Commitment =
  (process.env.COMMITMENT as Commitment) ?? "confirmed";

const GUARDIAN_RPC = process.env.GUARDIAN_RPC;
const SOLANA_RPC_URL = process.env.SOLANA_RPC_URL;

async function run() {
  const provider = new AnchorProvider(
    new Connection(SOLANA_RPC_URL ?? getPythClusterApiUrl(CLUSTER), COMMITMENT),
    new NodeWallet(KEYPAIR),
    {
      commitment: COMMITMENT,
      preflightCommitment: COMMITMENT,
    }
  );
  const multisigParser = MultisigParser.fromCluster(CLUSTER);

  const remoteExecutor = await Program.at(REMOTE_EXECUTOR_ADDRESS, provider);

  const claimRecordAddress: PublicKey = PublicKey.findProgramAddressSync(
    [Buffer.from(CLAIM_RECORD_SEED), EMITTER.toBuffer()],
    remoteExecutor.programId
  )[0];
  const executorKey: PublicKey = mapKey(EMITTER);
  const claimRecord = await remoteExecutor.account.claimRecord.fetchNullable(
    claimRecordAddress
  );
  let lastSequenceNumber: number = claimRecord
    ? (claimRecord.sequence as BN).toNumber()
    : -1;
  lastSequenceNumber = Math.max(lastSequenceNumber, OFFSET);
  const wormholeApi = GUARDIAN_RPC ?? WORMHOLE_API_ENDPOINT[CLUSTER];
  const productAccountToSymbol: { [key: string]: string } = {};
  while (true) {
    lastSequenceNumber += 1;
    console.log(`Trying sequence number : ${lastSequenceNumber}`);

    const response = await (
      await fetch(
        `${wormholeApi}/v1/signed_vaa/1/${EMITTER.toBuffer().toString(
          "hex"
        )}/${lastSequenceNumber}`
      )
    ).json();

    if (response.vaaBytes) {
      const vaa = parseVaa(Buffer.from(response.vaaBytes, "base64"));
      const governancePayload = decodeGovernancePayload(vaa.payload);

      if (
        governancePayload instanceof ExecutePostedVaa &&
        governancePayload.targetChainId == "pythnet"
      ) {
        const preInstructions: TransactionInstruction[] = [];

        console.log(`Found VAA ${lastSequenceNumber}, relaying ...`);
        await postVaaSolana(
          provider.connection,
          signTransactionFactory(KEYPAIR),
          WORMHOLE_ADDRESS[CLUSTER]!,
          provider.wallet.publicKey,
          Buffer.from(response.vaaBytes, "base64"),
          { commitment: COMMITMENT }
        );

        let extraAccountMetas: AccountMeta[] = [
          { pubkey: executorKey, isSigner: false, isWritable: true },
        ];

        for (const ix of governancePayload.instructions) {
          extraAccountMetas.push({
            pubkey: ix.programId,
            isSigner: false,
            isWritable: false,
          });
          extraAccountMetas.push(
            ...ix.keys.filter((acc) => {
              return !acc.pubkey.equals(executorKey);
            })
          );

          const parsedInstruction = multisigParser.parseInstruction(ix);
          if (
            parsedInstruction instanceof PythMultisigInstruction &&
            parsedInstruction.name == "addProduct"
          ) {
            preInstructions.push(
              await getCreateAccountWithSeedInstruction(
                provider.connection,
                CLUSTER,
                provider.wallet.publicKey,
                parsedInstruction.args.symbol,
                AccountType.Product
              )
            );
            productAccountToSymbol[
              parsedInstruction.accounts.named.productAccount.pubkey.toBase58()
            ] = parsedInstruction.args.symbol;
          } else if (
            parsedInstruction instanceof PythMultisigInstruction &&
            parsedInstruction.name == "addPrice"
          ) {
            const productAccount = await provider.connection.getAccountInfo(
              parsedInstruction.accounts.named.productAccount.pubkey
            );
            const productSymbol = productAccount
              ? parseProductData(productAccount.data).product.symbol
              : productAccountToSymbol[
                  parsedInstruction.accounts.named.productAccount.pubkey.toBase58()
                ];
            if (productSymbol) {
              preInstructions.push(
                await getCreateAccountWithSeedInstruction(
                  provider.connection,
                  CLUSTER,
                  provider.wallet.publicKey,
                  productSymbol,
                  AccountType.Price
                )
              );
            } else {
              throw Error("Product account not found");
            }
          }
        }

        try {
          await remoteExecutor.methods
            .executePostedVaa()
            .accounts({
              claimRecord: claimRecordAddress,
              postedVaa: derivePostedVaaKey(
                WORMHOLE_ADDRESS[CLUSTER]!,
                vaa.hash
              ),
            })
            .remainingAccounts(extraAccountMetas)
            .preInstructions(preInstructions)
            // Use a high compute unit limit to avoid running out of compute units
            // as some operations can use a lot of compute units.
            .postInstructions([
              ComputeBudgetProgram.setComputeUnitLimit({ units: 1000000 }),
            ])
            .rpc({ skipPreflight: true });
        } catch (e) {
          if (SKIP_FAILED_REMOTE_INSTRUCTIONS) {
            console.error(e);
          } else throw e;
        }
      }
    } else if (response.code == 5) {
      console.log(`All VAAs have been relayed`);
      console.log(
        `${wormholeApi}/v1/signed_vaa/1/${EMITTER.toBuffer().toString(
          "hex"
        )}/${lastSequenceNumber}`
      );
      break;
    } else {
      throw new Error("Could not connect to wormhole api");
    }
  }
}

(async () => {
  try {
    await run();
  } catch (err) {
    console.error(err);
    throw new Error();
  }
})();
