import {
    AddressLookupTableAccount,
    ComputeBudgetProgram,
    Connection,
    GetProgramAccountsFilter,
    Keypair,
    PublicKey,
    SystemProgram,
    Transaction,
    TransactionInstruction,
    TransactionMessage,
    TransactionSignature,
    VersionedTransaction,
} from "@solana/web3.js";
import {
    AnchorProvider,
    Program,
    Wallet,
    BN,
    Idl,
} from "@coral-xyz/anchor";
import { IDL, BasketsIDL } from "./basketsIDL";
import { Basket } from "./basketState";
import {
    CreateBasketParams,
    DATABASE_ADDESS,
    FilterOption,
    BasketError,
    BASKETS_PROGRAM_ID,
    BASKETS_PROGRAM_PDA,
    TokenSettings,
    TOKEN_LIST_ADDRESS,
    ADDITIONAL_FEE,
    SimpleEditParams,
    SimpleCreateParams,
    Side,
    ADDITIONAL_UNITS,
    SWB_PID,
    BASKETS_LOOKUP_TABLE_1,
    BASKETS_LOOKUP_TABLE_2,
    TransactionToSend,
} from "./config";
import { confirmTransaction, delay, fetchTokenList, generateJupSwapInstruction, generateJupTxData, getAddressLookupTableAccounts, getCurrentComposition, getFilteredProgramAccounts, getOraclePrices, sendSignedTransactions, signTransactionsWithWallet, signVersionedTransactions, tryMetadata } from "./utils";
import { BuyState } from "./buyState";
import { simulate } from "./simulation";
import { TOKEN_PROGRAM_ID } from "./splTokenHelpers";
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
import { buildDepositAfterRebalanceIx, buildUpdateCurrentWeightsIx, buildWithdrawBeforeRebalanceIx, updateOraclesTxs } from "./instructionsBuilder";
import { buildRebalanceTransactions, calculateRebalanceAmounts, getConfirmedTimestamp, getFlashRebalanceInfo, getJupiterSwapData, getLookupTableAccount, getSortedRebalanceInfo, isForceRebalanceNeeded, shouldProcessRebalance, signAndSendTransactions } from "./flashRebalance";

export * as BasketInstructions from "./basketInstructions";
export * from "./config";
export * from "./basketState";
export * from "./buyState";
export * from "./utils";
export * from "./basketsIDL";

export class BasketsSDK {

    private connection: Connection;
    private program: Program<BasketsIDL>;
    private swbProgram: Program;
    private swbIDL: Idl;
    private tokenList: TokenSettings[];
    private lookups: AddressLookupTableAccount[];
    private wallet?: Wallet;
    private lamports: number;
    private jupAPIkey: string = "https://quote-api.jup.ag/v6/";

    constructor(
        connection: Connection,
        program: Program<BasketsIDL>,
        swbProgram: Program,
        swbIDL: Idl,
        tokenList: TokenSettings[],
        lookups: AddressLookupTableAccount[],
        wallet?: Wallet
    ) {
        this.connection = connection;
        this.wallet = wallet;
        this.program = program;
        this.swbProgram = swbProgram;
        this.swbIDL = swbIDL;
        this.tokenList = tokenList;
        this.lamports = ADDITIONAL_FEE;
        this.lookups = lookups;
    }
    
    static async init(
        connection: Connection,
        wallet?: Wallet,
    ): Promise<BasketsSDK> {
        let provider = new AnchorProvider(
            connection,
            wallet ? wallet : new NodeWallet(Keypair.generate()),
            {
                skipPreflight: true,
                preflightCommitment: "recent",
                commitment: "processed",
            }
        );
        
        // const symmetryEngineIDL: BasketsIDL = (await Program.fetchIdl(BASKETS_PROGRAM_ID, provider))!;
        let program = new Program<BasketsIDL>(IDL, provider);
    
        let swbIDL = (await Program.fetchIdl(SWB_PID, provider))!;
        let swbProgram = new Program(swbIDL, provider);

        let tokenList = await fetchTokenList(program);

        let lookups = await Promise.all([
            getLookupTableAccount(program.provider.connection, BASKETS_LOOKUP_TABLE_1),
            getLookupTableAccount(program.provider.connection, BASKETS_LOOKUP_TABLE_2)
        ]);

        return new BasketsSDK(
            connection,
            program,
            swbProgram,
            swbIDL,
            tokenList,
            lookups,
            wallet,
        )
    }

