import { BN } from '@project-serum/anchor';
import type { Wallet } from '@saberhq/solana-contrib';
import type {
  AccountMeta,
  Connection,
  PublicKey,
  Transaction
} from '@solana/web3.js';
import { Keypair, SystemProgram } from '@solana/web3.js';

import { tryGetAccount } from '../../utils/tryGetAccount';
import { withFindOrInitAssociatedTokenAccount } from '../../utils/withFindOrInitAssociatedTokenAccount';
import { getIdentifier } from './accounts';
import { BLAZE_ADDRESS } from './constants';
import {
  claimPrizeInstruction,
  closeBlazeInstruction,
  initBlazeInstruction,
  initIdentifierInstruction,
  redeemEntrantsInstruction,
  resolveBlazeInstruction,
  updateBlazeInstruction
} from './instruction';
import { findBlazeId, findIdentifierId } from './pda';

/**
 * Add init blaze identifier instructions to a transaction
 * @param transaction
 * @param connection
 * @param wallet
 * @returns Transaction, public key for the created blaze identifier
 */
export const withInitBlazeIdentifier = async (
  transaction: Transaction,
  connection: Connection,
  wallet: Wallet
): Promise<[Transaction, PublicKey]> => {
  const [identifierId] = await findIdentifierId();
  transaction.add(
    await initIdentifierInstruction(connection, wallet, {
      identifierId: identifierId
    })
  );
  return [transaction, identifierId];
};

export const withInitBlaze = async (
  transaction: Transaction,
  connection: Connection,
  wallet: Wallet,
  params: {
    prizeMint: PublicKey;
    tokenMint?: PublicKey;
    tokenAccount?: PublicKey;
    treasury?: PublicKey;
    burnRate?: number;
    entrantFee: BN;
    maxEntrants: number;
    start: BN;
    end: BN;
    maxEntrantsPerWalletRate?: number;
    projectId: PublicKey;
    projectWallet: PublicKey;
    category?: string;
  }
): Promise<[Transaction, PublicKey, Keypair]> => {
  const [[identifierId], identifierData] = await Promise.all([
    findIdentifierId(),
    tryGetAccount(() => getIdentifier(connection))
  ]);
  const identifier = (identifierData?.parsed.count as BN) || new BN(1);

  if (!identifierData) {
    transaction.add(
      await initIdentifierInstruction(connection, wallet, {
        identifierId: identifierId
      })
    );
  }

  const [blazeId] = await findBlazeId(identifier);

  const [blazePrizeMintTokenAccount, userPrizeMintTokenAccount] =
    await Promise.all([
      withFindOrInitAssociatedTokenAccount(
        transaction,
        connection,
        params.prizeMint,
        blazeId,
        wallet.publicKey,
        true
      ),
      withFindOrInitAssociatedTokenAccount(
        transaction,
        connection,
        params.prizeMint,
        wallet.publicKey,
        wallet.publicKey
      )
    ]);
  const entrantsAccountDataSize = 8 + 4 + 4 + 32 * params.maxEntrants;
  const entrantsKeyPair = Keypair.generate();
  const lamports = await connection.getMinimumBalanceForRentExemption(
    entrantsAccountDataSize,
    'confirmed'
  );

  transaction.add(
    SystemProgram.createAccount({
      fromPubkey: wallet.publicKey,
      newAccountPubkey: entrantsKeyPair.publicKey,
      space: entrantsAccountDataSize,
      lamports,
      programId: BLAZE_ADDRESS
    }),
    await initBlazeInstruction(connection, wallet, {
      identifierId,
      blazeId,
      projectId: params.projectId,
      projectWallet: params.projectWallet,
      blazePrizeMintTokenAccount,
      userPrizeMintTokenAccount,
      entrants: entrantsKeyPair.publicKey,
      tokenMint: params.tokenMint,
      tokenAccount: params.tokenAccount,
      treasury: params.treasury,
      burnRate: params.burnRate,
      category: params.category,
      prizeMint: params.prizeMint,
      entrantFee: params.entrantFee,
      maxEntrants: params.maxEntrants,
      start: params.start,
      end: params.end,
      maxEntrantsPerWalletRate: params.maxEntrantsPerWalletRate
    })
  );
  return [transaction, blazeId, entrantsKeyPair];
};

