import { createExecutePartialSaleInstruction, createExecuteSaleInstruction, createAuctioneerExecuteSaleInstruction, createPrintPurchaseReceiptInstruction } from '@metaplex-foundation/mpl-auction-house';
import { SYSVAR_INSTRUCTIONS_PUBKEY } from '@solarti/web3.js';
import { BidAndListingHaveDifferentAuctionHousesError, BidAndListingHaveDifferentMintsError, CanceledBidIsNotAllowedError, CanceledListingIsNotAllowedError, AuctioneerAuthorityRequiredError, AuctioneerPartialSaleNotSupportedError, PartialPriceMismatchError } from '../errors.mjs';
import { useOperation } from '../../../types/Operation.mjs';
import { now } from '../../../types/DateTime.mjs';
import { lamports, amount } from '../../../types/Amount.mjs';
import { TransactionBuilder } from '../../../utils/TransactionBuilder.mjs';
import { isSigner } from '../../../types/Signer.mjs';

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

const Key = 'ExecuteSaleOperation';

/**
 * Executes a sale on a given bid and listing.
 *
 * ```ts
 * await metaplex
 *   .auctionHouse()
 *   .executeSale({ auctionHouse, bid, listing };
 * ```
 *
 * @group Operations
 * @category Constructors
 */
const executeSaleOperation = useOperation(Key);

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

/**
 * Executes a sale on a given bid and listing.
 *
 * ```ts
 * const transactionBuilder = metaplex
 *   .auctionHouse()
 *   .builders()
 *   .executeSale({ auctionHouse, listing, bid });
 * ```
 *
 * @group Transaction Builders
 * @category Constructors
 */
const executeSaleOperationHandler = {
  async handle(operation, metaplex, scope) {
    const {
      auctionHouse
    } = operation.input;
    const output = await executeSaleBuilder(metaplex, operation.input, scope).sendAndConfirm(metaplex, scope.confirmOptions);
    scope.throwIfCanceled();
    if (output.receipt) {
      const purchase = await metaplex.auctionHouse().findPurchaseByReceipt({
        auctionHouse,
        receiptAddress: output.receipt
      }, scope);
      return {
        purchase,
        ...output
      };
    }
    const lazyPurchase = {
      model: 'purchase',
      lazy: true,
      auctionHouse: operation.input.auctionHouse,
      buyerAddress: output.buyer,
      sellerAddress: output.seller,
      metadataAddress: output.metadata,
      bookkeeperAddress: output.bookkeeper,
      receiptAddress: output.receipt,
      price: output.price,
      tokens: output.tokens.basisPoints,
      createdAt: now()
    };
    return {
      purchase: await metaplex.auctionHouse().loadPurchase({
        lazyPurchase
      }, scope),
      ...output
    };
  }
};

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

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

/**
 * @group Transaction Builders
 * @category Constructors
 */