    setWallet(wallet: Wallet) {
        this.wallet = wallet;
        let provider = new AnchorProvider(
            this.connection,
            wallet,
            {
                skipPreflight: true,
                preflightCommitment: "recent",
                commitment: "recent",
            }
        )
        this.program = new Program<BasketsIDL>(IDL, provider);
        this.swbProgram = new Program(this.swbIDL, provider);
    }

    setPriorityFee(lamports: number) {
        this.lamports = lamports;
    }

    setJupAPIKey(apiKey: string) {
        this.jupAPIkey = apiKey;
    }

    async loadFromPubkey(pubkey: PublicKey): Promise<Basket> {
        return await Basket.loadFromPubkey(
            this.program,
            pubkey
        );
    }

    async getCurrentCompositions(baskets: Basket[]): Promise<any> {
        let oraclePriceData = await getOraclePrices(this.program, this.tokenList);
        let parsed = baskets.map(basket => getCurrentComposition(basket, this.tokenList, oraclePriceData));
        await Promise.all(parsed.map(x  => tryMetadata(x))).then(metadatas => {
            for (let i = 0; i < parsed.length; i++)
                parsed[i].metadata = metadatas[i];
        })
        return parsed;
    }

    async findBaskets(filters: FilterOption[]): Promise<Basket[]> {
        let accountFilters: GetProgramAccountsFilter[] = [{dataSize: 10208}];
        accountFilters.push({memcmp: {
            offset: 112,
            bytes: "11111111"
        }})
        accountFilters = [...accountFilters, ...filters.map(filter => {
            return {
                memcmp: {
                    offset: (filter.filterType == "host") ? 128 : 16,
                    bytes: filter.filterPubkey.toBase58(),
                }
            }
        })];
        let accounts = await getFilteredProgramAccounts(
            this.connection,
            accountFilters
        ).catch((e) => {console.log(e); return [];}); 
        return accounts
            .map(account => Basket.loadFromRawData(this.program, account))
    }

    async findActiveSellStates(user: PublicKey): Promise<Basket[]> {
        let accounts = await getFilteredProgramAccounts(
            this.connection,
            [
                { dataSize: 10208 },
                { memcmp: {
                    offset: 16,
                    bytes: user.toBase58()
                }},
                { memcmp: {
                    offset: 112,
                    bytes: "Ahg1opVcGX",
                }},
            ]
        );
        return accounts
            .map(account => Basket.loadFromRawData(this.program, account))
    }

    async findActiveBuyStates(user: PublicKey): Promise<BuyState[]> {
        let accounts = await getFilteredProgramAccounts(
            this.connection,
            [
                { dataSize: 680 },
                { memcmp: {
                    offset: 40,
                    bytes: user.toBase58()
                }}
            ]
        );
        return await BuyState.loadMultiple(this.program, accounts);
    }

    async fetchAllBuyStates(filters: FilterOption[]): Promise<BuyState[]> {
        let accountFilters: GetProgramAccountsFilter[] = [{ dataSize: 680 }];
        accountFilters = [...accountFilters, ...filters.map(filter => {
            return {
                memcmp: {
                    offset: (filter.filterType == "host") ? 104 : 72,
                    bytes: filter.filterPubkey.toBase58(),
                }
            }
        })];
        let accounts = await getFilteredProgramAccounts(
            this.connection,
            accountFilters
        );
        return await BuyState.loadMultiple(this.program, accounts);
    }

    async fetchBuyStateFromPubkey(pubkey: PublicKey): Promise<BuyState> {
        return await BuyState.loadFromPubkey(this.program, pubkey);
    }

