import { PublicKey, SystemProgram, Transaction, TransactionInstruction } from "@solana/web3.js";
import * as anchor from "@coral-xyz/anchor";
import { ASSOCIATED_TOKEN_PROGRAM_ID, createAssociatedTokenAccountInstruction, getAssociatedTokenAddress, TOKEN_PROGRAM_ID } from "@solana/spl-token";

import ProgramLoader from "./ProgramLoader";
import { BidRequest as BidRequestType } from "../types/bid-request";
import AuctionManager from "./AuctionManager";


/**
 * BidRequest
 * 
 * @description This is the main contract for the bid request. It is responsible for managing the bid request, including creating and updating the bid request, and executing bids.
 * 
 *  - create a new bid request
 *  - get a bid request
 *  - fetch many bid requests
 *  - get all bid requests
 *  - get bid requests by nft
 *  - get bid requests by authority
 *  - get bid requests by nft owner
 *  - accept a bid request
 *  - reject a bid request
 *  - cancel a bid request
 *  - increase a bid request price
 *  - make bid request public
 */
export default class BidRequest {
    private auctionManagerProgram: AuctionManager;
    public constructor(protected readonly programLoader: ProgramLoader) {
        this.auctionManagerProgram = new AuctionManager(programLoader);
    }

    /**
     * @description Create a new bid request
     * 
     * @param {PublicKey} nft - The nft to create the bid request for
     * @param {PublicKey} bidRequester - The bid requester
     * @param {PublicKey} nftOwner - The nft owner
     * @param {PublicKey} tokenMint - The token mint
     * @param {PublicKey} tokenMintProgramId - The program ID of the token mint
     * @param {number} price - The price of the bid
     * @returns {Promise<TransactionInstruction[]>} The transaction instructions to create the bid request
     */
    public async create(nft: PublicKey, bidRequester: PublicKey, nftOwner: PublicKey, tokenMint: PublicKey, tokenMintProgramId: PublicKey, price: number): Promise<TransactionInstruction[]> {
        try {
            // const seeds1 = [anchor.utils.bytes.utf8.encode("deserialize"), Uint8Array.from(this.programLoader.wallet.publicKey.toBuffer())]
            // const [applicationState, _] = PublicKey.findProgramAddressSync(seeds1, this.programLoader.program.programId);
            // console.log("[*] Application state: ", applicationState);
            var appState = await this.programLoader.program.account.applicationState.fetch(this.programLoader.applicationState);


            const seeds2 = [anchor.utils.bytes.utf8.encode("user_account"), Uint8Array.from(bidRequester.toBuffer()), Uint8Array.from(appState.listingAuthority.toBuffer()), anchor.utils.bytes.utf8.encode("deserialize")];
            const [userdata, userdataBump] = PublicKey.findProgramAddressSync(seeds2, this.programLoader.program.programId);
            console.log("[*] Userdata: ", userdata);
            console.log("[*] Userdata bump: ", userdataBump);

            const seeds = [anchor.utils.bytes.utf8.encode("bid_request"), Uint8Array.from(nft.toBuffer()), Uint8Array.from(nftOwner.toBuffer()), Uint8Array.from(bidRequester.toBuffer()), Uint8Array.from(tokenMint.toBuffer())];
            const [bidRequest, bidRequestBump] = PublicKey.findProgramAddressSync(seeds, this.programLoader.program.programId);
            console.log("[*] Bid request: ", bidRequest);
            console.log("[*] Bid request bump: ", bidRequestBump);

            let txns: TransactionInstruction[] = [];

            //prepare accounts
            let escrowTokenAccount = await getAssociatedTokenAddress(tokenMint, bidRequest, true, tokenMintProgramId);
            let userTokenAccount = await getAssociatedTokenAddress(tokenMint, bidRequester, true, tokenMintProgramId);

            //get account info
            let escrowTokenAccountInfo = await this.programLoader.connection.getAccountInfo(escrowTokenAccount);
            let userTokenAccountInfo = await this.programLoader.connection.getAccountInfo(userTokenAccount);

            if (!escrowTokenAccountInfo) {
                let inx = createAssociatedTokenAccountInstruction(
                    bidRequester,
                    escrowTokenAccount,
                    bidRequest,
                    tokenMint,
                    tokenMintProgramId
                )
                txns.push(inx);
            }
            if (!userTokenAccountInfo) {
                let inx = createAssociatedTokenAccountInstruction(
                    bidRequester,
                    userTokenAccount,
                    bidRequester,
                    tokenMint,
                    tokenMintProgramId
                )
                txns.push(inx);
            }

            //create bid request
            const createdAt = Math.floor(Date.now() / 1000);
            const inx = await this.programLoader.program.methods.
                createBidRequest(new anchor.BN(price), new anchor.BN(createdAt))
                .accounts({
                    applicationState: this.programLoader.applicationState,
                    userdata,
                    bidRequest,
                    nftOwner,
                    nft,
                    tokenMint,
                    feeAccount: appState.feeAccount,
                    escrowTokenAccount,
                    userTokenAccount,
                    user: bidRequester,
                    tokenProgram: tokenMintProgramId,
                    systemProgram: SystemProgram.programId
                })
                .instruction()
            txns.push(inx);

            return txns;
        } catch (e: any) {
            console.error(e);
            throw new Error(`Failed to create bid request: ${e.message}`);
        }
    }


