import {
  AccountUpdate,
  DeployArgs,
  method,
  Permissions,
  PublicKey,
  State,
  state,
  UInt64,
  SmartContract,
  Bool,
  Field,
  Struct,
  VerificationKey,
} from "o1js";
/**
 * The BulletinBoard contract serves as a centralized event emitter for NFT marketplace activities.
 * It provides a standardized way to broadcast and track various marketplace events such as:
 * - New collection listings
 * - Offers made on NFTs
 * - Offer cancellations
 * - Bids placed on NFTs
 * - Bid cancellations
 * - Completed sales
 *
 * While anyone can emit events through this contract, all events are prefixed with "BB_" to
 * distinguish them from events emitted directly by NFT contracts. This helps maintain clarity
 * in event tracking and marketplace activity monitoring.
 *
 * The BulletinBoard does not handle any NFT transfers or escrow - it purely exists as an
 * event broadcasting mechanism to help indexers and UIs track marketplace activity.
 */

export {
  BB_NewCollectionEvent,
  BB_OfferEvent,
  BB_CancelOfferEvent,
  BB_BidEvent,
  BB_CancelBidEvent,
  BB_SaleEvent,
  BB_UpgradeVerificationKeyEvent,
  BB_ChangeAdminEvent,
  BulletinBoard,
  BulletinBoardDeployProps,
};

class BB_NewCollectionEvent extends Struct({
  /** The collection address. */
  collection: PublicKey,
}) {}

class BB_OfferEvent extends Struct({
  /** The collection address. */
  collection: PublicKey,
  /** The NFT address. */
  nft: PublicKey,
  /** The offer address. */
  offer: PublicKey,
  /** The price. */
  price: UInt64,
}) {}

class BB_CancelOfferEvent extends Struct({
  /** The collection address. */
  collection: PublicKey,
  /** The NFT address. */
  nft: PublicKey,
}) {}

class BB_BidEvent extends Struct({
  /** The collection address. */
  collection: PublicKey,
  /** The NFT address. */
  nft: PublicKey,
  /** The bid address. */
  bid: PublicKey,
  /** The price. */
  price: UInt64,
}) {}

class BB_CancelBidEvent extends Struct({
  /** The collection address. */
  collection: PublicKey,
  /** The NFT address. */
  nft: PublicKey,
  /** The bid address. */
  bid: PublicKey,
}) {}

class BB_SaleEvent extends Struct({
  /** The collection address. */
  collection: PublicKey,
  /** The NFT address. */
  nft: PublicKey,
  /** The buyer address. */
  buyer: PublicKey,
  /** The price. */
  price: UInt64,
}) {}

class BB_UpgradeVerificationKeyEvent extends Struct({
  /** The new verification key. */
  vk: Field,
}) {}

class BB_ChangeAdminEvent extends Struct({
  /** The new admin. */
  admin: PublicKey,
}) {}

interface BulletinBoardDeployProps extends Exclude<DeployArgs, undefined> {
  /** The admin. */
  admin: PublicKey;
  /** The fee. */
  fee?: UInt64;
}
/**
 * The BulletinBoard contract serves as a centralized event emitter for NFT marketplace activities.
 * It provides a standardized way to broadcast and track various marketplace events such as:
 * - New collection listings
 * - Offers made on NFTs
 * - Offer cancellations
 * - Bids placed on NFTs
 * - Bid cancellations
 * - Completed sales
 *
 * While anyone can emit events through this contract, all events are prefixed with "BB_" to
 * distinguish them from events emitted directly by NFT contracts. This helps maintain clarity
 * in event tracking and marketplace activity monitoring.
 */
class BulletinBoard extends SmartContract {
  @state(PublicKey) admin = State<PublicKey>();
  @state(UInt64) fee = State<UInt64>();

  async deploy(args: BulletinBoardDeployProps) {
    await super.deploy(args);
    this.admin.set(args.admin);
    this.fee.set(args.fee ?? UInt64.from(100_000_000));
    this.account.permissions.set({
      ...Permissions.default(),
      send: Permissions.proof(),
      setVerificationKey:
        Permissions.VerificationKey.proofDuringCurrentVersion(),
      setPermissions: Permissions.impossible(),
    });
  }

  events = {
    newCollection: BB_NewCollectionEvent,
    offer: BB_OfferEvent,
    cancelOffer: BB_CancelOfferEvent,
    bid: BB_BidEvent,
    cancelBid: BB_CancelBidEvent,
    sale: BB_SaleEvent,
    upgradeVerificationKey: BB_UpgradeVerificationKeyEvent,
    changeAdmin: BB_ChangeAdminEvent,
    withdraw: UInt64,
    setFee: UInt64,
  };