    async fetchAllSellStates(filters: FilterOption[]): Promise<Basket[]> {
        let accountFilters: GetProgramAccountsFilter[] = [{ dataSize: 10208 }];
        accountFilters.push({memcmp: {
            offset: 112,
            bytes: "Ahg1opVcGX"
        }})
        accountFilters = [...accountFilters, ...filters.map(filter => {
            return {
                memcmp: {
                    offset: (filter.filterType == "host") ? 128 : 16,
                    bytes: filter.filterPubkey.toBase58(),
                }
            }
        })];
        let accounts = await getFilteredProgramAccounts(
            this.connection,
            accountFilters
        );
        return accounts
            .map(account => Basket.loadFromRawData(this.program, account))
    }

    async fetchAllHoldings(user: PublicKey): Promise<{basketAddress: string, mint: string, balance: number, basketData: any}[]> {
        let tokenAccountsRaw = await this.connection.getTokenAccountsByOwner(
            new PublicKey(user),
            { programId: new PublicKey(TOKEN_PROGRAM_ID) },
            "processed"
        );
        let tokenAccountsDecoded = tokenAccountsRaw.value.map(account => {
            return {
                mint: new PublicKey(account.account.data.slice(0,32)),
                balance: parseInt(new BN(account.account.data.slice(64, 72), "le").toString()),
            }
        });

        let basketsRaw = await getFilteredProgramAccounts(
            this.connection,
            [
                {dataSize: 10208},
                {
                    memcmp: {
                        offset: 112,
                        bytes: "11111111"
                    }
                }
            ]
        );
        let basketsDecoded = basketsRaw.map(account => {
            return {
                decoded: this.program.coder.accounts.decode("fundState", account.account.data),
                pubkey: account.pubkey
            }
        });

        let basketHoldings = [];
        for (let i = 0; i < basketsDecoded.length; i++) {
            let ataIndex = tokenAccountsDecoded
                .findIndex(x => x.mint.toBase58() == basketsDecoded[i].decoded.fundToken.toBase58());
            if (ataIndex == -1) continue;
            basketHoldings.push({
                basketAddress: basketsDecoded[i].pubkey.toBase58(),
                mint: basketsDecoded[i].decoded.fundToken.toBase58(),
                balance: tokenAccountsDecoded[ataIndex].balance / 10 ** 6,
                basketData: basketsDecoded[i].decoded,
            })
        }

        return basketHoldings;
    }

    getTokenListData(): TokenSettings[] {
        return this.tokenList.filter(x => x.isLive == true);
    }

    tokenIdFromMint(tokenMint: string): number {
        for (let i = 0; i < this.tokenList.length; i++)
            if (this.tokenList[i].tokenMint == tokenMint)
                return this.tokenList[i].id;
        throw new BasketError("Token not supported by Symmetry Engine")
    }

    async createBasket(createBasketParams: SimpleCreateParams): Promise<Basket> {
        if (!this.wallet)
            throw new BasketError("Wallet not provided");
        return await Basket.create(
            this.program,
            this.connection,
            this.wallet,
            this.tokenList,
            this.lookups,
            createBasketParams,
            this.lamports,
        )
    }

    async simulateBasket(createBasketParams: CreateBasketParams, simulationDays: number) {
        return await simulate(
            this.program,
            DATABASE_ADDESS,
            TOKEN_LIST_ADDRESS,
            createBasketParams,
            simulationDays
        )
    }

    async editBasket(basket: Basket, editBasketParams: SimpleEditParams): Promise<TransactionSignature> {
        if (!this.wallet)
            throw new BasketError("Wallet not provided");
        return await basket.edit(
            this.program,
            this.connection,
            this.wallet,
            this.tokenList,
            this.lookups,
            editBasketParams,
            this.lamports,
        )
    }

    async setMetadata(basket: Basket, symbol: string, name: string, uri: string = ""): Promise<TransactionSignature> {
        if (!this.wallet)
            throw new BasketError("Wallet not provided");
        return await basket.setMetaData(
            this.program,
            this.wallet,
            symbol,
            name,
            uri,
            this.lamports,
        )
    }

    async editManager(
        basket: Basket,
        newManager: PublicKey,
    ): Promise<TransactionSignature> {
        if (!this.wallet)
            throw new BasketError("Wallet not provided");
        return await basket.editManager(
            this.program,
            this.connection,
            this.wallet,
            newManager,
            this.lamports,
        )
    }