    /**
     * @description Create a new bid request transaction
     * 
     * @param {PublicKey} nft - The nft to create the bid request for
     * @param {PublicKey} bidRequester - The bid requester
     * @param {PublicKey} nftOwner - The nft owner
     * @param {PublicKey} tokenMint - The token mint
     * @param {number} price - The price of the bid
     * @returns {Promise<Transaction>} The transaction to create the bid request
     */
    public async createTransaction(nft: PublicKey, bidRequester: PublicKey, nftOwner: PublicKey, tokenMint: PublicKey, tokenMintProgramId: PublicKey, price: number): Promise<Transaction> {
        try {
            const txns = await this.create(nft, bidRequester, nftOwner, tokenMint, tokenMintProgramId, price);
            var transactions = new Transaction().add(...txns);
            transactions.recentBlockhash = (await this.programLoader.getRecentBlockHash()).blockhash;
            transactions.feePayer = bidRequester;

            return transactions;
        } catch (e: any) {
            console.error(e);
            throw new Error(`Failed to create bid request transaction: ${e.message}`);
        }
    }

    /**
     * @description Get a bid request
     * 
     * @param {PublicKey} bidRequestPubkey - The public key of the bid request
     * @returns {Promise<BidRequestType>} The bid request
     */
    public async get(bidRequestPubkey: PublicKey): Promise<BidRequestType> {
        try {
            var bidRequest = await this.programLoader.program.account.bidRequest.fetch(bidRequestPubkey);
            return bidRequest as unknown as BidRequestType;
        } catch (e: any) {
            console.error(e);
            throw new Error(`Failed to get bid request: ${e.message}`);
        }
    }


    /**
     * @description Fetch many bid requests
     * 
     * @param {PublicKey[]} bidReceiptPubkeys - The public keys of the bid requests
     * @returns {Promise<BidRequestType[]>} The bid requests
     */
    public async fetchMany(bidReceiptPubkeys: PublicKey[]): Promise<BidRequestType[]> {
        try {
            var bidRequests = await this.programLoader.program.account.bidRequest.fetchMultiple(bidReceiptPubkeys);
            return bidRequests as unknown as BidRequestType[];
        } catch (e: any) {
            console.error(e);
            throw new Error(`Failed to fetch many bid requests: ${e.message}`);
        }
    }


    /**
     * @description Get all bid requests
     * 
     * @returns {Promise<BidRequestType[]>} The bid requests
     */
    public async getAll(): Promise<BidRequestType[]> {
        try {
            var bidRequests = await this.programLoader.program.account.bidRequest.all();
            return bidRequests as unknown as BidRequestType[];
        } catch (e: any) {
            console.error(e);
            throw new Error(`Failed to get all bid requests: ${e.message}`);
        }
    }


    /**
     * @description Get bid requests by nft
     * 
     * @param {PublicKey} nft - The nft to get the bid requests for
     * @returns {Promise<BidRequestType[]>} The bid requests
     */
    public async getByNft(nft: PublicKey): Promise<BidRequestType[]> {
        try {
            var bidRequests = await this.getAll();
            return bidRequests.filter((bidRequest) => bidRequest.nft.equals(nft)) as unknown as BidRequestType[];
        } catch (e: any) {
            console.error(e);
            throw new Error(`Failed to get bid requests by nft: ${e.message}`);
        }
    }


