import { createBuyInstruction, createPublicBuyInstruction, createAuctioneerBuyInstruction, createAuctioneerPublicBuyInstruction, createPrintBidReceiptInstruction } from '@metaplex-foundation/mpl-auction-house';
import { SYSVAR_INSTRUCTIONS_PUBKEY } from '@solarti/web3.js';
import { AuctioneerAuthorityRequiredError } from '../errors.mjs';
import { now } from '../../../types/DateTime.mjs';
import { token, lamports, amount } from '../../../types/Amount.mjs';
import { useOperation, makeConfirmOptionsFinalizedOnMainnet } from '../../../types/Operation.mjs';
import { toPublicKey } from '../../../types/PublicKey.mjs';
import { isSigner } from '../../../types/Signer.mjs';
import { TransactionBuilder } from '../../../utils/TransactionBuilder.mjs';

// -----------------
// Operation
// -----------------

const Key = 'CreateBidOperation';

/**
 * Creates a bid on a given asset.
 *
 * You can post a public bid on a non-listed NFT by skipping seller and tokenAccount properties.
 * Public bids are specific to the token itself and not to any specific auction.
 * This means that a bid can stay active beyond the end of an auction
 * and be resolved if it meets the criteria for subsequent auctions of that token.
 *
 *
 * ```ts
 * await metaplex
 *   .auctionHouse()
 *   .createBid({ auctionHouse, mintAccount, seller };
 * ```
 *
 * @group Operations
 * @category Constructors
 */
const createBidOperation = useOperation(Key);

/**
 * @group Operations
 * @category Types
 */

/**
 * @group Operations
 * @category Handlers
 */
const createBidOperationHandler = {
  async handle(operation, metaplex, scope) {
    const {
      auctionHouse
    } = operation.input;
    const builder = await createBidBuilder(metaplex, operation.input, scope);
    const confirmOptions = makeConfirmOptionsFinalizedOnMainnet(metaplex, scope.confirmOptions);
    const output = await builder.sendAndConfirm(metaplex, confirmOptions);
    scope.throwIfCanceled();
    if (output.receipt) {
      const bid = await metaplex.auctionHouse().findBidByReceipt({
        auctionHouse,
        receiptAddress: output.receipt
      }, scope);
      return {
        bid,
        ...output
      };
    }
    scope.throwIfCanceled();
    const lazyBid = {
      model: 'bid',
      lazy: true,
      auctionHouse,
      tradeStateAddress: output.buyerTradeState,
      bookkeeperAddress: output.bookkeeper,
      tokenAddress: output.tokenAccount,
      buyerAddress: output.buyer,
      metadataAddress: output.metadata,
      receiptAddress: output.receipt,
      purchaseReceiptAddress: null,
      isPublic: Boolean(output.tokenAccount),
      price: output.price,
      tokens: output.tokens.basisPoints,
      createdAt: now(),
      canceledAt: null
    };
    return {
      bid: await metaplex.auctionHouse().loadBid({
        lazyBid
      }, scope),
      ...output
    };
  }
};

// -----------------
// Builder
// -----------------

/**
 * @group Transaction Builders
 * @category Inputs
 */

/**
 * Creates a bid on a given asset.
 *
 * You can post a public bid on a non-listed NFT by skipping seller and tokenAccount properties.
 * Public bids are specific to the token itself and not to any specific auction.
 * This means that a bid can stay active beyond the end of an auction
 * and be resolved if it meets the criteria for subsequent auctions of that token.
 *
 *
 * ```ts
 * const transactionBuilder = metaplex
 *   .auctionHouse()
 *   .builders()
 *   .createBid({ auctionHouse, mintAccount, seller })
 * ```
 *
 * @group Transaction Builders
 * @category Constructors
 */
