import { AddressLookupTableAccount, ComputeBudgetProgram, Keypair, PublicKey, SYSVAR_INSTRUCTIONS_PUBKEY, SYSVAR_RENT_PUBKEY, SystemProgram, TransactionInstruction, VersionedTransaction } from "@solana/web3.js";
import { BUY_FEE_ACCOUNT, BUY_FEE_WALLET, CREATE_FEE_ACCOUNT, CreateBasketParams, BASKETS_PROGRAM_ID, BASKETS_PROGRAM_PDA, FilterTime, FilterType, SortBy, TOKEN_LIST_ADDRESS, TOKEN_STATS_ADDRESS, TokenSettings, WeightTime, WeightType, ADDITIONAL_UNITS, SimpleCreateParams, SimpleEditParams, REBALANCE_FEE_ACCOUNT, REBALANCE_FEE_WALLET, TransactionToSend } from "./config";
import { MD5 } from "crypto-js";
import { BN, Program, Wallet } from "@coral-xyz/anchor";
import { BasketsIDL, IDL } from "./basketsIDL";
import { validateCreateBasketParams } from "./utils";
import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync } from "./splTokenHelpers";
import { Basket } from "./basketState";
import { BuyState } from "./buyState";
import { Metaplex } from "@metaplex-foundation/js";
import { FetchSignaturesMultiResponse, PullFeed } from "@switchboard-xyz/on-demand";

export async function buildCreateBasketIx(
    program: Program<BasketsIDL>,
    tokenList: TokenSettings[],
    basketParams: SimpleCreateParams,
): Promise<TransactionInstruction> {
    if (basketParams.composition.length == 0)
        throw new Error("Empty composition");
    if (!basketParams.feeDelegate)
        basketParams.feeDelegate = basketParams.manager;
    let assetPool = [0];
    let rules = [];
    let weightSum = 0;
    basketParams.composition.map(info => weightSum += info.weight);  

    for (let i = 0; i < basketParams.composition.length; i++) {
        let tokenSettings = tokenList.find(
            token => token.tokenMint == basketParams.composition[i].token.toBase58()
        );
        if (!tokenSettings)
            throw new Error("Token not supported.");
        if (tokenSettings.id != 0)
            assetPool.push(tokenSettings.id);
        rules.push({
            filterBy: FilterType.Fixed,
            filterDays: FilterTime.Day,
            sortBy: SortBy.DescendingOrder,
            totalWeight: Math.floor((basketParams.composition[i].weight * 1000) / weightSum),
            fixedAsset: tokenSettings.id,
            numAssets: 1,
            weightBy: WeightType.Fixed,
            weightDays: WeightTime.Day,
            weightExpo: 0,
            excludeAssets: [],
            ruleAssets: [],
        })
    }
    let validateBasketParams: CreateBasketParams = {
        ...basketParams,
        name: basketParams.name,
        symbol: basketParams.symbol,
        uri: basketParams.uri,
        manager: PublicKey.default,
        hostPlatform: PublicKey.default,
        hostPlatformFee: 0,
        activelyManaged: 1,
        assetPool: assetPool,
        rules: rules,
        refilterInterval: 7 * 24 * 3600,
        reweightInterval: 24 * 3600
    }
    await validateCreateBasketParams(validateBasketParams, tokenList, undefined, true);
    
    let tokens = rules.map(info => info.fixedAsset);
    let weights = rules.map(info => new BN(info.totalWeight));
    while (tokens.length < 15) tokens.push(0);
    while (weights.length < 15) weights.push(new BN(0));
    
    let message = basketParams.name + basketParams.symbol + basketParams.uri;
    let md5 = Array.from(Buffer.from(MD5(message).toString(), "hex"));

    let seedPubkey = Keypair.generate().publicKey;
    let [basketToken] = PublicKey.findProgramAddressSync(
        [Buffer.from("mint"), seedPubkey.toBuffer()],
        BASKETS_PROGRAM_ID
    );
    let [basketState] = PublicKey.findProgramAddressSync(
        [Buffer.from("basket"), seedPubkey.toBuffer()],
        BASKETS_PROGRAM_ID
    );

    if (basketParams.symbol.length < 3) throw new Error("Wrong symbol format");
    if (basketParams.name.length < 3) throw new Error("Wrong name format");
    const metaplex = Metaplex.make(program.provider.connection);
    const metadataAccount = metaplex.nfts().pdas().metadata({ mint: basketToken });

    let createInstruction =
        await program.methods
            .createBasket(
                basketParams.managerFee,
                basketParams.hostPlatformFee,
                basketParams.activelyManaged,
                new BN(basketParams.rebalanceInterval),
                basketParams.rebalanceThreshold,
                basketParams.rebalanceSlippage,
                basketParams.lpOffsetThreshold,
                [basketParams.disableRebalance? 1 : 0, basketParams.disableLp? 1 : 0],
                basketParams.composition.length,
                tokens,
                weights.map(x => x.toNumber()),
                {
                    name: basketParams.name,
                    symbol: basketParams.symbol,
                    uri: basketParams.uri,
                }
            )
            .accounts({
                manager: basketParams.manager,
                tokenList: TOKEN_LIST_ADDRESS,
                fundState: basketState,
                pdaAccount: BASKETS_PROGRAM_PDA,
                fundToken: basketToken,
                createFeeSweeper: CREATE_FEE_ACCOUNT,
                metadataAccount: metadataAccount,
                metadataProgram: new PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"),
                systemProgram: SystemProgram.programId,
                tokenProgram: TOKEN_PROGRAM_ID,
                rent: SYSVAR_RENT_PUBKEY,
                host: basketParams.hostPlatform,
                feeDelegate: basketParams.feeDelegate,
                seedPubkey: seedPubkey,
            })
            .instruction();
    return createInstruction;
}