    /**
     * @description Get bid requests by authority
     * 
     * @param {PublicKey} authority - The authority to get the bid requests for
     * @returns {Promise<BidRequestType[]>} The bid requests
     */
    public async getByAuthority(authority: PublicKey): Promise<BidRequestType[]> {
        try {
            var bidRequests = await this.getAll();
            return bidRequests.filter((bidRequest) => bidRequest.authority.equals(authority)) as unknown as BidRequestType[];
        } catch (e: any) {
            console.error(e);
            throw new Error(`Failed to get bid requests by authority: ${e.message}`);
        }
    }


    /**
     * @description Get bid requests by nft owner
     * 
     * @param {PublicKey} nftOwner - The nft owner to get the bid requests for
     * @returns {Promise<BidRequestType[]>} The bid requests
     */
    public async getByNftOwner(nftOwner: PublicKey): Promise<BidRequestType[]> {
        try {
            var bidRequests = await this.getAll();
            return bidRequests.filter((bidRequest) => bidRequest.owner.equals(nftOwner)) as unknown as BidRequestType[];
        } catch (e: any) {
            console.error(e);
            throw new Error(`Failed to get bid requests by nft owner: ${e.message}`);
        }
    }



    /**
     * @description Accept a bid request
     * 
     * @param {PublicKey} nft - The nft to accept the bid request for
     * @param {PublicKey} nftProgramId - The program ID of the nft
     * @param {PublicKey} nftOwner - The nft owner to accept the bid request for
     * @param {PublicKey} tokenMint - The token mint to accept the bid request for
     * @param {PublicKey} tokenMintProgramId - The program ID for the token mint
     * @param {PublicKey} bidRequestOwner - The bid request owner to accept the bid request for
     * @returns {Promise<Transaction>} The transaction to accept the bid request
     */
    public async accept(nft: PublicKey, nftProgramId: PublicKey, nftOwner: PublicKey, tokenMint: PublicKey, tokenMintProgramId: PublicKey, bidRequestOwner: PublicKey): Promise<Transaction> {
        try {
            const seeds1 = [anchor.utils.bytes.utf8.encode("deserialize"), Uint8Array.from(this.programLoader.wallet.publicKey.toBuffer())]
            const [applicationState, _] = PublicKey.findProgramAddressSync(seeds1, this.programLoader.program.programId);
            console.log("[*] Application state: ", applicationState);
            var appState = await this.programLoader.program.account.applicationState.fetch(applicationState);

            const seeds = [anchor.utils.bytes.utf8.encode("bid_request"), Uint8Array.from(nft.toBuffer()), Uint8Array.from(nftOwner.toBuffer()), Uint8Array.from(bidRequestOwner.toBuffer()), Uint8Array.from(tokenMint.toBuffer())];
            const [bidRequest, bidRequestBump] = PublicKey.findProgramAddressSync(seeds, this.programLoader.program.programId);
            console.log("[*] Bid request: ", bidRequest);
            console.log("[*] Bid request bump: ", bidRequestBump);

            var txn = new Transaction();
            var sellerNftTokenAccount = await getAssociatedTokenAddress(nft, nftOwner, true, nftProgramId);
            var buyerNftTokenAccount = await getAssociatedTokenAddress(nft, bidRequestOwner, true, nftProgramId);
            var escrowTokenAccount = await getAssociatedTokenAddress(tokenMint, bidRequest, true, tokenMintProgramId);
            var sellerTokenAccount = await getAssociatedTokenAddress(tokenMint, nftOwner, true, tokenMintProgramId);
            var feeTokenAccount = await getAssociatedTokenAddress(tokenMint, appState.feeAccount, true, tokenMintProgramId);

            var sellerNftTokenAccountInfo = await this.programLoader.connection.getAccountInfo(sellerNftTokenAccount);
            var buyerNftTokenAccountInfo = await this.programLoader.connection.getAccountInfo(buyerNftTokenAccount);
            var escrowTokenAccountInfo = await this.programLoader.connection.getAccountInfo(escrowTokenAccount);
            var sellerTokenAccountInfo = await this.programLoader.connection.getAccountInfo(sellerTokenAccount);
            var feeTokenAccountInfo = await this.programLoader.connection.getAccountInfo(feeTokenAccount);

            if (!sellerNftTokenAccountInfo) {
                let inx = await createAssociatedTokenAccountInstruction(
                    nftOwner,
                    sellerNftTokenAccount,
                    nftOwner,
                    nft,
                    nftProgramId
                )
                txn.add(inx);
            }
            if (!buyerNftTokenAccountInfo) {
                let inx = await createAssociatedTokenAccountInstruction(
                    nftOwner,
                    buyerNftTokenAccount,
                    bidRequestOwner,
                    nft,
                    nftProgramId
                )
                txn.add(inx);
            }
            if (!escrowTokenAccountInfo) {
                let inx = await createAssociatedTokenAccountInstruction(
                    nftOwner,
                    escrowTokenAccount,
                    bidRequest,
                    tokenMint,
                    tokenMintProgramId
                )
                txn.add(inx);
            }
            if (!sellerTokenAccountInfo) {
                let inx = await createAssociatedTokenAccountInstruction(
                    nftOwner,
                    sellerTokenAccount,
                    nftOwner,
                    tokenMint,
                    tokenMintProgramId
                )
                txn.add(inx);
            }
            if (!feeTokenAccountInfo) {
                let inx = await createAssociatedTokenAccountInstruction(
                    nftOwner,
                    feeTokenAccount,
                    appState.feeAccount,
                    tokenMint,
                    tokenMintProgramId
                )
                txn.add(inx);
            }


            var acceptedAt = Math.floor(Date.now() / 1000);
            let inx = await this.programLoader.program.methods
                .acceptBidRequest(new anchor.BN(acceptedAt))
                .accounts({
                    applicationState,
                    bidRequest,
                    user: nftOwner,
                    sellerNftTokenAccount,
                    buyerNftTokenAccount,
                    escrowTokenAccount,
                    sellerTokenAccount,
                    feeTokenAccount,
                    nft,
                    tokenMint,
                    tokenProgram: tokenMintProgramId,
                    nftTokenProgram: nftProgramId,
                    associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
                    systemProgram: SystemProgram.programId
                })
                .instruction();

            txn.add(inx);
            txn.recentBlockhash = (await this.programLoader.getRecentBlockHash()).blockhash;
            txn.feePayer = nftOwner;

            return txn;
        } catch (e: any) {
            console.error(e);
            throw new Error(`Failed to accept bid request: ${e.message}`);
        }
    }



