use crate::accounts::{fetch_all_vault, fetch_market, fetch_tuna_config, TunaConfig, Vault};
use crate::instructions::{OpenAndIncreaseTunaLpPositionOrca, OpenAndIncreaseTunaLpPositionOrcaInstructionArgs};
use crate::types::{AccountsType, RemainingAccountsInfo, RemainingAccountsSlice};
use crate::utils::get_create_ata_instructions;
use crate::utils::orca::get_swap_tick_arrays;
use crate::{
    get_market_address, get_tuna_config_address, get_tuna_liquidity_position_address, OpenAndIncreaseTunaLpPositionArgs,
    OpenAndIncreaseTunaLpPositionInstruction, WP_NFT_UPDATE_AUTH,
};
use anyhow::{anyhow, Result};
use orca_whirlpools_client::{
    fetch_whirlpool, get_oracle_address, get_position_address, get_tick_array_address, DynamicTickArray, InitializeDynamicTickArray,
    InitializeDynamicTickArrayInstructionArgs, Whirlpool,
};
use orca_whirlpools_core::get_tick_array_start_tick_index;
use solana_client::rpc_client::RpcClient;
use solana_instruction::{AccountMeta, Instruction};
use solana_keypair::Keypair;
use solana_pubkey::Pubkey;
use solana_sdk_ids::system_program;
use solana_signer::Signer;
use solana_sysvar::rent::Rent;
use solana_sysvar::slot_hashes::SysvarId;
use spl_associated_token_account::get_associated_token_address_with_program_id;
use spl_associated_token_account::instruction::create_associated_token_account_idempotent;

pub fn open_and_increase_tuna_lp_position_orca_instructions(
    rpc: &RpcClient,
    authority: &Pubkey,
    whirlpool_address: &Pubkey,
    args: OpenAndIncreaseTunaLpPositionArgs,
) -> Result<OpenAndIncreaseTunaLpPositionInstruction> {
    let rent = rpc.get_account(&Rent::id())?;
    let rent: Rent = bincode::deserialize(&rent.data)?;

    let whirlpool = fetch_whirlpool(rpc, whirlpool_address)?;
    let mint_a_address = whirlpool.data.token_mint_a;
    let mint_b_address = whirlpool.data.token_mint_b;

    let tuna_config = fetch_tuna_config(rpc, &get_tuna_config_address().0)?;

    let market_address = get_market_address(whirlpool_address).0;
    let market = fetch_market(&rpc, &market_address)?;

    let vaults = fetch_all_vault(&rpc, &[market.data.vault_a, market.data.vault_b])?;
    let (vault_a, vault_b) = (&vaults[0], &vaults[1]);

    let mint_accounts = rpc.get_multiple_accounts(&[mint_a_address.into(), mint_b_address.into()])?;
    let mint_a_account = mint_accounts[0].as_ref().ok_or(anyhow!("Token A mint account not found"))?;
    let mint_b_account = mint_accounts[1].as_ref().ok_or(anyhow!("Token B mint account not found"))?;

    let authority_ata_a_instructions = get_create_ata_instructions(&mint_a_address, authority, authority, &mint_a_account.owner, 0);
    let authority_ata_b_instructions = get_create_ata_instructions(&mint_b_address, authority, authority, &mint_b_account.owner, 0);

    let mut instructions = vec![];
    let mut non_refundable_rent: u64 = 0;
    let mut additional_signers: Vec<Keypair> = Vec::new();

    additional_signers.push(Keypair::new());
    let position_mint = additional_signers[0].pubkey();

    instructions.extend(authority_ata_a_instructions.create);
    instructions.extend(authority_ata_b_instructions.create);
    instructions.push(create_associated_token_account_idempotent(
        authority,
        &tuna_config.data.fee_recipient,
        &mint_a_address,
        &mint_a_account.owner,
    ));
    instructions.push(create_associated_token_account_idempotent(
        authority,
        &tuna_config.data.fee_recipient,
        &mint_b_address,
        &mint_b_account.owner,
    ));

    let tick_spacing = whirlpool.data.tick_spacing;
    let lower_tick_array_start_index = get_tick_array_start_tick_index(args.tick_lower_index, tick_spacing);
    let upper_tick_array_start_index = get_tick_array_start_tick_index(args.tick_upper_index, tick_spacing);

    let lower_tick_array_address = get_tick_array_address(&whirlpool_address, lower_tick_array_start_index)?.0;
    let upper_tick_array_address = get_tick_array_address(&whirlpool_address, upper_tick_array_start_index)?.0;

    let tick_array_infos = rpc.get_multiple_accounts(&[lower_tick_array_address.into(), upper_tick_array_address.into()])?;

    if tick_array_infos[0].is_none() {
        instructions.push(
            InitializeDynamicTickArray {
                whirlpool: whirlpool.address,
                funder: *authority,
                tick_array: lower_tick_array_address,
                system_program: system_program::id(),
            }
            .instruction(InitializeDynamicTickArrayInstructionArgs {
                start_tick_index: lower_tick_array_start_index,
                idempotent: false,
            }),
        );
        non_refundable_rent += rent.minimum_balance(DynamicTickArray::MIN_LEN);
    }

    if tick_array_infos[1].is_none() && lower_tick_array_start_index != upper_tick_array_start_index {
        instructions.push(
            InitializeDynamicTickArray {
                whirlpool: whirlpool.address,
                funder: *authority,
                tick_array: upper_tick_array_address,
                system_program: system_program::id(),
            }
            .instruction(InitializeDynamicTickArrayInstructionArgs {
                start_tick_index: upper_tick_array_start_index,
                idempotent: false,
            }),
        );
        non_refundable_rent += rent.minimum_balance(DynamicTickArray::MIN_LEN);
    }

    instructions.push(open_and_increase_tuna_lp_position_orca_instruction(
        authority,
        &position_mint,
        &tuna_config.data,
        &vault_a.address,
        &vault_a.data,
        &vault_b.address,
        &vault_b.data,
        &whirlpool.address,
        &whirlpool.data,
        &mint_a_account.owner,
        &mint_b_account.owner,
        args,
    ));
    instructions.extend(authority_ata_a_instructions.cleanup);
    instructions.extend(authority_ata_b_instructions.cleanup);

    Ok(OpenAndIncreaseTunaLpPositionInstruction {
        position_mint,
        instructions,
        additional_signers,
        initialization_cost: non_refundable_rent,
    })
}

