import { Connection, Keypair, PublicKey, Transaction, TransactionInstruction } from "@solana/web3.js";
import { BASKETS_LOOKUP_TABLE_1, BASKETS_LOOKUP_TABLE_2, BASKETS_PROGRAM_ID, SimpleCreateParams, SimpleEditParams, SWB_PID, TransactionToSend } from "./config";
import { AnchorProvider, Program, Wallet } from "@coral-xyz/anchor";
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
import { BasketsIDL, IDL } from "./basketsIDL";
import { fetchTokenList, getOraclePrices } from "./utils";
import { Basket } from "./basketState";
import { BuyState } from "./buyState";
import { buildBuyBasketIx, buildBuyBasketWithMultipleTokensIx, buildBuyBasketWithSingleTokenIx, buildClaimTokensFromBuyStateIxs, buildClaimTokensFromSellStateIxs, buildCloseBasketIx, buildCreateBasketIx, buildEditBasketIx, buildEditManagetIx, buildMintFromBuyStateIx, buildSellBasketIx, buildSellBasketToSingleTokenIx, buildSetMetadataIx, updateOraclesTxs } from "./instructionsBuilder";
import { buildRebalanceTransactions, getConfirmedTimestamp, getLookupTableAccount, getSortedRebalanceInfo, isForceRebalanceNeeded, signAndSendTransactions } from "./flashRebalance";

async function loadSwbProgram(connection: Connection) {
    let provider = new AnchorProvider(
        connection,
        new NodeWallet(Keypair.generate()),
        {
            skipPreflight: true,
            preflightCommitment: "recent",
            commitment: "processed",
        }
    );
    let swbIDL = (await Program.fetchIdl(SWB_PID, provider))!;
    return new Program(swbIDL, provider);
}

function loadProgram(connection: Connection): Program<BasketsIDL> {
    let provider = new AnchorProvider(
        connection,
        new NodeWallet(Keypair.generate()),
        {
            skipPreflight: true,
            preflightCommitment: "recent",
            commitment: "processed",
        }
    );
    let program = new Program<BasketsIDL>(IDL, provider);
    return program;
}

export async function createBasketIx(
    connection: Connection,
    basketParams: SimpleCreateParams,
): Promise<TransactionInstruction> {
    let program = loadProgram(connection);
    let tokenList = await fetchTokenList(program);
    return await buildCreateBasketIx(program, tokenList, basketParams);
}

export async function editBasketIx(
    connection: Connection,
    basket: PublicKey,
    basketParams: SimpleEditParams,
): Promise<TransactionInstruction> {
    let program = loadProgram(connection);
    let tokenList = await fetchTokenList(program);
    let basketObj = await Basket.loadFromPubkey(program, basket);
    return await buildEditBasketIx(program, tokenList, basketObj, basketParams);
}

export async function editManagerIx(
    connection: Connection,
    basket: PublicKey,
    newManager: PublicKey
): Promise<TransactionInstruction> {
    let program = loadProgram(connection);
    let basketObj = await Basket.loadFromPubkey(program, basket); 
    return await buildEditManagetIx(program, basketObj, newManager);
}

export async function closeBasketIx(
    connection: Connection,
    basket: PublicKey
): Promise<TransactionInstruction> {
    let program = loadProgram(connection);
    let basketState = await Basket.loadFromPubkey(program, basket);
    return await buildCloseBasketIx(program, basketState);
}

export async function setMetadataIx(
    connection: Connection,
    basket: PublicKey,
    symbol: string = "",
    name: string = "",
    uri: string = "",
): Promise<TransactionInstruction> {
    let program = loadProgram(connection);
    let basketObj = await Basket.loadFromPubkey(program, basket);
    return await buildSetMetadataIx(program, basketObj, {symbol: symbol, name: name, uri: uri});
}

export async function buyBasketIx(
    connection: Connection,
    user: PublicKey,
    basket: PublicKey,
    amount: number,
): Promise<TransactionInstruction> {
    let program = loadProgram(connection);
    let tokenList = await fetchTokenList(program);
    let basketObj = await Basket.loadFromPubkey(program, basket);
    return await buildBuyBasketIx(program, tokenList, user, basketObj, amount);
}

export async function claimTokensFromBuyStateIxs(
    connection: Connection,
    user: PublicKey,
    buyState: PublicKey,
): Promise<TransactionInstruction[]> {
    let program = loadProgram(connection);
    let tokenList = await fetchTokenList(program);
    let buyStateObj = await BuyState.loadFromPubkey(program, buyState);
    let basketObj = buyStateObj.basket;
    return await buildClaimTokensFromBuyStateIxs(program, tokenList, user, basketObj, buyStateObj);
}