    /**
     * @description Reject a bid request
     * 
     * @param {PublicKey} nft - The nft to reject the bid request for
     * @param {PublicKey} nftOwner - The nft owner to reject the bid request for
     * @param {PublicKey} bidRequestOwner - The bid request owner to reject the bid request for
     * @param {PublicKey} tokenMint - The token mint to reject the bid request for
     * @param {PublicKey} tokenMintProgramId - The program ID of the token mint
     * @returns {Promise<Transaction>} The transaction to reject the bid request
     */
    public async reject(nft: PublicKey, nftOwner: PublicKey, bidRequestOwner: PublicKey, tokenMint: PublicKey, tokenMintProgramId: PublicKey): Promise<Transaction> {
        try {
            const seeds = [anchor.utils.bytes.utf8.encode("bid_request"), Uint8Array.from(nft.toBuffer()), Uint8Array.from(nftOwner.toBuffer()), Uint8Array.from(bidRequestOwner.toBuffer()), Uint8Array.from(tokenMint.toBuffer())];
            const [bidRequest, bidRequestBump] = PublicKey.findProgramAddressSync(seeds, this.programLoader.program.programId);
            console.log("[*] Bid request: ", bidRequest);
            console.log("[*] Bid request bump: ", bidRequestBump);

            //get accounts
            var txn = new Transaction();
            var escrowTokenAccount = await getAssociatedTokenAddress(tokenMint, bidRequest, true, tokenMintProgramId);
            var requesterTokenAccount = await getAssociatedTokenAddress(tokenMint, bidRequestOwner, true, tokenMintProgramId);

            //get accounts info
            var escrowTokenAccountInfo = await this.programLoader.connection.getAccountInfo(escrowTokenAccount);
            var requesterTokenAccountInfo = await this.programLoader.connection.getAccountInfo(requesterTokenAccount);

            if (!escrowTokenAccountInfo) {
                let inx = await createAssociatedTokenAccountInstruction(
                    nftOwner,
                    escrowTokenAccount,
                    bidRequest,
                    tokenMint,
                    tokenMintProgramId
                )
                txn.add(inx);
            }
            if (!requesterTokenAccountInfo) {
                let inx = await createAssociatedTokenAccountInstruction(
                    nftOwner,
                    requesterTokenAccount,
                    bidRequestOwner,
                    tokenMint,
                    tokenMintProgramId
                )
                txn.add(inx);
            }


            var rejectedAt = Math.floor(Date.now() / 1000);
            let inx = await this.programLoader.program.methods
                .rejectBidRequest(new anchor.BN(rejectedAt))
                .accounts({
                    bidRequest,
                    user: nftOwner,
                    escrowTokenAccount,
                    requesterTokenAccount,
                    tokenMint,
                    tokenProgram: tokenMintProgramId,
                    associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
                    systemProgram: SystemProgram.programId
                })
                .instruction();
            txn.add(inx);
            txn.recentBlockhash = (await this.programLoader.getRecentBlockHash()).blockhash;
            txn.feePayer = nftOwner;

            return txn;
        } catch (e: any) {
            console.error(e);
            throw new Error(`Failed to reject bid request: ${e.message}`);
        }
    }