    // async simpleEditBasket(
    //     basket: Basket,
    //     basketParams: SimpleEditParams,
    // ): Promise<TransactionSignature[]> {
    //     if (!this.wallet)
    //         throw new BasketError("Wallet not provided");
    //     return await basket.simpleEdit(
    //         this.program,
    //         this.connection,
    //         this.wallet,
    //         this.tokenList,
    //         basketParams,
    //         this.lamports,
    //     )
    // }

    async closeBasket(basket: Basket): Promise<TransactionSignature> {
        if (!this.wallet)
            throw new BasketError("Wallet not provided");
        return await basket.close(this.program, this.wallet, this.lamports);
    }

    async refilterBasketInstruction(basket: Basket): Promise<TransactionInstruction> {
        //@ts-ignore
        return await refilterInstruction(this.program, basket.ownAddress, this.wallet?.publicKey);
    }

    // async refilterBasket(basket: Basket): Promise<TransactionSignature> {
    //     if (!this.wallet)
    //         throw new BasketError("Wallet not provided");
    //     return await basket.refilter(
    //         this.program,
    //         this.wallet,
    //         this.lamports,
    //     );
    // }

    async reweightBasketInstruction(basket: Basket): Promise<TransactionInstruction> {
        //@ts-ignore
        return await reweightInstruction(this.program, basket.ownAddress, this.wallet?.publicKey);
    }

    // async reweightBasket(basket: Basket): Promise<TransactionSignature> {
    //     if (!this.wallet)
    //         throw new BasketError("Wallet not provided");
    //     return await basket.reweight(
    //         this.program,
    //         this.wallet,
    //         this.lamports,
    //     );
    // }

    // async rebalanceBasketToken(
    //     basket: Basket,
    //     token: PublicKey,
    //     updateOracles: boolean = true,
    // ): Promise<TransactionSignature> {
    //     let oraclePriceData = await getOraclePrices(this.program, this.tokenList);
    //     //@ts-ignore
    //     let tokenSettings: TokenSettings = this.tokenList.find(x => x.tokenMint == token.toBase58());
    //     if (!this.wallet)
    //         throw new BasketError("Wallet not provided");
    //     if (!tokenSettings)
    //         throw new BasketError("Token not supported");
    //     let timestamp = await this.connection.getBlockTime(
    //         await this.connection.getSlot("finalized")
    //     ).catch((e) => null);
    //     let rebalanceInfos = basket.getRebalanceInfo(
    //         this.program,
    //         this.tokenList,
    //         oraclePriceData,
    //         timestamp ? timestamp : 2000000000,
    //         (this.wallet.publicKey.equals(basket.data.manager) && basket.data.activelyManaged.toNumber() == 1)
    //     );
    //     let rebalanceInfo = rebalanceInfos.find(info => info.tokenId == tokenSettings.id);
    //     if (!rebalanceInfo)
    //         throw new BasketError("Token not in composition");
    //     let jupSwapData = await generateJupSwapInstruction(
    //         rebalanceInfo,
    //         basket.data.rebalanceSlippage.toNumber(),
    //         this.connection,
    //         rebalanceInfo.side == Side.To ?
    //             oraclePriceData[rebalanceInfo.tokenId] / 10 ** this.tokenList[rebalanceInfo.tokenId].decimals :
    //             oraclePriceData[0] / 10 ** this.tokenList[0].decimals,
    //         rebalanceInfo.side == Side.From ?
    //             oraclePriceData[rebalanceInfo.tokenId] / 10 ** this.tokenList[rebalanceInfo.tokenId].decimals :
    //             oraclePriceData[0] / 10 ** this.tokenList[0].decimals,
    //         rebalanceInfo.tokenId == 48 ? this.tokenList[5].tokenMint : this.tokenList[1].tokenMint,
    //         rebalanceInfo.tokenId == 48 ? this.tokenList[5].pdaTokenAccount : this.tokenList[1].pdaTokenAccount,
    //         this.jupAPIkey,
    //     ).catch((e) => {console.log("JUP SWAP DATA", e.message); return null;})
    //     if (!jupSwapData)
    //         throw new BasketError("Couldn't load quote.");
    //     return await basket.rebalanceSingle(
    //         this.program,
    //         this.wallet,
    //         this.tokenList,
    //         jupSwapData,
    //         rebalanceInfo,
    //         this.lamports,
    //         updateOracles
    //     )
    // }

