import { TransactionAccount } from "@sqds/mesh/lib/types";
import SquadsMesh, { getIxPDA } from "@sqds/mesh";
import { PythCluster } from "@pythnetwork/client/lib/cluster";
import {
  AccountMeta,
  Commitment,
  PublicKey,
  SystemProgram,
  Transaction,
} from "@solana/web3.js";
import {
  MultisigParser,
  PythMultisigInstruction,
  WormholeMultisigInstruction,
} from "./multisig_transaction";
import BN from "bn.js";
import {
  deriveFeeCollectorKey,
  getWormholeBridgeData,
} from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole";
import { getCreateAccountWithSeedInstruction } from "./deterministic_oracle_accounts";
import { AccountType, parseProductData } from "@pythnetwork/client";
import { AnchorProvider } from "@project-serum/anchor";
import {
  TransactionBuilder,
  PriorityFeeConfig,
} from "@pythnetwork/solana-utils";

/**
 * Returns the instruction to pay the fee for a wormhole postMessage instruction
 * This instruction is usually not included in the proposals to save space and shorten the tx size
 * @param wormholeBridgeAddress address of the wormhole bridge program deployed on the cluster
 * @param squad
 */
export async function getPostMessageFeeInstruction(
  wormholeBridgeAddress: PublicKey,
  squad: SquadsMesh
) {
  const wormholeFee = wormholeBridgeAddress
    ? (
        await getWormholeBridgeData(
          squad.connection,
          wormholeBridgeAddress,
          "confirmed"
        )
      ).config.fee
    : 0;

  return SystemProgram.transfer({
    lamports: wormholeFee,
    toPubkey: deriveFeeCollectorKey(wormholeBridgeAddress),
    fromPubkey: squad.wallet.publicKey,
  });
}

/**
 * Continues executing a multisig proposal from the last executed instruction
 * Each instruction is executed in a separate transaction
 * Returns the signatures of the executed transactions
 * @param proposal the account which stores the proposed transaction
 * @param squad squad owning the proposal
 * @param cluster cluster the proposal and squad are on
 * @param commitment commitment level to use for RPC calls
 */
export async function executeProposal(
  proposal: TransactionAccount,
  squad: SquadsMesh,
  cluster: PythCluster,
  commitment: Commitment = "confirmed",
  priorityFeeConfig: PriorityFeeConfig
) {
  const multisigParser = MultisigParser.fromCluster(cluster);
  const signatures: string[] = [];
  for (
    let i = proposal.executedIndex + 1;
    i <= proposal.instructionIndex;
    i++
  ) {
    const instructionPda = getIxPDA(
      proposal.publicKey,
      new BN(i),
      squad.multisigProgramId
    )[0];
    const instruction = await squad.getInstruction(instructionPda);
    const parsedInstruction = multisigParser.parseInstruction({
      programId: instruction.programId,
      data: instruction.data as Buffer,
      keys: instruction.keys as AccountMeta[],
    });
    const transaction = new Transaction();

    if (
      parsedInstruction instanceof WormholeMultisigInstruction &&
      parsedInstruction.name == "postMessage"
    ) {
      transaction.add(
        await getPostMessageFeeInstruction(
          multisigParser.wormholeBridgeAddress!,
          squad
        )
      );
    } else if (
      parsedInstruction instanceof PythMultisigInstruction &&
      parsedInstruction.name == "addProduct"
    ) {
      /// Add product, fetch the symbol from the instruction
      transaction.add(
        await getCreateAccountWithSeedInstruction(
          squad.connection,
          cluster,
          squad.wallet.publicKey,
          parsedInstruction.args.symbol,
          AccountType.Product
        )
      );
    } else if (
      parsedInstruction instanceof PythMultisigInstruction &&
      parsedInstruction.name == "addPrice"
    ) {
      /// Add price, fetch the symbol from the product account
      const productAccount = await squad.connection.getAccountInfo(
        parsedInstruction.accounts.named.productAccount.pubkey
      );
      if (productAccount) {
        transaction.add(
          await getCreateAccountWithSeedInstruction(
            squad.connection,
            cluster,
            squad.wallet.publicKey,
            parseProductData(productAccount.data).product.symbol,
            AccountType.Price
          )
        );
      } else {
        throw Error("Product account not found");
      }
    }

    TransactionBuilder.addPriorityFee(transaction, priorityFeeConfig);

    transaction.add(
      await squad.buildExecuteInstruction(
        proposal.publicKey,
        getIxPDA(proposal.publicKey, new BN(i), squad.multisigProgramId)[0]
      )
    );

    signatures.push(
      await new AnchorProvider(squad.connection, squad.wallet, {
        commitment: commitment,
        preflightCommitment: commitment,
      }).sendAndConfirm(transaction, [], { skipPreflight: true })
    );
  }
  return signatures;
}
