import BN from 'bn.js';
import { PublicKey } from '@solana/web3.js';
import { Wallet } from '../wallet';
import { Connection } from '../Connection';
import { sendTransaction } from './transactions';
import { Auction, AuctionExtended, BidderMetadata } from '@metaplex-foundation/mpl-auction';
import { TransactionsBatch } from '../utils/transactions-batch';
import {
  AuctionManager,
  PrizeTrackingTicket,
  SafetyDepositConfig,
} from '@metaplex-foundation/mpl-metaplex';
import { Vault } from '@metaplex-foundation/mpl-token-vault';
import {
  Edition,
  EditionMarker,
  MasterEdition,
  Metadata,
  UpdatePrimarySaleHappenedViaToken,
} from '@renec-foundation/mpl-token-metadata';
import { RedeemPrintingV2Bid } from '@metaplex-foundation/mpl-metaplex';
import { prepareTokenAccountAndMintTxs } from './shared';
import { getBidRedemptionPDA } from './redeemFullRightsTransferBid';

export interface RedeemBidParams {
  connection: Connection;
  wallet: Wallet;
  auction: PublicKey;
  store: PublicKey;
}

export interface RedeemBidResponse {
  txId: string;
}

export interface RedeemBidTransactionsParams {
  bidder: PublicKey;
  bidderPotToken?: PublicKey;
  bidderMeta: PublicKey;
  auction: PublicKey;
  auctionExtended: PublicKey;
  destination: PublicKey;
  vault: PublicKey;
  store: PublicKey;
  auctionManager: PublicKey;
  bidRedemption: PublicKey;
  safetyDepositTokenStore: PublicKey;
  safetyDeposit: PublicKey;
  safetyDepositConfig: PublicKey;
  metadata: PublicKey;
  newMint: PublicKey;
  newMetadata: PublicKey;
  newEdition: PublicKey;
  masterEdition: PublicKey;
  editionMarker: PublicKey;
  prizeTrackingTicket: PublicKey;
  winIndex: BN;
  editionOffset: BN;
}