    async rebalanceBasket(basket: Basket, updateOracles: boolean = true): Promise<TransactionSignature[]> {
        if (!this.wallet)
            throw new BasketError("Wallet not provided");
        let [oraclePriceData, timestamp, updateOraclesTxData] = await Promise.all([
            getOraclePrices(this.program, this.tokenList),
            getConfirmedTimestamp(this.connection, basket),
            updateOracles ? updateOraclesTxs(
                this.swbProgram,
                this.wallet.publicKey,
                basket.getSwbFeeds(this.tokenList),
                this.lamports,
            ) : []
        ]);

        let rebalanceInfos = basket.getRebalanceInfo(
            this.program,
            this.tokenList,
            oraclePriceData,
            timestamp ? timestamp : 2000000000,
            (this.wallet.publicKey.equals(basket.data.manager) && basket.data.activelyManaged.toNumber() == 1)
        );
        let jupSwapDatas = await Promise.all(
            rebalanceInfos.map(rebalanceInfo =>
                generateJupSwapInstruction(
                    rebalanceInfo,
                    basket.data.rebalanceSlippage.toNumber(),
                    this.connection,
                    rebalanceInfo.side == Side.To ?
                        oraclePriceData[rebalanceInfo.tokenId] / 10 ** this.tokenList[rebalanceInfo.tokenId].decimals :
                        oraclePriceData[0] / 10 ** this.tokenList[0].decimals,
                    rebalanceInfo.side == Side.From ?
                        oraclePriceData[rebalanceInfo.tokenId] / 10 ** this.tokenList[rebalanceInfo.tokenId].decimals :
                        oraclePriceData[0] / 10 ** this.tokenList[0].decimals,
                    rebalanceInfo.tokenId == 48 ? this.tokenList[5].tokenMint : this.tokenList[1].tokenMint,
                    rebalanceInfo.tokenId == 48 ? this.tokenList[5].pdaTokenAccount : this.tokenList[1].pdaTokenAccount,
                    this.jupAPIkey,
                ).catch((e) => {console.log("JUP SWAP DATA", e.message); return null;})
            )
        );
        let txsToSend: TransactionToSend[] = updateOraclesTxData;
        let updates = txsToSend.length;
        await basket.rebalanceTo(
            this.program,
            this.wallet,
            this.tokenList, //@ts-ignore
            jupSwapDatas,
            rebalanceInfos,
            this.lookups,
            this.lamports,
        ).then(result => txsToSend.push(...result));
        let toSwaps = txsToSend.length - updates;
        if (basket.data.sellState.toNumber() == 0)
            await basket.rebalanceFrom(
                this.program,
                this.wallet,
                this.tokenList, //@ts-ignore
                jupSwapDatas,
                rebalanceInfos,
                this.lookups,
                this.lamports,
            ).then(result => txsToSend.push(...result));
        let blockhash = (await this.connection.getLatestBlockhash("confirmed")).blockhash;
        let signedTransactions = await signVersionedTransactions(
            this.wallet,
            txsToSend.map(tx => new VersionedTransaction(
                new TransactionMessage({
                    payerKey: tx.payerKey,
                    recentBlockhash: blockhash,
                    instructions: tx.instructions,
                }).compileToV0Message(tx.lookupTables)
            ))
        );
        let updateTxs = await sendSignedTransactions(
            this.connection,
            signedTransactions.slice(0, updates),
            1,
        );
        let rebalanceTo = await sendSignedTransactions(
            this.connection,
            signedTransactions.slice(updates, updates + toSwaps),
            0,
        );
        let rebalanceFrom = await sendSignedTransactions(
            this.connection,
            signedTransactions.slice(updates + toSwaps, signedTransactions.length),
            0,
        );
        let resultTxs = [...updateTxs, ...rebalanceFrom, ...rebalanceTo];
        if (resultTxs.length > 0)
            await confirmTransaction(this.connection, resultTxs[resultTxs.length - 1]).catch(() => {});
        return resultTxs;
    }