    /**
     * @description Cancel a bid request
     * 
     * @param {PublicKey} nft - The nft to cancel the bid request for
     * @param {PublicKey} nftOwner - The nft owner to cancel the bid request for
     * @param {PublicKey} bidRequestOwner - The bid request owner to cancel the bid request for
     * @param {PublicKey} tokenMint - The token mint to cancel the bid request for
     * @param {PublicKey} tokenMintProgramId - The program ID for the token mint
     * @returns {Promise<Transaction>} The transaction to cancel the bid request
     */
    public async cancel(nft: PublicKey, nftOwner: PublicKey, bidRequestOwner: PublicKey, tokenMint: PublicKey, tokenMintProgramId: PublicKey): Promise<Transaction> {
        try {
            const seeds = [anchor.utils.bytes.utf8.encode("bid_request"), Uint8Array.from(nft.toBuffer()), Uint8Array.from(nftOwner.toBuffer()), Uint8Array.from(bidRequestOwner.toBuffer()), Uint8Array.from(tokenMint.toBuffer())];
            const [bidRequest, bidRequestBump] = PublicKey.findProgramAddressSync(seeds, this.programLoader.program.programId);
            console.log("[*] Bid request: ", bidRequest);
            console.log("[*] Bid request bump: ", bidRequestBump);

            //get accounts
            var txn = new Transaction();
            var escrowTokenAccount = await getAssociatedTokenAddress(tokenMint, bidRequest, true, tokenMintProgramId);
            var cancellerTokenAccount = await getAssociatedTokenAddress(tokenMint, bidRequestOwner, true, tokenMintProgramId);

            //get accounts info
            var escrowTokenAccountInfo = await this.programLoader.connection.getAccountInfo(escrowTokenAccount);
            var cancellerTokenAccountInfo = await this.programLoader.connection.getAccountInfo(cancellerTokenAccount);

            if (!escrowTokenAccountInfo) {
                let inx = await createAssociatedTokenAccountInstruction(
                    bidRequestOwner,
                    escrowTokenAccount,
                    bidRequest,
                    tokenMint,
                    tokenMintProgramId
                )
                txn.add(inx);
            }
            if (!cancellerTokenAccountInfo) {
                let inx = await createAssociatedTokenAccountInstruction(
                    bidRequestOwner,
                    cancellerTokenAccount,
                    bidRequestOwner,
                    tokenMint,
                    tokenMintProgramId
                )
                txn.add(inx);
            }


            var cancelledAt = Math.floor(Date.now() / 1000);
            let inx = await this.programLoader.program.methods
                .cancelBidRequest(new anchor.BN(cancelledAt))
                .accounts({
                    bidRequest,
                    user: bidRequestOwner,
                    escrowTokenAccount,
                    cancellerTokenAccount,
                    tokenMint,
                    tokenProgram: tokenMintProgramId,
                    associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
                    systemProgram: SystemProgram.programId
                })
                .instruction();
            txn.add(inx);
            txn.recentBlockhash = (await this.programLoader.getRecentBlockHash()).blockhash;
            txn.feePayer = bidRequestOwner;

            return txn;
        } catch (e: any) {
            console.error(e);
            throw new Error(`Failed to cancel bid request: ${e.message}`);
        }
    }