export async function mintFromBuyStateIx(
    connection: Connection,
    user: PublicKey,
    buyState: PublicKey
): Promise<TransactionInstruction> {
    let program = loadProgram(connection);
    let tokenList = await fetchTokenList(program);
    let buyStateObj = await BuyState.loadFromPubkey(program, buyState);
    let basketObj = buyStateObj.basket;
    return await buildMintFromBuyStateIx(program, tokenList, user, basketObj, buyStateObj);
}

export async function buyBasketWithMultipleTokensIx(
    connection: Connection,
    user: PublicKey,
    basket: PublicKey,
    contribution: {token: PublicKey, amount: number}[],
): Promise<TransactionInstruction> {
    let program = loadProgram(connection);
    let tokenList = await fetchTokenList(program);
    let basketObj = await Basket.loadFromPubkey(program, basket);
    return await buildBuyBasketWithMultipleTokensIx(program, tokenList, user, basketObj, contribution);   
}

export async function buyBasketWithSingleTokenIx(
    connection: Connection,
    user: PublicKey,
    basket: PublicKey,
    contributionToken: PublicKey,
    contributionAmount: number
): Promise<TransactionInstruction> {
    let program = loadProgram(connection);
    let tokenList = await fetchTokenList(program);
    let basketObj = await Basket.loadFromPubkey(program, basket);
    return await buildBuyBasketWithSingleTokenIx(program, tokenList, user, basketObj, contributionToken, contributionAmount);
}

export async function sellBasketToSingleTokenIx(
    connection: Connection,
    user: PublicKey,
    basket: PublicKey,
    withdrawToken: PublicKey,
    amount: number,
): Promise<TransactionInstruction> {
    let program = loadProgram(connection);
    let tokenList = await fetchTokenList(program);
    let basketObj = await Basket.loadFromPubkey(program, basket);
    return await buildSellBasketToSingleTokenIx(program, tokenList, user, basketObj, withdrawToken, amount);   
}

export async function sellBasketIx(
    connection: Connection,
    user: PublicKey,
    basket: PublicKey,
    amount: number,
    rebalance: number,
): Promise<TransactionInstruction> {
    let program = loadProgram(connection);
    let basketObj = await Basket.loadFromPubkey(program, basket);
    return await buildSellBasketIx(program, user, basketObj, amount, rebalance);   
}

export async function claimTokensFromSellStateIxs(
    connection: Connection,
    user: PublicKey,
    basket: PublicKey,
): Promise<TransactionInstruction[]> {
    let program = loadProgram(connection);
    let tokenList = await fetchTokenList(program);
    let basketObj = await Basket.loadFromPubkey(program, basket);
    return await buildClaimTokensFromSellStateIxs(program, tokenList, user, basketObj);
}

export async function cronRebalanceBasketIxs(
    connection: Connection,
    user: PublicKey,
    basketAddress: PublicKey,
    jupAPIkey: string = "https://quote-api.jup.ag/v6/",
    lamports: number = 50000,
    updateOracles: boolean = true,
    maxAllowedAccounts: number = 45,
    softCap: number = 5,
    hardCap: number = 5000,
    underTokens: number = 3,
    overTokens: number = 3,
): Promise<TransactionToSend[]> {
    let program = loadProgram(connection);
    let tokenList = await fetchTokenList(program);
    let basket = await Basket.loadFromPubkey(program, basketAddress);
    let swbProgram = await loadSwbProgram(connection);
    let lookups = await Promise.all([
        getLookupTableAccount(program.provider.connection, BASKETS_LOOKUP_TABLE_1),
        getLookupTableAccount(program.provider.connection, BASKETS_LOOKUP_TABLE_2)
    ]);
    const [oraclePriceData, timestamp, updateOraclesTxData] = await Promise.all([
        getOraclePrices(program, tokenList),
        getConfirmedTimestamp(connection, basket),
        updateOracles ? updateOraclesTxs(
            swbProgram,
            user,
            basket.getSwbFeeds(tokenList),
            50000,
        ) : []
    ]);
    const forceRebalance = isForceRebalanceNeeded(basket, user);
    const rebalanceInfos = getSortedRebalanceInfo(basket, oraclePriceData, timestamp, tokenList);

    const txsToSend = await buildRebalanceTransactions(
        basket,
        rebalanceInfos,
        oraclePriceData,
        forceRebalance,
        lookups,
        maxAllowedAccounts,
        user,
        connection,
        program,
        tokenList,
        lamports,
        updateOraclesTxData,
        softCap,
        hardCap,
        underTokens,
        overTokens,
        jupAPIkey,
    );
    
    return txsToSend;
}