export const redeemPrintingV2Bid = async ({
  connection,
  wallet,
  store,
  auction,
}: RedeemBidParams): Promise<RedeemBidResponse> => {
  const bidder = wallet.publicKey;
  const {
    data: { bidState },
  } = await Auction.load(connection, auction);
  const auctionManagerPDA = await AuctionManager.getPDA(auction);
  const manager = await AuctionManager.load(connection, auctionManagerPDA);
  const vault = await Vault.load(connection, manager.data.vault);
  const auctionExtendedPDA = await AuctionExtended.getPDA(vault.pubkey);
  const [safetyDepositBox] = await vault.getSafetyDepositBoxes(connection);
  const originalMint = new PublicKey(safetyDepositBox.data.tokenMint);

  const safetyDepositTokenStore = new PublicKey(safetyDepositBox.data.store);
  const bidderMetaPDA = await BidderMetadata.getPDA(auction, bidder);
  const bidRedemptionPDA = await getBidRedemptionPDA(auction, bidderMetaPDA);
  const safetyDepositConfigPDA = await SafetyDepositConfig.getPDA(
    auctionManagerPDA,
    safetyDepositBox.pubkey,
  );

  const { mint, createMintTx, createAssociatedTokenAccountTx, mintToTx, recipient } =
    await prepareTokenAccountAndMintTxs(connection, wallet.publicKey);

  const newMint = mint.publicKey;
  const newMetadataPDA = await Metadata.getPDA(newMint);
  const newEditionPDA = await Edition.getPDA(newMint);

  const metadataPDA = await Metadata.getPDA(originalMint);
  const masterEditionPDA = await MasterEdition.getPDA(originalMint);
  const masterEdition = await MasterEdition.load(connection, masterEditionPDA);

  const prizeTrackingTicketPDA = await PrizeTrackingTicket.getPDA(auctionManagerPDA, originalMint);

  let prizeTrackingTicket: PrizeTrackingTicket;
  // this account doesn't exist when we do redeem for the first time
  try {
    prizeTrackingTicket = await PrizeTrackingTicket.load(connection, prizeTrackingTicketPDA);
  } catch (e) {
    prizeTrackingTicket = null;
  }

  const winIndex = bidState.getWinnerIndex(bidder.toBase58()) || 0;

  const editionOffset = getEditionOffset(winIndex);
  const editionBase = prizeTrackingTicket?.data.supplySnapshot || masterEdition.data.supply;
  const desiredEdition = editionBase.add(editionOffset);
  const editionMarkerPDA = await EditionMarker.getPDA(originalMint, desiredEdition);

  // checking if edition marker is taken
  try {
    const editionMarker = await EditionMarker.load(connection, editionMarkerPDA);
    const isEditionTaken = editionMarker.data.editionTaken(desiredEdition.toNumber());
    if (isEditionTaken) {
      throw new Error('The edition is already taken');
    }
  } catch (e) {
    // it's not. continue
  }

  const txBatch = await getRedeemPrintingV2BidTransactions({
    bidder,
    bidderMeta: bidderMetaPDA,
    store,
    vault: vault.pubkey,
    destination: recipient,
    auction,
    auctionExtended: auctionExtendedPDA,
    auctionManager: auctionManagerPDA,
    safetyDepositTokenStore,
    safetyDeposit: safetyDepositBox.pubkey,
    bidRedemption: bidRedemptionPDA,
    safetyDepositConfig: safetyDepositConfigPDA,

    metadata: metadataPDA,
    newMint,
    newMetadata: newMetadataPDA,
    newEdition: newEditionPDA,
    masterEdition: masterEditionPDA,
    editionMarker: editionMarkerPDA,
    prizeTrackingTicket: prizeTrackingTicketPDA,
    editionOffset,
    winIndex: new BN(winIndex),
  });

  txBatch.addSigner(mint);
  txBatch.addBeforeTransaction(createMintTx);
  txBatch.addBeforeTransaction(createAssociatedTokenAccountTx);
  txBatch.addBeforeTransaction(mintToTx);

  const txId = await sendTransaction({
    connection,
    wallet,
    txs: txBatch.toTransactions(),
    signers: txBatch.signers,
  });

  return { txId };
};

export const getRedeemPrintingV2BidTransactions = async ({
  bidder,
  destination,
  store,
  vault,
  auction,
  auctionManager,
  auctionExtended,
  bidRedemption,
  bidderMeta: bidMetadata,
  safetyDepositTokenStore,
  safetyDeposit,
  safetyDepositConfig,

  metadata,
  newMint,
  newMetadata,
  newEdition,
  masterEdition,
  editionMarker: editionMark,
  prizeTrackingTicket,

  winIndex,
  editionOffset,
}: RedeemBidTransactionsParams) => {
  const txBatch = new TransactionsBatch({ transactions: [] });

  const redeemPrintingV2BidTx = new RedeemPrintingV2Bid(
    { feePayer: bidder },
    {
      store,
      vault,
      auction,
      auctionManager,
      bidRedemption,
      bidMetadata,
      safetyDepositTokenStore,
      destination,
      safetyDeposit,
      bidder,
      safetyDepositConfig,
      auctionExtended,

      newMint,
      newEdition,
      newMetadata,
      metadata,
      masterEdition,
      editionMark,
      prizeTrackingTicket,
      winIndex,
      editionOffset,
    },
  );
  txBatch.addTransaction(redeemPrintingV2BidTx);

  const updatePrimarySaleHappenedViaTokenTx = new UpdatePrimarySaleHappenedViaToken(
    { feePayer: bidder },
    {
      metadata: newMetadata,
      owner: bidder,
      tokenAccount: destination,
    },
  );
  txBatch.addTransaction(updatePrimarySaleHappenedViaTokenTx);

  return txBatch;
};

export function getEditionOffset(winIndex: number) {
  const offset = new BN(1);
  // NOTE: not sure if this the right way to calculate it
  return offset.add(new BN(winIndex));
}