const executeSaleBuilder = (metaplex, params, options = {}) => {
  const {
    programs,
    payer = metaplex.rpc().getDefaultFeePayer()
  } = options;
  const {
    auctionHouse,
    listing,
    bid,
    auctioneerAuthority
  } = params;
  const {
    sellerAddress,
    asset
  } = listing;
  const {
    buyerAddress
  } = bid;
  const {
    hasAuctioneer,
    isNative,
    treasuryMint,
    address: auctionHouseAddress,
    authorityAddress,
    feeAccountAddress,
    treasuryAccountAddress
  } = auctionHouse;
  const isPartialSale = bid.tokens.basisPoints < listing.tokens.basisPoints;

  // Use full size of listing & price when finding trade state PDA for the partial sale.
  const {
    tokens,
    price
  } = isPartialSale ? listing : bid;
  const {
    price: buyerPrice,
    tokens: buyerTokensSize
  } = bid;
  if (!listing.auctionHouse.address.equals(bid.auctionHouse.address)) {
    throw new BidAndListingHaveDifferentAuctionHousesError();
  }
  if (!listing.asset.address.equals(bid.asset.address)) {
    throw new BidAndListingHaveDifferentMintsError();
  }
  if (bid.canceledAt) {
    throw new CanceledBidIsNotAllowedError();
  }
  if (listing.canceledAt) {
    throw new CanceledListingIsNotAllowedError();
  }
  if (hasAuctioneer && !auctioneerAuthority) {
    throw new AuctioneerAuthorityRequiredError();
  }
  if (isPartialSale && hasAuctioneer) {
    throw new AuctioneerPartialSaleNotSupportedError();
  }
  if (isPartialSale) {
    const listingPricePerToken = price.basisPoints.div(tokens.basisPoints);
    const buyerPricePerToken = buyerPrice.basisPoints.div(buyerTokensSize.basisPoints);
    if (!listingPricePerToken.eq(buyerPricePerToken)) {
      throw new PartialPriceMismatchError(auctionHouse.isNative ? lamports(listingPricePerToken) : amount(listingPricePerToken, auctionHouse.treasuryMint.currency), auctionHouse.isNative ? lamports(buyerPricePerToken) : amount(buyerPricePerToken, auctionHouse.treasuryMint.currency));
    }
  }

  // Accounts.
  const sellerPaymentReceiptAccount = isNative ? sellerAddress : metaplex.tokens().pdas().associatedTokenAccount({
    mint: treasuryMint.address,
    owner: sellerAddress,
    programs
  });
  const buyerReceiptTokenAccount = metaplex.tokens().pdas().associatedTokenAccount({
    mint: asset.address,
    owner: buyerAddress,
    programs
  });
  const escrowPayment = metaplex.auctionHouse().pdas().buyerEscrow({
    auctionHouse: auctionHouseAddress,
    buyer: buyerAddress,
    programs
  });
  const freeTradeState = metaplex.auctionHouse().pdas().tradeState({
    auctionHouse: auctionHouseAddress,
    wallet: sellerAddress,
    treasuryMint: treasuryMint.address,
    tokenMint: asset.address,
    price: lamports(0).basisPoints,
    tokenSize: tokens.basisPoints,
    tokenAccount: asset.token.address,
    programs
  });
  const programAsSigner = metaplex.auctionHouse().pdas().programAsSigner();
  const accounts = {
    buyer: buyerAddress,
    seller: sellerAddress,
    tokenAccount: asset.token.address,
    tokenMint: asset.address,
    metadata: asset.metadataAddress,
    treasuryMint: treasuryMint.address,
    escrowPaymentAccount: escrowPayment,
    sellerPaymentReceiptAccount,
    buyerReceiptTokenAccount,
    authority: authorityAddress,
    auctionHouse: auctionHouseAddress,
    auctionHouseFeeAccount: feeAccountAddress,
    auctionHouseTreasury: treasuryAccountAddress,
    buyerTradeState: bid.tradeStateAddress,
    sellerTradeState: listing.tradeStateAddress,
    freeTradeState,
    programAsSigner
  };

  // Args.
  const args = {
    freeTradeStateBump: freeTradeState.bump,
    escrowPaymentBump: escrowPayment.bump,
    programAsSignerBump: programAsSigner.bump,
    buyerPrice: price.basisPoints,
    tokenSize: tokens.basisPoints
  };

  // Execute Sale Instruction
  const partialSaleArgs = {
    ...args,
    partialOrderSize: bid.tokens.basisPoints,
    partialOrderPrice: bid.price.basisPoints
  };
  let executeSaleInstruction = isPartialSale ? createExecutePartialSaleInstruction(accounts, partialSaleArgs) : createExecuteSaleInstruction(accounts, args);
  if (auctioneerAuthority) {
    const auctioneerAccounts = {
      ...accounts,
      auctioneerAuthority: auctioneerAuthority.publicKey,
      ahAuctioneerPda: metaplex.auctionHouse().pdas().auctioneer({
        auctionHouse: auctionHouse.address,
        auctioneerAuthority: auctioneerAuthority.publicKey,
        programs
      })
    };
    executeSaleInstruction = createAuctioneerExecuteSaleInstruction(auctioneerAccounts, args);
  }

  // Provide additional keys to pay royalties.
  asset.creators.forEach(({
    address
  }) => {
    executeSaleInstruction.keys.push({
      pubkey: address,
      isWritable: true,
      isSigner: false
    });

    // Provide ATA to receive SPL token royalty if is not native SOL sale.
    if (!isNative) {
      executeSaleInstruction.keys.push({
        pubkey: metaplex.tokens().pdas().associatedTokenAccount({
          mint: treasuryMint.address,
          owner: address,
          programs
        }),
        isWritable: true,
        isSigner: false
      });
    }
  });

  // Signers.
  const executeSaleSigners = [auctioneerAuthority].filter(isSigner);

  // Receipt.
  const shouldPrintReceipt = (params.printReceipt ?? true) && Boolean(listing.receiptAddress && bid.receiptAddress && !isPartialSale);
  const bookkeeper = params.bookkeeper ?? metaplex.identity();
  const purchaseReceipt = metaplex.auctionHouse().pdas().purchaseReceipt({
    listingTradeState: listing.tradeStateAddress,
    bidTradeState: bid.tradeStateAddress,
    programs
  });
  return TransactionBuilder.make().setFeePayer(payer).setContext({
    sellerTradeState: listing.tradeStateAddress,
    buyerTradeState: bid.tradeStateAddress,
    buyer: buyerAddress,
    seller: sellerAddress,
    metadata: asset.metadataAddress,
    bookkeeper: shouldPrintReceipt ? bookkeeper.publicKey : null,
    receipt: shouldPrintReceipt ? purchaseReceipt : null,
    price,
    tokens
  })

  // Execute Sale.
  .add({
    instruction: executeSaleInstruction,
    signers: executeSaleSigners,
    key: params.instructionKey ?? 'executeSale'
  })

  // Print the Purchase Receipt.
  .when(shouldPrintReceipt, builder => builder.add({
    instruction: createPrintPurchaseReceiptInstruction({
      purchaseReceipt,
      listingReceipt: listing.receiptAddress,
      bidReceipt: bid.receiptAddress,
      bookkeeper: bookkeeper.publicKey,
      instruction: SYSVAR_INSTRUCTIONS_PUBKEY
    }, {
      purchaseReceiptBump: purchaseReceipt.bump
    }),
    signers: [bookkeeper],
    key: 'printPurchaseReceipt'
  }));
};

export { executeSaleBuilder, executeSaleOperation, executeSaleOperationHandler };
//# sourceMappingURL=executeSale.mjs.map