export async function buildEditBasketIx(
    program: Program<BasketsIDL>,
    tokenList: TokenSettings[],
    basketObj: Basket,
    basketParams: SimpleEditParams,
): Promise<TransactionInstruction> {
    if (basketParams.composition.length == 0)
        throw new Error("Empty composition");

    if (!basketParams.feeDelegate)
        basketParams.feeDelegate = basketObj.data.feeDelegate;
    if (!basketParams.composition) {
        basketParams.composition = [];
        for (let i = 0; i < basketObj.data.numRuleTokens.toNumber(); i++)
            if (basketObj.data.ruleTokenWeights[i].toNumber() != 0)
                basketParams.composition.push({
                    token: new PublicKey(
                        tokenList[basketObj.data.ruleTokens[i].toNumber()].tokenMint
                    ),
                    weight: basketObj.data.ruleTokenWeights[i].toNumber()
                })
    }

    let assetPool = [0];
    let rules = [];
    let weightSum = 0;
    basketParams.composition.map(info => weightSum += info.weight);  

    for (let i = 0; i < basketParams.composition.length; i++) {
        let tokenSettings = tokenList.find( //@ts-ignore
            token => token.tokenMint == basketParams.composition[i].token.toBase58()
        );
        if (!tokenSettings)
            throw new Error("Token not supported.");
        if (tokenSettings.id != 0)
            assetPool.push(tokenSettings.id);
        rules.push({
            filterBy: FilterType.Fixed,
            filterDays: FilterTime.Day,
            sortBy: SortBy.DescendingOrder,
            totalWeight: Math.floor((basketParams.composition[i].weight * 1000) / weightSum),
            fixedAsset: tokenSettings.id,
            numAssets: 1,
            weightBy: WeightType.Fixed,
            weightDays: WeightTime.Day,
            weightExpo: 0,
            excludeAssets: [],
            ruleAssets: [],
        })
    }
    
    //@ts-ignore
    let validateBasketParams: CreateBasketParams = {
        ...basketParams,
        name: "",
        symbol: "",
        uri: "",
        manager: PublicKey.default,
        hostPlatform: PublicKey.default,
        hostPlatformFee: 0,
        activelyManaged: 1,
        assetPool: assetPool,
        rules: rules,
        refilterInterval: 7 * 24 * 3600,
        reweightInterval: 24 * 3600
    }
    await validateCreateBasketParams(validateBasketParams, tokenList, basketObj.ownAddress, true);

    let tokens = rules.map(info => info.fixedAsset);
    let weights = rules.map(info => new BN(info.totalWeight));
    while (tokens.length < 15) tokens.push(0);
    while (weights.length < 15) weights.push(new BN(0));

    return await program.methods
        .editBasket(
            basketParams.managerFee,
            new BN(basketParams.rebalanceInterval),
            basketParams.rebalanceThreshold,
            basketParams.rebalanceSlippage,
            (basketParams.lpOffsetThreshold),
            [basketParams.disableRebalance? 1 : 0, basketParams.disableLp? 1 : 0],
            rules.length,
            tokens,
            weights.map(x => x.toNumber()),
        )
        .accounts({
            fundState: basketObj.ownAddress,
            manager: basketObj.data.manager,
            tokenList: TOKEN_LIST_ADDRESS,
            feeDelegate: basketParams.feeDelegate,
        })
        .instruction();
}