export const withUpdateBlaze = async (
  transaction: Transaction,
  connection: Connection,
  wallet: Wallet,
  params: {
    blazeId: PublicKey;
    projectId: PublicKey;

    tokenMint?: PublicKey;
    tokenAccount?: PublicKey;
    treasury?: PublicKey;
    burnRate?: number;

    entrantFee: BN;
    start: BN;
    end: BN;
    maxEntrantsPerWalletRate?: number;
    category?: string;
  }
): Promise<Transaction> => {
  transaction.add(
    await updateBlazeInstruction(connection, wallet, {
      blazeId: params.blazeId,
      projectId: params.projectId,

      tokenMint: params.tokenMint,
      tokenAccount: params.tokenAccount,
      treasury: params.treasury,
      burnRate: params.burnRate,

      entrantFee: params.entrantFee,
      start: params.start,
      end: params.end,
      maxEntrantsPerWalletRate: params.maxEntrantsPerWalletRate,
      category: params.category
    })
  );
  return transaction;
};

export const withCloseBlaze = async (
  transaction: Transaction,
  connection: Connection,
  wallet: Wallet,
  params: {
    blazeId: PublicKey;
    projectId: PublicKey;
    prizeMint: PublicKey;
    entrants: PublicKey;
  }
): Promise<Transaction> => {
  const [blazePrizeMintTokenAccount, userPrizeMintTokenAccount] =
    await Promise.all([
      withFindOrInitAssociatedTokenAccount(
        transaction,
        connection,
        params.prizeMint,
        params.blazeId,
        wallet.publicKey,
        true
      ),
      withFindOrInitAssociatedTokenAccount(
        transaction,
        connection,
        params.prizeMint,
        wallet.publicKey,
        wallet.publicKey
      )
    ]);
  transaction.add(
    await closeBlazeInstruction(connection, wallet, {
      blazeId: params.blazeId,
      projectId: params.projectId,
      entrants: params.entrants,
      blazePrizeMintTokenAccount,
      userPrizeMintTokenAccount
    })
  );
  return transaction;
};

export const withRedeemEntrants = async (
  transaction: Transaction,
  connection: Connection,
  wallet: Wallet,
  params: {
    quantity: number;
    blazeId: PublicKey;
    entrants: PublicKey;
    remainingAccountsForRedeem: AccountMeta[];
  }
): Promise<Transaction> => {
  transaction.add(await redeemEntrantsInstruction(connection, wallet, params));
  return transaction;
};

export const withResolveBlaze = async (
  transaction: Transaction,
  connection: Connection,
  wallet: Wallet,
  params: {
    blazeId: PublicKey;
    entrants: PublicKey;
  }
): Promise<Transaction> => {
  transaction.add(
    await resolveBlazeInstruction(connection, wallet, {
      blazeId: params.blazeId,
      entrants: params.entrants
    })
  );
  return transaction;
};

export const withClaimPrize = async (
  transaction: Transaction,
  connection: Connection,
  wallet: Wallet,
  params: {
    blazeId: PublicKey;
    prizeMint: PublicKey;
  }
): Promise<Transaction> => {
  const [blazePrizeMintTokenAccount, winnerPrizeMintTokenAccount] =
    await Promise.all([
      withFindOrInitAssociatedTokenAccount(
        transaction,
        connection,
        params.prizeMint,
        params.blazeId,
        wallet.publicKey,
        true
      ),
      withFindOrInitAssociatedTokenAccount(
        transaction,
        connection,
        params.prizeMint,
        wallet.publicKey,
        wallet.publicKey
      )
    ]);
  transaction.add(
    await claimPrizeInstruction(connection, wallet, {
      blazeId: params.blazeId,
      blazePrizeMintTokenAccount,
      winnerPrizeMintTokenAccount
    })
  );
  return transaction;
};