pub fn open_and_increase_tuna_lp_position_orca_instruction(
    authority: &Pubkey,
    position_mint: &Pubkey,
    tuna_config: &TunaConfig,
    vault_a_address: &Pubkey,
    vault_a: &Vault,
    vault_b_address: &Pubkey,
    vault_b: &Vault,
    whirlpool_address: &Pubkey,
    whirlpool: &Whirlpool,
    token_program_a: &Pubkey,
    token_program_b: &Pubkey,
    args: OpenAndIncreaseTunaLpPositionArgs,
) -> Instruction {
    let mint_a = whirlpool.token_mint_a;
    let mint_b = whirlpool.token_mint_b;
    let tick_spacing = whirlpool.tick_spacing;

    assert_eq!(vault_a.mint, mint_a);
    assert_eq!(vault_b.mint, mint_b);

    let tuna_config_address = get_tuna_config_address().0;
    let market_address = get_market_address(&whirlpool_address).0;
    let tuna_position_address = get_tuna_liquidity_position_address(&position_mint).0;
    let tuna_position_owner_ata_a = get_associated_token_address_with_program_id(&authority, &mint_a, token_program_a);
    let tuna_position_owner_ata_b = get_associated_token_address_with_program_id(&authority, &mint_b, token_program_b);
    let orca_position_address = get_position_address(&position_mint).unwrap().0;
    let oracle_address = get_oracle_address(&whirlpool_address).unwrap().0;

    let tick_array_lower_start_tick_index = get_tick_array_start_tick_index(args.tick_lower_index, tick_spacing);
    let tick_array_lower_address = get_tick_array_address(&whirlpool_address, tick_array_lower_start_tick_index).unwrap().0;

    let tick_array_upper_start_tick_index = get_tick_array_start_tick_index(args.tick_upper_index, tick_spacing);
    let tick_array_upper_address = get_tick_array_address(&whirlpool_address, tick_array_upper_start_tick_index).unwrap().0;

    let swap_ticks_arrays = get_swap_tick_arrays(whirlpool.tick_current_index, tick_spacing, &whirlpool_address);

    let ix_builder = OpenAndIncreaseTunaLpPositionOrca {
        authority: *authority,
        tuna_config: tuna_config_address,
        mint_a,
        mint_b,
        market: market_address,
        vault_a: *vault_a_address,
        vault_b: *vault_b_address,
        vault_a_ata: get_associated_token_address_with_program_id(vault_a_address, &mint_a, token_program_a),
        vault_b_ata: get_associated_token_address_with_program_id(vault_b_address, &mint_b, token_program_b),
        tuna_position: tuna_position_address,
        tuna_position_mint: *position_mint,
        tuna_position_ata: get_associated_token_address_with_program_id(&tuna_position_address, &position_mint, &spl_token_2022::ID),
        tuna_position_ata_a: get_associated_token_address_with_program_id(&tuna_position_address, &mint_a, token_program_a),
        tuna_position_ata_b: get_associated_token_address_with_program_id(&tuna_position_address, &mint_b, token_program_b),
        tuna_position_owner_ata_a,
        tuna_position_owner_ata_b,
        fee_recipient_ata_a: get_associated_token_address_with_program_id(&tuna_config.fee_recipient, &mint_a, token_program_a),
        fee_recipient_ata_b: get_associated_token_address_with_program_id(&tuna_config.fee_recipient, &mint_b, token_program_b),
        oracle_price_update_a: vault_a.oracle_price_update,
        oracle_price_update_b: vault_b.oracle_price_update,
        whirlpool_program: orca_whirlpools_client::ID,
        whirlpool: *whirlpool_address,
        orca_position: orca_position_address,
        token_program_a: *token_program_a,
        token_program_b: *token_program_b,
        metadata_update_auth: WP_NFT_UPDATE_AUTH,
        memo_program: spl_memo::ID,
        token2022_program: spl_token_2022::ID,
        system_program: system_program::ID,
        associated_token_program: spl_associated_token_account::ID,
    };

    ix_builder.instruction_with_remaining_accounts(
        OpenAndIncreaseTunaLpPositionOrcaInstructionArgs {
            tick_lower_index: args.tick_lower_index,
            tick_upper_index: args.tick_upper_index,
            lower_limit_order_sqrt_price: args.lower_limit_order_sqrt_price,
            upper_limit_order_sqrt_price: args.upper_limit_order_sqrt_price,
            flags: args.flags,
            collateral_a: args.collateral_a,
            collateral_b: args.collateral_b,
            borrow_a: args.borrow_a,
            borrow_b: args.borrow_b,
            min_added_amount_a: args.min_added_amount_a,
            min_added_amount_b: args.min_added_amount_b,
            unused: 0,
            remaining_accounts_info: RemainingAccountsInfo {
                slices: vec![
                    RemainingAccountsSlice {
                        accounts_type: AccountsType::SwapTickArrays,
                        length: 5,
                    },
                    RemainingAccountsSlice {
                        accounts_type: AccountsType::TickArrayLower,
                        length: 1,
                    },
                    RemainingAccountsSlice {
                        accounts_type: AccountsType::TickArrayUpper,
                        length: 1,
                    },
                    RemainingAccountsSlice {
                        accounts_type: AccountsType::PoolVaultTokenA,
                        length: 1,
                    },
                    RemainingAccountsSlice {
                        accounts_type: AccountsType::PoolVaultTokenB,
                        length: 1,
                    },
                    RemainingAccountsSlice {
                        accounts_type: AccountsType::WhirlpoolOracle,
                        length: 1,
                    },
                ],
            },
        },
        &[
            AccountMeta::new(swap_ticks_arrays[0], false),
            AccountMeta::new(swap_ticks_arrays[1], false),
            AccountMeta::new(swap_ticks_arrays[2], false),
            AccountMeta::new(swap_ticks_arrays[3], false),
            AccountMeta::new(swap_ticks_arrays[4], false),
            AccountMeta::new(tick_array_lower_address, false),
            AccountMeta::new(tick_array_upper_address, false),
            AccountMeta::new(whirlpool.token_vault_a, false),
            AccountMeta::new(whirlpool.token_vault_b, false),
            AccountMeta::new(oracle_address, false),
        ],
    )
}