    async updateAllSwbOracles() {
        if (!this.wallet) {
            throw new BasketError("Wallet not provided");
        }
        let feeds: PublicKey[] = [];
        for (let i = 0; i < this.tokenList.length; i++)
            if (this.tokenList[i].oracleType == "SwbOnDemand")
                feeds.push(new PublicKey(this.tokenList[i].oracleAccount));
        let txsToSend = await updateOraclesTxs(
            this.swbProgram,
            this.wallet.publicKey,
            feeds,
            this.lamports,
        );
        return signAndSendTransactions(txsToSend, this.connection, this.wallet, 0);
    }

    async rebalanceCronJob(
        basket: Basket,
        updateOracles: boolean = true,
        maxAllowedAccounts: number = 45,
        softCap: number = 5,
        hardCap: number = 5000,
        underTokens: number = 3,
        overTokens: number = 3,
    ): Promise<TransactionSignature[]> {
        if (!this.wallet) {
            throw new BasketError("Wallet not provided");
        }

        const [oraclePriceData, timestamp, updateOraclesTxData] = await Promise.all([
            getOraclePrices(this.program, this.tokenList),
            getConfirmedTimestamp(this.connection, basket),
            updateOracles ? updateOraclesTxs(
                this.swbProgram,
                this.wallet.publicKey,
                basket.getSwbFeeds(this.tokenList),
                this.lamports,
            ) : []
        ]);

        const forceRebalance = isForceRebalanceNeeded(basket, this.wallet.publicKey);
        const rebalanceInfos = getSortedRebalanceInfo(basket, oraclePriceData, timestamp, this.tokenList);

        const txsToSend = await buildRebalanceTransactions(
            basket,
            rebalanceInfos,
            oraclePriceData,
            forceRebalance,
            this.lookups,
            maxAllowedAccounts,
            this.wallet.publicKey,
            this.connection,
            this.program,
            this.tokenList,
            this.lamports,
            updateOraclesTxData,
            softCap,
            hardCap,
            underTokens,
            overTokens,
            this.jupAPIkey,
        );
    
        return signAndSendTransactions(txsToSend, this.connection, this.wallet, updateOracles == true ? 1 : 0);
    }

    async filterCronJobBaskets(allBaskets: Basket[], softCap: number = 10): Promise<Basket[]> {
        const timestamp = Date.now() / 1000;
        const oraclePriceData = await getOraclePrices(this.program, this.tokenList);
        let basketsToRebalance = [];
        for (let i = 0; i < allBaskets.length; i++) {
            let basket = allBaskets[i];
            let possibleRebalances = 0;
            if (basket.data.disableRebalance.toNumber() == 1) continue;
            let rebalanceInfos = getSortedRebalanceInfo(
                basket,
                oraclePriceData,
                timestamp,
                this.tokenList
            );
            for (const over of rebalanceInfos.over) {
                for (const under of rebalanceInfos.under) {
                     if (!shouldProcessRebalance(over, under, false)) continue;
                    const { from, to, tokenAmount, value } = calculateRebalanceAmounts(
                        over,
                        under,
                        oraclePriceData,
                        this.tokenList,
                        10000,
                    );
                    if (value <= softCap) continue;
                    possibleRebalances += 1;
                }
            }
            if (possibleRebalances > 0)
                basketsToRebalance.push(basket)
        }
        return basketsToRebalance;
    }

    async buyBasket(basket: Basket, amountUsdc: number): Promise<BuyState> {
        if (!this.wallet)
            throw new BasketError("Wallet not provided");
        return await BuyState.createNew(
            this.program,
            this.wallet,
            this.tokenList,
            basket,
            amountUsdc,
            this.lamports,
        )
    }

    async getOraclePrices(): Promise<number[]> {
        let oraclePrices = await getOraclePrices(this.program, this.tokenList);
        return oraclePrices;
    }

    async computeMintAmountWithMultipleTokens(basket: Basket, contribution: {token: PublicKey, amount: number}[]): Promise<number> {
        let oraclePrices = await getOraclePrices(this.program, this.tokenList);
        let amount = BuyState.computeMintAmountWithMultipleTokens(
            this.tokenList,
            basket,
            contribution,
            oraclePrices
        );
        return amount;
    }

