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

import BidReceipt from "./BidReciept";
import Listing from "./Listing";
import ProgramLoader from "./ProgramLoader";
import { Userdata as UserdataType } from "../types/userdata";
import { BidReceipt as BidReceiptType } from "../types/bid-receipt";
import { Listing as ListingType } from "../types/listing";
import { BidRequest as BidRequestType } from "../types/bid-request";
import BidRequest from "./BidRequest";



/**
 * Userdata
 * 
 * @description A class for managing user data
 * 
 *  - get all user data
 *  - get user data by pubkey
 *  - create user data
 *  - get user bidreceipts
 *  - get user listings
 *  - get user bid requests
 */
export default class Userdata {
    private listingProgram: Listing;
    private bidReceiptProgram: BidReceipt;
    private bidRequestProgram: BidRequest;
    public constructor(protected readonly programLoader: ProgramLoader){
        this.listingProgram = new Listing(programLoader);
        this.bidReceiptProgram = new BidReceipt(programLoader);
        this.bidRequestProgram = new BidRequest(programLoader);
    }

    /**
     * @description Get all user data
     * 
     * @returns {UserdataType[]} The user data
     */
    public async getAll(): Promise<UserdataType[]> {
        try{
            const allUserdata = await this.programLoader.program.account.userData.all();

            return allUserdata as unknown as UserdataType[];
        }catch(e: any){
            console.error(e);
            throw new Error(`Failed to get all userdata: ${e.message}`);
        }
    }


    /**
     * @description Get user data by pubkey
     * 
     * @param {PublicKey} userdataPubkey - The user data pubkey
     * @returns {UserdataType} The user data
     */
    public async get(userdataPubkey: PublicKey): Promise<UserdataType | null> {
        try{
            // const seed1 = [anchor.utils.bytes.utf8.encode("deserialize"), Uint8Array.from(this.programLoader.wallet.publicKey.toBuffer())]
            // const [applicationState, applicationStateBump] = PublicKey.findProgramAddressSync(seed1, this.programLoader.program.programId);
            // console.log("Application State", applicationState)
            // console.log("Application State Bump", applicationStateBump)
        
            // let appState = await this.programLoader.program.account.applicationState.fetch(applicationState)
            let appState = await this.programLoader.program.account.applicationState.fetch(this.programLoader.applicationState);
            console.log("[*] APPLICATION STATE: ", JSON.stringify(appState, null, 2));

            const seeds = [anchor.utils.bytes.utf8.encode("user_account"), Uint8Array.from(userdataPubkey.toBuffer()), Uint8Array.from(appState.listingAuthority.toBuffer()), anchor.utils.bytes.utf8.encode("deserialize")];
            const [userdataAccount, _] = PublicKey.findProgramAddressSync(seeds, this.programLoader.program.programId);
            var userdataAccountInfo = await this.programLoader.connection.getAccountInfo(userdataAccount);
            if(!userdataAccountInfo) return null;
            const userdata = await this.programLoader.program.account.userData.fetch(userdataAccount);


            return userdata as unknown as UserdataType;
        }catch(e: any){
            console.error(e);
            throw new Error(`Failed to get userdata: ${e.message}`);
        }
    }


    /**
     * @description Get user data or create userdata by pubkey
     * 
     * @param {PublicKey} userdataPubkey - The user data pubkey
     * @returns {UserdataType} The user data
     */
    public async getOrCreate(userdataPubkey: PublicKey): Promise<UserdataType | Transaction> {
        try{
            // const seed1 = [anchor.utils.bytes.utf8.encode("deserialize"), Uint8Array.from(this.programLoader.wallet.publicKey.toBuffer())]
            // const [applicationState, applicationStateBump] = PublicKey.findProgramAddressSync(seed1, this.programLoader.program.programId);
            // console.log("Application State", applicationState)
            // console.log("Application State Bump", applicationStateBump)
        
            let appState = await this.programLoader.program.account.applicationState.fetch(this.programLoader.applicationState)
            console.log("[*] APPLICATION STATE: ", JSON.stringify(appState, null, 2));

            const seeds = [anchor.utils.bytes.utf8.encode("user_account"), Uint8Array.from(userdataPubkey.toBuffer()), Uint8Array.from(appState.listingAuthority.toBuffer()), anchor.utils.bytes.utf8.encode("deserialize")];
            const [userdataAccount, _] = PublicKey.findProgramAddressSync(seeds, this.programLoader.program.programId);
            var userdataAccountInfo = await this.programLoader.connection.getAccountInfo(userdataAccount);
            console.log("[*] Userdata Account Info: ", JSON.stringify(userdataAccountInfo, null, 4));
            if(!userdataAccountInfo) {
                //create userdata
                let tx = await this.create(userdataPubkey);
                tx.feePayer = userdataPubkey;
                tx.recentBlockhash = (await this.programLoader.connection.getLatestBlockhash()).blockhash;
                return tx;
            }

            const userdata = await this.programLoader.program.account.userData.fetch(userdataAccount);


            return userdata as unknown as UserdataType;
        }catch(e: any){
            console.error(e);
            throw new Error(`Failed to get userdata: ${e.message}`);
        }
    }