export async function buildEditManagetIx(
    program: Program<BasketsIDL>,
    basketState: Basket,
    newManager: PublicKey,
): Promise<TransactionInstruction> {
    return await program.methods
        .editManager(
            newManager
        )
        .accounts({
            manager: basketState.data.manager,
            fundState: basketState.ownAddress,
        })
        .instruction();
}

export async function buildCloseBasketIx(
    program: Program<BasketsIDL>,
    basketState: Basket,
): Promise<TransactionInstruction> {
    return await program.methods.closeBasket()
        .accounts({
            manager: basketState.data.manager,
            fundState: basketState.ownAddress,
            fundToken: basketState.data.fundToken,
            pdaAccount: BASKETS_PROGRAM_PDA,
            systemProgram: SystemProgram.programId,
            tokenProgram: TOKEN_PROGRAM_ID
        })
        .instruction();
}


export async function buildCreateMetadataIx(
    program: Program<BasketsIDL>,
    creator: PublicKey,
    basket: PublicKey,
    tokenMint: PublicKey,
    metadata: {
        symbol: string,
        name: string,
        uri: string
    }
): Promise<TransactionInstruction> {
    let {symbol, name, uri} = metadata;
    if (symbol.length < 3 || symbol.length > 10) throw new Error("Wrong symbol format");
    if (name.length < 3 || name.length > 60) throw new Error("Wrong name format");
    const metaplex = Metaplex.make(program.provider.connection);
    const metadataAccount = metaplex.nfts().pdas().metadata({ mint: tokenMint });
    
    return await program.methods.createFundTokenMintMetadata({
        name: name,
        symbol: symbol,
        uri: uri,
    }).accounts({
        manager: creator,
        fundToken: tokenMint,
        fundState: basket,
        updateAuthority: BASKETS_PROGRAM_PDA,
        metadataAccount: metadataAccount,
        metadataProgram: new PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"),
        systemProgram: SystemProgram.programId,
        rent: SYSVAR_RENT_PUBKEY,
      }).instruction();
}

export async function buildUpdateMetadataIx(
    program: Program<BasketsIDL>,
    creator: PublicKey,
    basket: PublicKey,
    tokenMint: PublicKey,
    metadata: {
        symbol: string,
        name: string,
        uri: string
    }
): Promise<TransactionInstruction> {
    let {symbol, name, uri} = metadata;
    if (symbol.length < 3 || symbol.length > 10) throw new Error("Wrong symbol format");
    if (name.length < 3 || name.length > 60) throw new Error("Wrong name format");
    const metaplex = Metaplex.make(program.provider.connection);
    const metadataAccount = metaplex.nfts().pdas().metadata({ mint: tokenMint });
    
    return await program.methods.updateFundTokenMintMetadata({
        name: name,
        symbol: symbol,
        uri: uri,
    }).accounts({
        manager: creator,
        fundToken: tokenMint,
        fundState: basket,
        updateAuthority: BASKETS_PROGRAM_PDA,
        metadataAccount: metadataAccount,
        metadataProgram: new PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"),
        systemProgram: SystemProgram.programId,
        rent: SYSVAR_RENT_PUBKEY,
      }).instruction();
}