const createBidBuilder = async (metaplex, params, options = {}) => {
  // Data.
  const {
    programs,
    payer = metaplex.rpc().getDefaultFeePayer()
  } = options;
  const {
    auctionHouse
  } = params;
  const tokens = params.tokens ?? token(1);
  const priceBasisPoint = params.price?.basisPoints ?? 0;
  const price = auctionHouse.isNative ? lamports(priceBasisPoint) : amount(priceBasisPoint, auctionHouse.treasuryMint.currency);
  if (auctionHouse.hasAuctioneer && !params.auctioneerAuthority) {
    throw new AuctioneerAuthorityRequiredError();
  }

  // Accounts.
  const buyer = params.buyer ?? metaplex.identity();
  const authority = params.authority ?? auctionHouse.authorityAddress;
  const metadata = metaplex.nfts().pdas().metadata({
    mint: params.mintAccount,
    programs
  });
  const paymentAccount = auctionHouse.isNative ? toPublicKey(buyer) : metaplex.tokens().pdas().associatedTokenAccount({
    mint: auctionHouse.treasuryMint.address,
    owner: toPublicKey(buyer),
    programs
  });
  const escrowPayment = metaplex.auctionHouse().pdas().buyerEscrow({
    auctionHouse: auctionHouse.address,
    buyer: toPublicKey(buyer),
    programs
  });
  const tokenAccount = params.tokenAccount ?? (params.seller ? metaplex.tokens().pdas().associatedTokenAccount({
    mint: params.mintAccount,
    owner: params.seller,
    programs
  }) : null);
  const buyerTokenAccount = metaplex.tokens().pdas().associatedTokenAccount({
    mint: params.mintAccount,
    owner: toPublicKey(buyer),
    programs
  });
  const buyerTradeState = metaplex.auctionHouse().pdas().tradeState({
    auctionHouse: auctionHouse.address,
    wallet: toPublicKey(buyer),
    treasuryMint: auctionHouse.treasuryMint.address,
    tokenMint: params.mintAccount,
    price: price.basisPoints,
    tokenSize: tokens.basisPoints,
    tokenAccount,
    programs
  });
  const accounts = {
    wallet: toPublicKey(buyer),
    paymentAccount,
    transferAuthority: toPublicKey(buyer),
    treasuryMint: auctionHouse.treasuryMint.address,
    metadata,
    escrowPaymentAccount: escrowPayment,
    authority: toPublicKey(authority),
    auctionHouse: auctionHouse.address,
    auctionHouseFeeAccount: auctionHouse.feeAccountAddress,
    buyerTradeState
  };

  // Args.
  const args = {
    tradeStateBump: buyerTradeState.bump,
    escrowPaymentBump: escrowPayment.bump,
    buyerPrice: price.basisPoints,
    tokenSize: tokens.basisPoints
  };

  // Sell Instruction.
  let buyInstruction = tokenAccount ? createBuyInstruction({
    ...accounts,
    tokenAccount
  }, args) : createPublicBuyInstruction({
    ...accounts,
    tokenAccount: buyerTokenAccount
  }, args);
  if (params.auctioneerAuthority) {
    const ahAuctioneerPda = metaplex.auctionHouse().pdas().auctioneer({
      auctionHouse: auctionHouse.address,
      auctioneerAuthority: params.auctioneerAuthority.publicKey,
      programs
    });
    const accountsWithAuctioneer = {
      ...accounts,
      auctioneerAuthority: params.auctioneerAuthority.publicKey,
      ahAuctioneerPda
    };
    buyInstruction = tokenAccount ? createAuctioneerBuyInstruction({
      ...accountsWithAuctioneer,
      tokenAccount
    }, args) : createAuctioneerPublicBuyInstruction({
      ...accountsWithAuctioneer,
      tokenAccount: buyerTokenAccount
    }, args);
  }

  // Signers.
  const buySigners = [buyer, authority, params.auctioneerAuthority].filter(isSigner);

  // Update the accounts to be signers since it's not covered properly by MPL due to its dynamic nature.
  buySigners.forEach(signer => {
    const signerKeyIndex = buyInstruction.keys.findIndex(({
      pubkey
    }) => pubkey.equals(signer.publicKey));
    buyInstruction.keys[signerKeyIndex].isSigner = true;
  });

  // Receipt.
  // Since createPrintBidReceiptInstruction can't deserialize createAuctioneerBuyInstruction due to a bug
  // Don't print Auctioneer Bid receipt for the time being.
  const shouldPrintReceipt = (params.printReceipt ?? true) && !params.auctioneerAuthority;
  const bookkeeper = params.bookkeeper ?? metaplex.identity();
  const receipt = metaplex.auctionHouse().pdas().bidReceipt({
    tradeState: buyerTradeState,
    programs
  });
  const builder = TransactionBuilder.make().setFeePayer(payer).setContext({
    buyerTradeState,
    tokenAccount,
    metadata,
    buyer: toPublicKey(buyer),
    receipt: shouldPrintReceipt ? receipt : null,
    bookkeeper: shouldPrintReceipt ? bookkeeper.publicKey : null,
    price,
    tokens
  });

  // Create a TA for public bid if it doesn't exist
  if (!tokenAccount) {
    const account = await metaplex.rpc().getAccount(buyerTokenAccount);
    if (!account.exists) {
      builder.add(await metaplex.tokens().builders().createToken({
        mint: params.mintAccount,
        owner: toPublicKey(buyer)
      }));
    }
  }
  return builder
  // Create bid.
  .add({
    instruction: buyInstruction,
    signers: buySigners,
    key: 'buy'
  })

  // Print the Bid Receipt.
  .when(shouldPrintReceipt, builder => builder.add({
    instruction: createPrintBidReceiptInstruction({
      receipt,
      bookkeeper: bookkeeper.publicKey,
      instruction: SYSVAR_INSTRUCTIONS_PUBKEY
    }, {
      receiptBump: receipt.bump
    }),
    signers: [bookkeeper],
    key: 'printBidReceipt'
  }));
};

export { createBidBuilder, createBidOperation, createBidOperationHandler };
//# sourceMappingURL=createBid.mjs.map