    /**
     * @description Create user data
     * 
     * @param {PublicKey} user - The user pubkey
     * @returns {Transaction} The transaction
     */
    public async create(user: PublicKey): Promise<Transaction> {
        try{
            // const seed1 = [anchor.utils.bytes.utf8.encode("deserialize"), Uint8Array.from(this.programLoader.wallet.publicKey.toBuffer())]
            // const [applicationState, applicationStateBump] = PublicKey.findProgramAddressSync(seed1, this.programLoader.program.programId);
            // console.log("Application State", applicationState)
            // console.log("Application State Bump", applicationStateBump)
        
            let appState = await this.programLoader.program.account.applicationState.fetch(this.programLoader.applicationState)
            console.log("[*] APPLICATION STATE: ", JSON.stringify(appState, null, 2));
        
            const seeds = [anchor.utils.bytes.utf8.encode("user_account"), Uint8Array.from(user.toBuffer()), Uint8Array.from(appState.listingAuthority.toBuffer()), anchor.utils.bytes.utf8.encode("deserialize")];
            const [userdata, userdataBump] = PublicKey.findProgramAddressSync(seeds, this.programLoader.program.programId);
            console.log("[*] Userdata: ", userdata);
            console.log("[*] Userdata bump: ", userdataBump);
        
            let tx = await this.programLoader.program.methods
            .initializeAccount()
            .accounts({
              applicationState: this.programLoader.applicationState,
              userdata,
              user,
              systemProgram: SystemProgram.programId
            })
            .transaction()

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



    /**
     * @description Get user bid receipts
     * 
     * @param {PublicKey} user - The user pubkey
     * @returns {BidReceiptType[]} The bid receipts
     */
    public async getBidReceipts(user: PublicKey): Promise<BidReceiptType[]> {
        try{
            let userdata = await this.get(user);
            console.log("[*] USERDATA: ", JSON.stringify(userdata, null, 4));
            if(userdata instanceof Transaction || userdata.bids.length === 0) return [];
            let bidReceipts = await this.bidReceiptProgram.fetchMany(userdata.bids);

            return bidReceipts;
        }catch(e: any){
            console.error(e);
            throw new Error(`Failed to get bid receipts: ${e.message}`);
        }
    }


    /**
     * @description Get user listings
     * 
     * @param {PublicKey} user - The user pubkey
     * @returns {ListingType[]} The listings
     */
    public async getListings(user: PublicKey): Promise<ListingType[]> {
        try{
            let userdata = await this.get(user);
            if(userdata instanceof Transaction || userdata.listings.length === 0) return [];
            let listings = await this.listingProgram.fetchMany(userdata.listings);

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


    /**
     * @description Get user bid requests
     * @param {PublicKey} user the user pubkey
     * @returns {BidRequestType[]}
     */
    public async getBidRequests(user: PublicKey): Promise<BidRequestType[]> {
        try{
            let userdata = await this.get(user);
            if(userdata instanceof Transaction || userdata.bidRequests.length === 0) return [];
            let bidRequests = await this.bidRequestProgram.fetchMany(userdata.bidRequests);

            return bidRequests;
        }catch(e: any){
            console.error(e);
            throw new Error(`Failed to get bid requests: ${e.message}`);
        }
    }
}