export async function buildSetMetadataIx(
    program: Program<BasketsIDL>,
    basket: Basket,
    metadata: {
        symbol: string,
        name: string,
        uri: string
    }
): Promise<TransactionInstruction> {
    if (basket.data.symbolLength == 0)
        return await buildCreateMetadataIx(
            program, basket.data.manager, basket.ownAddress, basket.data.fundToken, metadata
        ); else
        return await buildUpdateMetadataIx(
            program, basket.data.manager, basket.ownAddress, basket.data.fundToken, metadata
        );
}

export async function buildBuyBasketIx(
    program: Program<BasketsIDL>,
    tokenList: TokenSettings[],
    user: PublicKey,
    basketObj: Basket,
    amount: number,
): Promise<TransactionInstruction> {
    let buyerUsdcTokenAccount = getAssociatedTokenAddressSync(
        new PublicKey(tokenList[0].tokenMint),
        user,
        true,
    );
    let managerUsdcTokenAccount = getAssociatedTokenAddressSync(
        new PublicKey(tokenList[0].tokenMint),
        basketObj.data.feeDelegate.toBase58() == PublicKey.default.toBase58()
            ? basketObj.data.manager : basketObj.data.feeDelegate,
        true,
    );
    let hostUsdcTokenAccount = getAssociatedTokenAddressSync(
        new PublicKey(tokenList[0].tokenMint),
        basketObj.data.hostPubkey,
        true,
    );
    let buyerBasketTokenAccount = getAssociatedTokenAddressSync(
        basketObj.data.fundToken,
        user,
        true,
    );
   
    let seedPubkey = Keypair.generate().publicKey;
    let [buyState] = PublicKey.findProgramAddressSync(
        [Buffer.from("buy"), seedPubkey.toBuffer()],
        BASKETS_PROGRAM_ID
    );

    return await program.methods
        .buyFund(new BN(amount * 10 ** tokenList[0].decimals))
        .accounts({
            buyer: user,
            fundState: basketObj.ownAddress,
            fundToken: basketObj.data.fundToken,
            tokenList: TOKEN_LIST_ADDRESS,
            pdaAccount: BASKETS_PROGRAM_PDA,
            pdaUsdcAccount: new PublicKey(tokenList[0].pdaTokenAccount),
            buyerUsdcAccount: buyerUsdcTokenAccount,
            managerUsdcAccount: managerUsdcTokenAccount,
            smfFeeAccount: BUY_FEE_ACCOUNT,
            hostUsdcAccount: hostUsdcTokenAccount,
            buyerFundTokenAccount: buyerBasketTokenAccount,
            buyState: buyState,
            systemProgram: SystemProgram.programId,
            tokenProgram: TOKEN_PROGRAM_ID,
            rent: SYSVAR_RENT_PUBKEY,
            associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
            seedPubkey: seedPubkey,
        })
        .instruction()
}

export async function buildClaimTokensFromBuyStateIxs(
    program: Program<BasketsIDL>,
    tokenList: TokenSettings[],
    feePayer: PublicKey,
    basketObj: Basket,
    buyStateObj: BuyState,
): Promise<TransactionInstruction[]> {
    let ixs: TransactionInstruction[] = [];
    for (let i = 0; i < buyStateObj.data.token.length; i++) {
        if (parseInt(buyStateObj.data.amountBought[i].toString()) == 0 && i != 0)
            continue;
        let tokenId = buyStateObj.data.token[i].toNumber();
        let userTokenAccount = getAssociatedTokenAddressSync(
            new PublicKey(tokenList[tokenId].tokenMint),
            buyStateObj.data.buyer,
            true,
        );
        ixs.push(
            await program.methods
                .claimTokenFromBuyState(new BN(tokenId))
                .accounts({
                    signer: feePayer,
                    buyer: buyStateObj.data.buyer,
                    fundState: basketObj.ownAddress,
                    buyState: buyStateObj.ownAddress,
                    tokenList: TOKEN_LIST_ADDRESS,
                    buyerTokenAccount: userTokenAccount,
                    pdaTokenAccount: new PublicKey(tokenList[tokenId].pdaTokenAccount),
                    pdaAccount: BASKETS_PROGRAM_PDA,
                    systemProgram: SystemProgram.programId,
                    tokenProgram: TOKEN_PROGRAM_ID,
                })
                .instruction()
        );
    }
    return ixs;
}


