import { PublicKey, SystemProgram, Transaction, TransactionSignature } from "@solana/web3.js";
import * as anchor from "@coral-xyz/anchor";

import ProgramLoader from "./ProgramLoader";
import { ApplicationState as ApplicationStateType } from "../types/application-state";
import Listing from "./Listing";
import BidReceipt from "./BidReciept";
import { ListingState } from "../types/listing";
import { ASSOCIATED_TOKEN_PROGRAM_ID, getAssociatedTokenAddress, TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { checkEnumState } from "../helper/check-enum-status.helper";




/**
 * ApplicationState
 * 
 * @description A class for managing the application state
 *  - get the application state by pubkey
 *  - update application state
 *  - execute sale ?
 */
export default class ApplicationState {
    private listingProgram: Listing;
    private bidReceiptProgram: BidReceipt;
    public constructor(protected readonly programLoader: ProgramLoader) {
        this.listingProgram = new Listing(programLoader);
        this.bidReceiptProgram = new BidReceipt(programLoader);
    }



    public async create(feeAccount: string): Promise<ApplicationStateType> {
        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("[*] NEW APPLICATION STATE:- " + applicationState);

            let txSignature = await this.programLoader.program.methods
                .initialize()
                .accounts({
                    applicationState,
                    user: this.programLoader.wallet.publicKey,
                    feeAccount,
                    systemProgram: SystemProgram.programId
                })
                .signers([this.programLoader.wallet])
                .rpc();

            console.log("[*] Transaction signature:- ", txSignature);


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

            return appState as unknown as ApplicationStateType;
        } catch (e: any) {
            console.error(e);
            throw new Error(`Failed to create application state: ${e.message}`);
        }
    }


    /**
     * @description Get all application states
     * @returns {ApplicationStateType[]} The application states
     */
    public async getAll(): Promise<ApplicationStateType[]> {
        try {
            const applicationStates = await this.programLoader.program.account.applicationState.all();
            console.log("[*] Application states: ", applicationStates);
            return applicationStates as unknown as ApplicationStateType[];
        } catch (e: any) {
            console.error(e);
            throw new Error(`Failed to get all application states: ${e.message}`);
        }
    }

    /**
     * @description Get the application state
     * 
     * @param {PublicKey} appKey - The application state pubkey
     * @returns {ApplicationStateType} The application state
     */
    public async get(appKey: PublicKey): Promise<ApplicationStateType> {
        try {
            const applicationState = await this.programLoader.program.account.applicationState.fetch(appKey);
            return applicationState as any;
        } catch (e: any) {
            console.error(e);
            throw new Error(`Failed to get application state: ${e.message}`);
        }
    }


    /**
     * @description Update the application state
     * 
     * @param {PublicKey} appKey - The application state pubkey
     * @param {PublicKey} feeAccount - The fee account pubkey
     * @returns {ApplicationStateType} The application state
     */
    public async update(appKey: PublicKey, feeAccount: PublicKey): Promise<ApplicationStateType> {
        try {
            await this.programLoader.program.methods
                .updateApplicationState(feeAccount)
                .accounts({
                    applicationState: appKey,
                    user: this.programLoader.wallet.publicKey,
                })
                .signers([this.programLoader.wallet])
                .rpc();

            let state = await this.get(appKey);
            return state;
        } catch (e: any) {
            console.error(e);
            throw new Error(`Failed to update application state: ${e.message}`);
        }
    }


    /**
     * @description Execute a sale
     * @param listingPubkey 
     * @param executor 
     * @returns 
     */
    public async executeSale(listingPubkey: PublicKey, executor: PublicKey, tokenMintProgramId: PublicKey, nftMintProgramId: PublicKey): Promise<Transaction> {
        try {
            const listingData = await this.listingProgram.get(listingPubkey);
            console.log("[*] Listing data: ", JSON.stringify(listingData, null, 4));

            // const seeds1 = [anchor.utils.bytes.utf8.encode("deserialize"), Uint8Array.from(this.programLoader.wallet.publicKey.toBuffer())]
            // const [applicationState, applicationStateBump] = PublicKey.findProgramAddressSync(seeds1, this.programLoader.program.programId);
            let appState = await this.programLoader.program.account.applicationState.fetch(this.programLoader.applicationState);
            console.log("[*] Application state: ", JSON.stringify(appState, null, 4));

            // if(!executor.equals(listingData.authority) || !executor.equals(appState.listingAuthority)) throw new Error('The user has no access to execute sale');

            const seeds2 = [anchor.utils.bytes.utf8.encode("auction_manager"), Uint8Array.from(listingData.nft.toBuffer()), Uint8Array.from(listingData.authority.toBuffer())];
            const [auctionManager, auctionManagerBump] = PublicKey.findProgramAddressSync(seeds2, this.programLoader.program.programId);
            var auctionManagerState = await this.programLoader.program.account.auctionManager.fetch(auctionManager);
            console.log("[*] Auction manager state: ", JSON.stringify(auctionManagerState, null, 4));

            const seeds3 = [anchor.utils.bytes.utf8.encode("listing"), Uint8Array.from(listingData.nft.toBuffer()), Uint8Array.from(listingData.authority.toBuffer()), Uint8Array.from(auctionManager.toBuffer()), Uint8Array.from(listingData.tokenMint.toBuffer())];
            const [listing, listingBump] = PublicKey.findProgramAddressSync(seeds3, this.programLoader.program.programId);
            var listingState = await this.programLoader.program.account.listing.fetch(listing);
            console.log("[*] Listing: ", JSON.stringify(listingState, null, 4));

            if (!listingPubkey.equals(listing)) throw new Error("Invalid Listing");
            console.log("[*] Listing state: ", listingData.state);
            if (!checkEnumState(listingData.state as any, ListingState.Active)) throw new Error("Listing is not active");

            let highestBidInfo = await this.listingProgram.getHighestBidInfo(listingPubkey);
            console.log("[*] Highest bid info: ", JSON.stringify(highestBidInfo, null, 4));
            let bidsInfoToRefund = await this.listingProgram.getBidsInfoToRefund(listingPubkey);
            console.log("[*] Bids info to refund: ", JSON.stringify(bidsInfoToRefund, null, 4));

            let txn: Transaction = new Transaction();

            //create refund bid transactions
            for await (let bid of bidsInfoToRefund) {
                let inx = await this.bidReceiptProgram.refund(bid.receipt, bid.bidder, tokenMintProgramId);
                txn.add(inx);
            }


            const nftEscrow = await getAssociatedTokenAddress(listingData.nft, auctionManager, true);
            // const nftOwner = await getAssociatedTokenAddress(listingData.nft, listingData.authority, true);

            const winnerNftAccount = await getAssociatedTokenAddress(listingData.nft, highestBidInfo.bidder, true, nftMintProgramId);
            const tokenEscrow = await getAssociatedTokenAddress(listingData.tokenMint, auctionManager, true, tokenMintProgramId);
            const feeTokenAccount = await getAssociatedTokenAddress(listingData.tokenMint, appState.feeAccount, true, tokenMintProgramId);
            const seeds = [anchor.utils.bytes.utf8.encode("user_account"), Uint8Array.from(highestBidInfo.bidder.toBuffer()), Uint8Array.from(appState.listingAuthority.toBuffer()), anchor.utils.bytes.utf8.encode("deserialize")];
            const [userdata, _] = PublicKey.findProgramAddressSync(seeds, this.programLoader.program.programId);
            console.log("[*] WINNER NFT account: ", winnerNftAccount);
            console.log("[*] AUCTION MANAGER Token escrow: ", tokenEscrow);


            //create transaction to execute sale
            const submittedAt = Math.floor(Date.now() / 1000);
            const inx = await this.programLoader.program.methods
                .executeSale(new anchor.BN(submittedAt))
                .accounts({
                    applicationState: this.programLoader.applicationState,
                    userdata,
                    bidReceipt: highestBidInfo.receipt,
                    user: executor,
                    listing,
                    nft: listingData.nft,
                    auctionManager,
                    nftEscrow,
                    winnerNftAccount,
                    proceedsWallet: listingData.auctionProceedsWallet,
                    tokenEscrow,
                    tokenMint: listingData.tokenMint,
                    tokenProgram: tokenMintProgramId,
                    nftTokenProgram: nftMintProgramId,
                    associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
                    systemProgram: SystemProgram.programId,
                    feeTokenAccount
                })
                .instruction();

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

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