    // async buyBasketWithMultipleTokens(
    //     basket: Basket,
    //     contribution: {token: PublicKey, amount: number}[],
    //     updateOracles: boolean = true,
    // ): Promise<TransactionSignature> {
    //     if (!this.wallet)
    //         throw new BasketError("Wallet not provided");
    //     return await BuyState.multipleTokensDeposit(
    //         this.program,
    //         this.wallet,
    //         this.tokenList,
    //         basket,
    //         contribution,
    //         this.lamports,
    //         updateOracles,
    //     );
    // }

    async computeMintAmountWithSingleToken(basket: Basket, token: PublicKey, amount: number): Promise<number> {
        let oraclePrices = await getOraclePrices(this.program, this.tokenList);
        let tokenSettings = this.tokenList.find(x => x.tokenMint == token.toBase58());
        if (!tokenSettings)
            throw new BasketError("Token not in composition");
        let exp = BuyState.computeMintAmountWithSingleToken(
            this.tokenList,
            basket,
            tokenSettings,
            amount,
            oraclePrices
        );
        return exp;
    }

    async buyWithSingleToken(
        basket: Basket,
        token: PublicKey,
        amount: number,
        updateOracles: boolean = true,
    ): Promise<TransactionSignature> {
        if (!this.wallet)
            throw new BasketError("Wallet not provided");
         return await BuyState.singleTokenDeposit(
            this.program,
            this.wallet,
            this.tokenList,
            basket,
            token,
            amount,
            this.lamports,
            updateOracles,
        );
    }

    async rebalanceBuyState(
        buyState: BuyState,
        updateOracles: boolean = true,
    ): Promise<TransactionSignature[]> {
        if (!this.wallet)
            throw new BasketError("Wallet not provided");
        let [oraclePriceData, updateOraclesTxData] = await Promise.all([
            await getOraclePrices(this.program, this.tokenList),
            updateOracles ? updateOraclesTxs(
                this.swbProgram,
                this.wallet.publicKey,
                buyState.basket.getSwbFeeds(this.tokenList),
                this.lamports,
            ) : []
        ]);
        let rebalanceInfos = buyState.getBuyStateRebalanceInfo(this.tokenList);
        let jupSwapDatas = await Promise.all(
            rebalanceInfos.map(rebalanceInfo =>
                generateJupSwapInstruction(
                    rebalanceInfo,
                    buyState.basket.data.rebalanceSlippage.toNumber(),
                    this.connection,
                    rebalanceInfo.side == Side.To ?
                        oraclePriceData[rebalanceInfo.tokenId] / 10 ** this.tokenList[rebalanceInfo.tokenId].decimals :
                        oraclePriceData[0] / 10 ** this.tokenList[0].decimals,
                    rebalanceInfo.side == Side.From ?
                        oraclePriceData[rebalanceInfo.tokenId] / 10 ** this.tokenList[rebalanceInfo.tokenId].decimals :
                        oraclePriceData[0] / 10 ** this.tokenList[0].decimals,
                    rebalanceInfo.tokenId == 48 ? this.tokenList[5].tokenMint : this.tokenList[1].tokenMint,
                    rebalanceInfo.tokenId == 48 ? this.tokenList[5].pdaTokenAccount : this.tokenList[1].pdaTokenAccount,
                    this.jupAPIkey
                ).catch((e) => {console.log("JUP SWAP DATA", e.message); return null;})
            )
        );

        return await buyState.rebalanceBuyState(
            this.program,
            this.wallet,
            this.tokenList, //@ts-ignore
            jupSwapDatas,
            this.lamports,
            updateOraclesTxData,
            this.lookups,
        );
    }

    async mintBasket(
        buyState: BuyState,
        updateOracles: boolean = true,
    ): Promise<TransactionSignature[]> {
        if (!this.wallet)
            throw new BasketError("Wallet not provided");
        return await buyState.mint(
            this.program,
            this.swbProgram,
            this.wallet,
            this.tokenList,
            this.lookups,
            this.lamports,
            updateOracles,
        );
    }