export async function buildMintFromBuyStateIx(
    program: Program<BasketsIDL>,
    tokenList: TokenSettings[],
    feePayer: PublicKey,
    basketObj: Basket,
    buyStateObj: BuyState,
): Promise<TransactionInstruction> {
    let accounts = [];
    for (let i = 0; i < basketObj.data.numOfTokens.toNumber(); i++)
        accounts.push({
            pubkey: new PublicKey(
                tokenList[basketObj.data.currentCompToken[i].toNumber()].oracleAccount
            ),
            isSigner: false,
            isWritable: false,
        })
    return await program.methods
        .mintFund()
        .accounts({
            signer: feePayer,
            buyer: buyStateObj.data.buyer,
            fundState: basketObj.ownAddress,
            tokenList: TOKEN_LIST_ADDRESS,
            buyState: buyStateObj.ownAddress,
            oracleSol: new PublicKey(tokenList[1].oracleAccount),
            pdaAccount: BASKETS_PROGRAM_PDA,
            buyerFundTokenAccount: buyStateObj.data.buyerFundTokenAccount,
            fundToken: basketObj.data.fundToken,
            systemProgram: SystemProgram.programId,
            tokenProgram: TOKEN_PROGRAM_ID
        })
        .remainingAccounts(accounts)
        .instruction();
}

export async function buildBuyBasketWithMultipleTokensIx(
    program: Program<BasketsIDL>,
    tokenList: TokenSettings[],
    user: PublicKey,
    basketObj: Basket,
    contribution: {token: PublicKey, amount: number}[],
): Promise<TransactionInstruction> {
    let tokenMints = basketObj.data.currentCompToken.slice(0, basketObj.data.numOfTokens.toNumber())
        .map(token => new PublicKey(tokenList[token.toNumber()].tokenMint));
    tokenMints.push(basketObj.data.fundToken);
    let buyerAtas = tokenMints.map(token =>
        getAssociatedTokenAddressSync(token, user, true)
    );
    for (let i = 0; i < contribution.length; i++) {
        let inBasket = basketObj.data.currentCompToken.find(info =>
            tokenList[info.toNumber()].tokenMint == contribution[i].token.toBase58()
        );
        if (!inBasket)
            throw new Error("Token not present in the current compostion.");
    }
    let amountsBN: BN[] = [];
    let remainingAccounts = [];
    for (let i = 0; i < basketObj.data.numOfTokens.toNumber(); i++) {
        let tokenSettings = tokenList[basketObj.data.currentCompToken[i].toNumber()];
        let tokenContribution = contribution.find(info => info.token.toBase58() == tokenSettings.tokenMint);
        let amount = (tokenContribution) ? tokenContribution.amount : 0;
        amountsBN.push(new BN(Math.floor(amount * 10 ** tokenSettings.decimals)));
        remainingAccounts.push({
            pubkey: new PublicKey(tokenSettings.oracleAccount),
            isSigner: false,
            isWritable: false,
        });
        remainingAccounts.push({
            pubkey: buyerAtas[i],
            isSigner: false,
            isWritable: true,
        });
        remainingAccounts.push({
            pubkey: new PublicKey(tokenSettings.pdaTokenAccount),
            isSigner: false,
            isWritable: true,
        });
    }
    let buyerBasketTokenAccount = buyerAtas[buyerAtas.length - 1];
    while (amountsBN.length < 20) amountsBN.push(new BN(0));

    return await program.methods
        .instantMint(amountsBN)
        .accounts({
            authority: user,
            buyerFundTokenAccount: buyerBasketTokenAccount,
            fundToken: basketObj.data.fundToken,
            fundState: basketObj.ownAddress,
            tokenList: TOKEN_LIST_ADDRESS,
            pdaAccount: BASKETS_PROGRAM_PDA,
            oracleSol: new PublicKey(tokenList[1].oracleAccount),
            tokenProgram: TOKEN_PROGRAM_ID,
        })
        .remainingAccounts(remainingAccounts)
        .instruction();
}