    /**
     * @description Increase a bid request amount
     * 
     * @param {PublicKey} nft - The nft to increase the bid request amount for
     * @param {PublicKey} nftOwner - The nft owner to increase the bid request amount for
     * @param {PublicKey} tokenMint - The token mint to increase the bid request amount for
     * @param {PublicKey} tokenMintProgramId - The program ID for the token mint
     * @param {PublicKey} bidRequestOwner - The bid request owner to increase the bid request amount for
     * @param {number} newAmount - The new amount to increase the bid request to
     * @returns {Promise<Transaction>} The transaction to increase the bid request amount
     */
    public async increaseAmount(nft: PublicKey, nftOwner: PublicKey, tokenMint: PublicKey, tokenMintProgramId: PublicKey, bidRequestOwner: PublicKey, newAmount: number): Promise<Transaction> {
        try {
            const seeds = [anchor.utils.bytes.utf8.encode("bid_request"), Uint8Array.from(nft.toBuffer()), Uint8Array.from(nftOwner.toBuffer()), Uint8Array.from(bidRequestOwner.toBuffer()), Uint8Array.from(tokenMint.toBuffer())];
            const [bidRequest, bidRequestBump] = PublicKey.findProgramAddressSync(seeds, this.programLoader.program.programId);
            console.log("[*] Bid request: ", bidRequest);
            console.log("[*] Bid request bump: ", bidRequestBump);

            //get accounts
            var txn = new Transaction();
            var escrowTokenAccount = await getAssociatedTokenAddress(tokenMint, bidRequest, true, tokenMintProgramId);
            var ownerTokenAccount = await getAssociatedTokenAddress(tokenMint, bidRequestOwner, true, tokenMintProgramId);

            //get accounts info
            var escrowTokenAccountInfo = await this.programLoader.connection.getAccountInfo(escrowTokenAccount);
            var ownerTokenAccountInfo = await this.programLoader.connection.getAccountInfo(ownerTokenAccount);

            if (!escrowTokenAccountInfo) {
                let inx = await createAssociatedTokenAccountInstruction(
                    bidRequestOwner,
                    escrowTokenAccount,
                    bidRequest,
                    tokenMint,
                    tokenMintProgramId
                )
                txn.add(inx);
            }
            if (!ownerTokenAccountInfo) {
                let inx = await createAssociatedTokenAccountInstruction(
                    bidRequestOwner,
                    ownerTokenAccount,
                    bidRequestOwner,
                    tokenMint,
                    tokenMintProgramId
                )
                txn.add(inx);
            }


            var increasedAt = Math.floor(Date.now() / 1000);
            let inx = await this.programLoader.program.methods
                .increaseBidRequest(new anchor.BN(newAmount), new anchor.BN(increasedAt))
                .accounts({
                    bidRequest,
                    user: bidRequestOwner,
                    escrowTokenAccount,
                    ownerTokenAccount,
                    tokenMint,
                    tokenProgram: tokenMintProgramId,
                    associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
                    systemProgram: SystemProgram.programId
                })
                .instruction();
            txn.add(inx);
            txn.recentBlockhash = (await this.programLoader.getRecentBlockHash()).blockhash;
            txn.feePayer = bidRequestOwner;

            return txn;
        } catch (e: any) {
            console.error(e);
            throw new Error(`Failed to increase bid request amount: ${e.message}`);
        }
    }