    // async sellBasketToSingleToken(
    //     basket: Basket,
    //     withdrawToken: PublicKey,
    //     amount: number,
    //     updateOracles: boolean = true,
    // ): Promise<TransactionSignature> {
    //     if (!this.wallet)
    //         throw new BasketError("Wallet not provided");
    //     return await basket.sellBasketToSingleToken(
    //         this.program,
    //         this.wallet,
    //         this.tokenList,
    //         withdrawToken,
    //         amount,
    //         this.lamports,
    //         updateOracles,
    //     )
    // }

    async computeOutputAmountWithSingleToken(basket: Basket, withdrawToken: PublicKey, amount: number): Promise<number> {
        let oraclePrices = await getOraclePrices(this.program, this.tokenList);
        let tokenSettings = this.tokenList.find(x => x.tokenMint == withdrawToken.toBase58());
        if (!tokenSettings)
            throw new BasketError("Token not in composition");
        let exp = basket.computeOutputAmountWithSingleToken(
            oraclePrices,
            this.tokenList,
            tokenSettings,
            amount,
        );
        return exp;
    }

    async sellBasket(basket: Basket, amount: number, rebalance: number): Promise<Basket> {
        if (!this.wallet)
            throw new BasketError("Wallet not provided");
        return await basket.sell(
            this.program,
            this.wallet,
            amount,
            rebalance,
            this.lamports,
        )
    }

    async claimTokensFromBuyState(buyState: BuyState): Promise<TransactionSignature[]> {
        if (!this.wallet)
            throw new BasketError("Wallet not provided");
        return await buyState.claimTokens(
            this.program,
            this.wallet,
            this.tokenList,
            this.lamports,
        )
    }

    async claimTokens(basket: Basket): Promise<TransactionSignature[]> {
        if (!this.wallet)
            throw new BasketError("Wallet not provided");
        return await basket.claimTokens(
            this.program,
            this.wallet,
            this.tokenList,
            this.lamports,
        )
    }

    async removeDust(
        basket: Basket,
        updateOracles: boolean = true,
    ): Promise<TransactionSignature[]> {
        if (!this.wallet)
            throw new BasketError("Wallet not provided");
        let oraclePriceData = await getOraclePrices(this.program, this.tokenList);
        return await basket.removeDust(
            this.program,
            this.wallet,
            this.tokenList,
            oraclePriceData,
            this.lamports,
            updateOracles,
        );
    }

    async freezeProgram(): Promise<TransactionSignature> {
        if (!this.wallet)
            throw new BasketError("Wallet not provided");
        let transaction = new Transaction();
        transaction = transaction.add(
            await this.program.methods.freezeProgram().accounts({
                owner: this.wallet?.publicKey,
                tokenList: TOKEN_LIST_ADDRESS,
                systemProgram: SystemProgram.programId,
            }).instruction()
        );
        transaction = transaction.add(
            ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS})
        ).add(
            ComputeBudgetProgram.setComputeUnitPrice({microLamports: this.lamports})
        );
        let signedTransactions = await signTransactionsWithWallet(
            this.program.provider.connection,
            this.wallet,
            [{transaction: transaction, signers: []}]
        );
        return (await sendSignedTransactions(
            this.program.provider.connection,
            signedTransactions,
            1
        ))[0]
    }

    async unfreezeProgram(): Promise<TransactionSignature> {
        if (!this.wallet)
            throw new BasketError("Wallet not provided");
        let transaction = new Transaction();
        transaction = transaction.add(
            await this.program.methods.unfreezeProgram().accounts({
                owner: this.wallet?.publicKey,
                tokenList: TOKEN_LIST_ADDRESS,
                systemProgram: SystemProgram.programId,
            }).instruction()
        );
        transaction = transaction.add(
            ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS})
        ).add(
            ComputeBudgetProgram.setComputeUnitPrice({microLamports: this.lamports})
        );
        let signedTransactions = await signTransactionsWithWallet(
            this.program.provider.connection,
            this.wallet,
            [{transaction: transaction, signers: []}]
        );
        return (await sendSignedTransactions(
            this.program.provider.connection,
            signedTransactions,
            1
        ))[0]
    }

}