export async function buildBuyBasketWithSingleTokenIx(
    program: Program<BasketsIDL>,
    tokenList: TokenSettings[],
    user: PublicKey,
    basketObj: Basket,
    contributionToken: PublicKey,
    contributionAmount: number,
): Promise<TransactionInstruction> {
    let tokenSettings = tokenList.find(info =>
        info.tokenMint == contributionToken.toBase58()
    );
    if (!tokenSettings || !basketObj.data.currentCompToken.find(x => x.toNumber() == tokenSettings?.id))
        throw new Error("Token not present in the current compostion.");

    let buyerTokenAccount = getAssociatedTokenAddressSync(contributionToken, user, true);
    let buyerBasketTokenAccount = getAssociatedTokenAddressSync(basketObj.data.fundToken, user, true);
    let symmetryFeeAccount = getAssociatedTokenAddressSync(contributionToken, BUY_FEE_WALLET);
    let hostFeeAccount = getAssociatedTokenAddressSync(contributionToken, basketObj.data.hostPubkey, true);
    let managerFeeAccount = getAssociatedTokenAddressSync(contributionToken, basketObj.data.manager, true);

    let remainingAccounts = [];
    for (let i = 0; i < basketObj.data.numOfTokens.toNumber(); i++) {
        let tokenSettings = tokenList[basketObj.data.currentCompToken[i].toNumber()];
        remainingAccounts.push({
            pubkey: new PublicKey(tokenSettings.oracleAccount),
            isSigner: false,
            isWritable: false,
        });
    }

    return await program.methods
        .singleTokenDeposit(tokenSettings?.id, new BN(contributionAmount * 10 ** tokenSettings.decimals))
        .accounts({
            authority: user,
            fundState: basketObj.ownAddress,
            fundToken: basketObj.data.fundToken,
            buyerTokenAccount: buyerTokenAccount,
            buyerFundTokenAccount: buyerBasketTokenAccount,
            pdaAccount: BASKETS_PROGRAM_PDA,
            pdaTokenAccount: new PublicKey(tokenSettings.pdaTokenAccount),
            symmetryFeeAccount: symmetryFeeAccount,
            hostFeeAccount: hostFeeAccount,
            managerFeeAccount: managerFeeAccount,
            oracleAccount: new PublicKey(tokenSettings.oracleAccount),
            oracleSol: new PublicKey(tokenList[1].oracleAccount),
            tokenList: TOKEN_LIST_ADDRESS,
            tokenProgram: TOKEN_PROGRAM_ID,
        })
        .remainingAccounts(remainingAccounts)
        .instruction();
}

export async function buildSellBasketToSingleTokenIx(
    program: Program<BasketsIDL>,
    tokenList: TokenSettings[],
    user: PublicKey,
    basketObj: Basket,
    withdrawToken: PublicKey,
    amount: number,
): Promise<TransactionInstruction> {
    let tokenSettings = tokenList.find(info => info.tokenMint == withdrawToken.toBase58());
    if (!tokenSettings || !basketObj.data.currentCompToken.find(x => x.toNumber() == tokenSettings?.id))
        throw new Error("Token not present in the current compostion.");

    let userBasketTokenAccount = getAssociatedTokenAddressSync(basketObj.data.fundToken, user, true);
    let userTokenAccount = getAssociatedTokenAddressSync(withdrawToken, user, true);
    
    let remainingAccounts = [];
    for (let i = 0; i < basketObj.data.numOfTokens.toNumber(); i++) {
        let tokenSettings = tokenList[basketObj.data.currentCompToken[i].toNumber()];
        remainingAccounts.push({
            pubkey: new PublicKey(tokenSettings.oracleAccount),
            isSigner: false,
            isWritable: false,
        });
    }

    return await program.methods
        .instantBurn(
            new BN(amount * 10 ** 6),
            tokenSettings.id,
        )
        .accounts({
            seller: user,
            pdaAccount: BASKETS_PROGRAM_PDA,
            fundState: basketObj.ownAddress,
            fundToken: basketObj.data.fundToken,
            sellerFundTokenAccount: userBasketTokenAccount,
            withdrawTokenMint: withdrawToken,
            sellerTokenAccount: userTokenAccount,
            pdaTokenAccount: new PublicKey(tokenSettings?.pdaTokenAccount),
            tokenList: TOKEN_LIST_ADDRESS,
            systemProgram: SystemProgram.programId,
            tokenProgram: TOKEN_PROGRAM_ID,
        })
        .remainingAccounts(remainingAccounts)
        .instruction()
}