  /**
   * Pays the fee to prevent spamming the BulletinBoard with fake events.
   */
  async payFee() {
    const fee = this.fee.getAndRequireEquals();
    const sender = this.sender.getUnconstrained();
    const feeUpdate = AccountUpdate.createSigned(sender);
    feeUpdate.body.useFullCommitment = Bool(true); // Prevent memo and fee change
    feeUpdate.balance.subInPlace(fee);
    this.balance.addInPlace(fee);
    return feeUpdate;
  }

  /**
   * Emits a new collection event.
   * @param collection - The collection address.
   */
  @method async newCollection(collection: PublicKey) {
    await this.payFee();
    this.emitEvent(
      "newCollection",
      new BB_NewCollectionEvent({
        collection,
      })
    );
  }

  /**
   * Emits an offer event.
   * @param collection - The collection address.
   * @param nft - The NFT address.
   * @param offer - The offer address.
   * @param price - The price.
   */
  @method async offer(
    collection: PublicKey,
    nft: PublicKey,
    offer: PublicKey,
    price: UInt64
  ) {
    await this.payFee();
    this.emitEvent(
      "offer",
      new BB_OfferEvent({ collection, nft, offer, price })
    );
  }

  /**
   * Emits a cancel offer event.
   * @param collection - The collection address.
   * @param nft - The NFT address.
   */
  @method async cancelOffer(collection: PublicKey, nft: PublicKey) {
    await this.payFee();
    this.emitEvent("cancelOffer", new BB_CancelOfferEvent({ collection, nft }));
  }

  /**
   * Emits a bid event.
   * @param collection - The collection address.
   * @param nft - The NFT address.
   * @param bid - The bid address.
   * @param price - The price.
   */
  @method async bid(
    collection: PublicKey,
    nft: PublicKey,
    bid: PublicKey,
    price: UInt64
  ) {
    await this.payFee();
    this.emitEvent("bid", new BB_BidEvent({ collection, nft, bid, price }));
  }

  /**
   * Emits a cancel bid event.
   * @param collection - The collection address.
   * @param nft - The NFT address.
   * @param bid - The bid address.
   */
  @method async cancelBid(
    collection: PublicKey,
    nft: PublicKey,
    bid: PublicKey
  ) {
    await this.payFee();
    this.emitEvent(
      "cancelBid",
      new BB_CancelBidEvent({ collection, nft, bid })
    );
  }

  /**
   * Emits a sale event.
   * @param collection - The collection address.
   * @param nft - The NFT address.
   * @param buyer - The buyer address.
   * @param price - The price.
   */
  @method async sale(
    collection: PublicKey,
    nft: PublicKey,
    buyer: PublicKey,
    price: UInt64
  ) {
    await this.payFee();
    this.emitEvent("sale", new BB_SaleEvent({ collection, nft, buyer, price }));
  }

  /**
   * Ensures that the transaction is authorized by the contract owner.
   * @returns A signed `AccountUpdate` from the admin.
   */
  async ensureOwnerSignature(): Promise<AccountUpdate> {
    const admin = this.admin.getAndRequireEquals();
    const adminUpdate = AccountUpdate.createSigned(admin);
    adminUpdate.body.useFullCommitment = Bool(true); // Prevent memo and fee change
    return adminUpdate;
  }

  /**
   * Changes the contract's admin
   * @param admin - The new admin.
   */
  @method
  async changeAdmin(admin: PublicKey) {
    await this.ensureOwnerSignature();

    // Set the new admin
    this.admin.set(admin);

    // Emit the change admin event
    this.emitEvent("changeAdmin", new BB_ChangeAdminEvent({ admin }));
  }

  /**
   * Changes the contract's fee
   * @param fee - The new fee.
   */
  @method
  async setFee(fee: UInt64) {
    await this.ensureOwnerSignature();

    // Set the new fee
    this.fee.set(fee);

    // Emit the change fee event
    this.emitEvent("setFee", fee);
  }

  /**
   * Upgrades the contract's verification key after validating with the upgrade authority.
   * @param vk - The new verification key to upgrade to.
   */
  @method
  async upgradeVerificationKey(vk: VerificationKey) {
    await this.ensureOwnerSignature();

    // Set the new verification key
    this.account.verificationKey.set(vk);

    // Emit the upgrade event
    this.emitEvent(
      "upgradeVerificationKey",
      new BB_UpgradeVerificationKeyEvent({ vk: vk.hash })
    );
  }

  /**
   * Withdraws the fee by admin
   */
  @method
  async withdraw(amount: UInt64) {
    const adminUpdate = await this.ensureOwnerSignature();
    adminUpdate.balance.addInPlace(amount);
    this.balance.subInPlace(amount);
    this.emitEvent("withdraw", amount);
  }
}