    /**
     * @description Make a bid request public
     * 
     * @param {PublicKey} nft - The nft to make the bid request public for
     * @param {PublicKey} nftOwner - The nft owner to make the bid request public for
     * @param {PublicKey} nftProgramId - The program ID of the nft
     * @param {PublicKey} tokenMint - The token mint to make the bid request public for
     * @param {PublicKey} tokenMintProgramId - The program ID of the token mint
     * @param {PublicKey} bidRequestOwner - The bid request owner to make the bid request public for
     * @param {number} period - The period to make the bid request public for
     * @param {number} timeExtension - The time extension to make the bid request public for
     * @returns {Promise<Transaction>} The transaction to make the bid request public
     */
    public async makePublic(nft: PublicKey, nftOwner: PublicKey, nftProgramId: PublicKey, tokenMint: PublicKey, tokenMintProgramId: PublicKey, bidRequestOwner: PublicKey, period: number, timeExtension: number): Promise<Transaction> {
        try {
            // const seeds1 = [anchor.utils.bytes.utf8.encode("deserialize"), Uint8Array.from(this.programLoader.wallet.publicKey.toBuffer())]
            // const [applicationState, _] = PublicKey.findProgramAddressSync(seeds1, this.programLoader.program.programId);

            const seeds2 = [anchor.utils.bytes.utf8.encode("auction_manager"), Uint8Array.from(nft.toBuffer()), Uint8Array.from(nftOwner.toBuffer())];
            const [auctionManager, auctionManagerBump] = PublicKey.findProgramAddressSync(seeds2, this.programLoader.program.programId);
            console.log("[*] Auction manager: ", auctionManager);
            console.log("[*] Auction manager bump: ", auctionManagerBump);

            const seeds3 = [anchor.utils.bytes.utf8.encode("listing"), Uint8Array.from(nft.toBuffer()), Uint8Array.from(nftOwner.toBuffer()), Uint8Array.from(auctionManager.toBuffer()), Uint8Array.from(tokenMint.toBuffer())];
            const [listing, listingBump] = PublicKey.findProgramAddressSync(seeds3, this.programLoader.program.programId);
            console.log("[*] Listing: ", listing);
            console.log("[*] Listing bump: ", listingBump);

            let appState = await this.programLoader.program.account.applicationState.fetch(this.programLoader.applicationState)

            const seeds4 = [anchor.utils.bytes.utf8.encode("user_account"), Uint8Array.from(nftOwner.toBuffer()), Uint8Array.from(appState.listingAuthority.toBuffer()), anchor.utils.bytes.utf8.encode("deserialize")];
            const [creatorUserdata, creatorUserdataBump] = PublicKey.findProgramAddressSync(seeds4, this.programLoader.program.programId);
            console.log("[*] Creator userdata: ", creatorUserdata);
            console.log("[*] Creator userdata bump: ", creatorUserdataBump);

            const seeds5 = [anchor.utils.bytes.utf8.encode("user_account"), Uint8Array.from(bidRequestOwner.toBuffer()), Uint8Array.from(appState.listingAuthority.toBuffer()), anchor.utils.bytes.utf8.encode("deserialize")];
            const [bidderUserdata, bidderUserdataBump] = PublicKey.findProgramAddressSync(seeds5, this.programLoader.program.programId);
            console.log("[*] Bidder userdata: ", bidderUserdata);
            console.log("[*] Bidder userdata bump: ", bidderUserdataBump);

            const seeds6 = [anchor.utils.bytes.utf8.encode("bid_request"), Uint8Array.from(nft.toBuffer()), Uint8Array.from(nftOwner.toBuffer()), Uint8Array.from(bidRequestOwner.toBuffer()), Uint8Array.from(tokenMint.toBuffer())];
            const [bidRequest, bidRequestBump] = PublicKey.findProgramAddressSync(seeds6, this.programLoader.program.programId);
            console.log("[*] Bid request: ", bidRequest);
            console.log("[*] Bid request bump: ", bidRequestBump);

            const seeds = [anchor.utils.bytes.utf8.encode("bid_receipt"), Uint8Array.from(listing.toBuffer()), Uint8Array.from(bidRequestOwner.toBuffer()), Uint8Array.from(tokenMint.toBuffer())];
            const [bidReceipt, bidReceiptBump] = PublicKey.findProgramAddressSync(seeds, this.programLoader.program.programId);
            console.log("[*] Bid receipt: ", bidReceipt);
            console.log("[*] Bid receipt bump: ", bidReceiptBump);


            //get accounts
            var txn = new Transaction();
            var bidRequestTokenAccount = await getAssociatedTokenAddress(tokenMint, bidRequest, true, tokenMintProgramId);
            var escrowTokenAccount = await getAssociatedTokenAddress(tokenMint, auctionManager, true, tokenMintProgramId);
            var escrowNftTokenAccount = await getAssociatedTokenAddress(nft, auctionManager, true, nftProgramId);
            var creatorNftTokenAccount = await getAssociatedTokenAddress(nft, nftOwner, true, nftProgramId);
            var creatorTokenAccount = await getAssociatedTokenAddress(tokenMint, nftOwner, true, tokenMintProgramId);

            //get accounts info
            var auctionManagerInfo = await this.programLoader.connection.getAccountInfo(auctionManager);
            var bidRequestTokenAccountInfo = await this.programLoader.connection.getAccountInfo(bidRequestTokenAccount);
            var escrowTokenAccountInfo = await this.programLoader.connection.getAccountInfo(escrowTokenAccount);
            var escrowNftTokenAccountInfo = await this.programLoader.connection.getAccountInfo(escrowNftTokenAccount);
            var creatorNftTokenAccountInfo = await this.programLoader.connection.getAccountInfo(creatorNftTokenAccount);
            var creatorTokenAccountInfo = await this.programLoader.connection.getAccountInfo(creatorTokenAccount);


            if (!auctionManagerInfo) {
                let inx = await this.auctionManagerProgram.create(nft, nftOwner, nftProgramId);
                txn.add(inx);
            }
            if (!bidRequestTokenAccountInfo) {
                let inx = await createAssociatedTokenAccountInstruction(
                    bidRequestOwner,
                    bidRequestTokenAccount,
                    bidRequest,
                    tokenMint,
                    tokenMintProgramId
                )
                txn.add(inx);
            }
            if (!escrowTokenAccountInfo) {
                let inx = await createAssociatedTokenAccountInstruction(
                    nftOwner,
                    escrowTokenAccount,
                    auctionManager,
                    tokenMint, tokenMintProgramId
                )
                txn.add(inx);
            }
            if (!escrowNftTokenAccountInfo) {
                let inx = await createAssociatedTokenAccountInstruction(
                    nftOwner,
                    escrowNftTokenAccount,
                    auctionManager,
                    nft,
                    nftProgramId
                )
                txn.add(inx);
            }
            if (!creatorNftTokenAccountInfo) {
                let inx = await createAssociatedTokenAccountInstruction(
                    nftOwner,
                    creatorNftTokenAccount,
                    nftOwner,
                    nft,
                    nftProgramId
                )
                txn.add(inx);
            }
            if (!creatorTokenAccountInfo) {
                let inx = await createAssociatedTokenAccountInstruction(
                    nftOwner,
                    creatorTokenAccount,
                    nftOwner,
                    tokenMint, 
                    tokenMintProgramId
                )
                txn.add(inx);
            }

            var createdAt = Math.floor(Date.now() / 1000);
            let endTime = null;
            if (period) {
                endTime = createdAt + ((60 * 60 * 24) * period);
            }
            let inx = await this.programLoader.program.methods
                .makeBidRequestPublic(new anchor.BN(createdAt), new anchor.BN(endTime), new anchor.BN(timeExtension))
                .accounts({
                    applicationState: this.programLoader.applicationState,
                    creatorUserdata,
                    bidderUserdata,
                    user: nftOwner,
                    bidder: bidRequestOwner,
                    listing,
                    auctionManager,
                    bidReceipt,
                    bidRequest,
                    bidRequestTokenAccount,
                    escrowTokenAccount,
                    escrowNftAccount: escrowNftTokenAccount,
                    creatorNftAccount: creatorNftTokenAccount,
                    creatorTokenAccount,
                    tokenMint,
                    feeAccount: appState.feeAccount,
                    nft,
                    tokenProgram: tokenMintProgramId,
                    nftTokenProgram: nftProgramId,
                    associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
                    systemProgram: SystemProgram.programId,
                })
                .instruction();
            txn.add(inx);
            txn.recentBlockhash = (await this.programLoader.getRecentBlockHash()).blockhash;
            txn.feePayer = nftOwner;

            return txn;
        } catch (e: any) {
            console.error(e);
            throw new Error(`Failed to make bid request public: ${e.message}`);
        }
    }
}