export async function buildSellBasketIx(
    program: Program<BasketsIDL>,
    user: PublicKey,
    basketObj: Basket,
    amount: number,
    rebalance: number,
): Promise<TransactionInstruction> {
    let userBasketTokenAccount = getAssociatedTokenAddressSync(
        basketObj.data.fundToken,
        user,
        true,
    );
    let seedPubkey = Keypair.generate().publicKey;
    let [sellState] = PublicKey.findProgramAddressSync(
        [Buffer.from("sell"), seedPubkey.toBuffer()],
        BASKETS_PROGRAM_ID
    );
    
    return await program.methods
        .sellFund(
            new BN(amount * 10 ** 6),
            new BN(rebalance),
        )
        .accounts({
            seller: user,
            fundState: basketObj.ownAddress,
            pdaAccount: BASKETS_PROGRAM_PDA,
            newFundState: sellState,
            sellerFundTokenAccount: userBasketTokenAccount,
            fundToken: basketObj.data.fundToken,
            systemProgram: SystemProgram.programId,
            tokenProgram: TOKEN_PROGRAM_ID,
            rent: SYSVAR_RENT_PUBKEY,
            seedPubkey: seedPubkey,
        })
        .instruction()
}

export async function buildClaimTokensFromSellStateIxs(
    program: Program<BasketsIDL>,
    tokenList: TokenSettings[],
    feePayer: PublicKey,
    basketObj: Basket,
): Promise<TransactionInstruction[]> {
    let basketOwner = basketObj.data.manager;
    let ixs: TransactionInstruction[] = [];
    for (let i = 0; i < basketObj.data.numOfTokens.toNumber(); i++) {
        if (parseInt(basketObj.data.currentCompAmount[i].toString()) == 0 && i != 0)
            continue;
        let tokenId = basketObj.data.currentCompToken[i].toNumber();
        let userTokenAccount = getAssociatedTokenAddressSync(
            new PublicKey(tokenList[tokenId].tokenMint),
            basketOwner,
            true,
        );
        ixs.push(
            await program.methods
                .claimToken(new BN(tokenId))
                .accounts({
                    signer: feePayer,
                    manager: basketOwner,
                    fundState: basketObj.ownAddress,
                    tokenList: TOKEN_LIST_ADDRESS,
                    sellerTokenAccount: userTokenAccount,
                    pdaTokenAccount: new PublicKey(tokenList[tokenId].pdaTokenAccount),
                    pdaAccount: BASKETS_PROGRAM_PDA,
                    systemProgram: SystemProgram.programId,
                    tokenProgram: TOKEN_PROGRAM_ID,
                })
                .instruction()
        );
    }
    return ixs;
}


export async function buildUpdateCurrentWeightsIx(
    program: Program<BasketsIDL>,
    basket: Basket,
    tokenList: TokenSettings[],
): Promise<TransactionInstruction> {
    let accounts = [];
    for (let i = 0; i < basket.data.numOfTokens.toNumber(); i++)
        accounts.push({
            pubkey: new PublicKey(
                tokenList[basket.data.currentCompToken[i].toNumber()].oracleAccount
            ),
            isSigner: false,
            isWritable: false,
        })
    return await program.methods
        .updateCurrentWeights()
        .accounts({
            fundState: basket.ownAddress,
            tokenList: TOKEN_LIST_ADDRESS,
            oracleSol: new PublicKey(tokenList[1].oracleAccount),
        })
        .remainingAccounts(accounts)
        .instruction();
}


export async function buildWithdrawBeforeRebalanceIx(
    program: Program<BasketsIDL>,
    feePayer: PublicKey,
    basket: Basket,
    tokenList: TokenSettings[],
    fromToken: number,
    toToken: number,
    fromAmount: number,
): Promise<TransactionInstruction> {
    return await program.methods.withdrawBeforeRebalance(
        fromToken,
        toToken,
        new BN(fromAmount)
    ).accounts({
        signer: feePayer,
        basketState: basket.ownAddress,
        tokenList: TOKEN_LIST_ADDRESS,
        oracleSol: new PublicKey(tokenList[1].oracleAccount),
        oracleTokenFrom: new PublicKey(tokenList[fromToken].oracleAccount),
        oracleTokenTo: new PublicKey(tokenList[toToken].oracleAccount),
        pdaAccount: BASKETS_PROGRAM_PDA,
        pdaTokenFrom: new PublicKey(tokenList[fromToken].pdaTokenAccount),
        pdaTokenTo: new PublicKey(tokenList[toToken].pdaTokenAccount),
        signerTokenFrom: getAssociatedTokenAddressSync(new PublicKey(tokenList[fromToken].tokenMint), feePayer, true),
        signerTokenTo: getAssociatedTokenAddressSync(new PublicKey(tokenList[toToken].tokenMint), feePayer, true),
        fromTokenMint: new PublicKey(tokenList[fromToken].tokenMint),
        toTokenMint: new PublicKey(tokenList[toToken].tokenMint),
        rebalanceState: PublicKey.findProgramAddressSync([Buffer.from("flashrebalance")], BASKETS_PROGRAM_ID)[0],
        tokenProgram: TOKEN_PROGRAM_ID,
        associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
        systemProgram: SystemProgram.programId,
        rent: SYSVAR_RENT_PUBKEY,
        instructionsSysvar: SYSVAR_INSTRUCTIONS_PUBKEY,
    }).instruction();
}

export async function buildDepositAfterRebalanceIx(
    program: Program<BasketsIDL>,
    feePayer: PublicKey,
    basket: Basket,
    tokenList: TokenSettings[],
    toToken: number,
) {
    return await program.methods.depositAfterRebalance().accounts({
        signer: feePayer,
        basketState: basket.ownAddress,
        tokenList: TOKEN_LIST_ADDRESS,
        oracleSol: new PublicKey(tokenList[1].oracleAccount),
        oracleTokenTo: new PublicKey(tokenList[toToken].oracleAccount),
        pdaAccount: BASKETS_PROGRAM_PDA,
        pdaTokenTo: new PublicKey(tokenList[toToken].pdaTokenAccount),
        signerTokenTo: getAssociatedTokenAddressSync(new PublicKey(tokenList[toToken].tokenMint), feePayer, true),
        rebalanceState: PublicKey.findProgramAddressSync([Buffer.from("flashrebalance")], BASKETS_PROGRAM_ID)[0],
        rebalanceFeeAccount: getAssociatedTokenAddressSync(new PublicKey(tokenList[toToken].tokenMint), REBALANCE_FEE_WALLET, true),
        tokenProgram: TOKEN_PROGRAM_ID,
        systemProgram: SystemProgram.programId,
    }).instruction();
}

export async function getSwbFeedsUpdateIxs(
    program: Program,
    payer: PublicKey,
    feeds : PublicKey[],
    numSignatures: number = 2,
): Promise<[
    TransactionInstruction,
    AddressLookupTableAccount[],
    FetchSignaturesMultiResponse
][]> {
    let maxUpdates = Math.floor(18 / numSignatures);
    let ixPromises = [];
    for (let i = 0; i < feeds.length; i += maxUpdates) {
        let promise = PullFeed.fetchUpdateManyIx(program, {
            feeds: feeds.slice(i, i + maxUpdates),
            numSignatures: numSignatures,
            payer: payer,
        });
        ixPromises.push(promise);
    }
    return await Promise.all(ixPromises);   
}

export async function updateOraclesTxs(
    swbProgram: Program,
    payer: PublicKey,
    feeds: PublicKey[],
    lamports: number,
): Promise<TransactionToSend[]> {
    let res = await getSwbFeedsUpdateIxs(
        swbProgram,
        payer,
        feeds,
    ).catch((e) => {
        console.log("Couldn't fetch SWB oracle update txs", e.message);
        return [];
    });
    return res.map(update => {
        return {
            payerKey: payer,
            instructions: [
                update[0],
                ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS}),
                ComputeBudgetProgram.setComputeUnitPrice({microLamports: lamports})
            ],
            lookupTables: update[1]
        }
    })
}
