use solana_sdk::{
    instruction::{AccountMeta, Instruction},
    pubkey::Pubkey,
    sysvar,
};
use spl_associated_token_account::get_associated_token_address_with_program_id;

use crate::constants::Discriminator;
use crate::helpers::derive::{get_fee_config_address, get_spread_config_address};
use crate::types::{PoolAddresses, SwapParams};

/// Build a SwapExactIn instruction (discriminator 3).
///
/// Base accounts (16): tokenProgramX, tokenProgramY, config, midpriceOracle,
/// curveMeta, curvePrefabs(w), config(authority), user, userSource(w),
/// vaultSource(w), vaultDest(w), userDest(w), feeConfig, feeRecipientAta(w),
/// clock, curveUpdates(w).
///
/// If spread config is initialized: +spreadConfig, +instructionsSysvar.
pub fn build_swap_exact_in(
    user: &Pubkey,
    pool_addresses: &PoolAddresses,
    mint_x: &Pubkey,
    mint_y: &Pubkey,
    token_program_x: &Pubkey,
    token_program_y: &Pubkey,
    params: &SwapParams,
    program_id: &Pubkey,
    spread_config_initialized: bool,
    fee_config_override: Option<Pubkey>,
) -> Instruction {
    let user_x = get_associated_token_address_with_program_id(user, mint_x, token_program_x);
    let user_y = get_associated_token_address_with_program_id(user, mint_y, token_program_y);

    let (user_source, vault_source, vault_dest, user_dest) = if params.is_x {
        (user_x, pool_addresses.vault_x, pool_addresses.vault_y, user_y)
    } else {
        (user_y, pool_addresses.vault_y, pool_addresses.vault_x, user_x)
    };

    let fee_config_pda = fee_config_override.unwrap_or_else(|| get_fee_config_address(program_id).0);
    let input_mint = if params.is_x { mint_x } else { mint_y };
    let input_mint_program = if params.is_x {
        token_program_x
    } else {
        token_program_y
    };
    let fee_recipient_ata = get_associated_token_address_with_program_id(
        &params.fee_recipient,
        input_mint,
        input_mint_program,
    );

    let expiration = params.expiration.unwrap_or_else(default_expiration);

    let mut data = Vec::with_capacity(26);
    data.push(Discriminator::SwapExactIn as u8);
    data.push(if params.is_x { 1 } else { 0 });
    data.extend_from_slice(&params.amount_in.to_le_bytes());
    data.extend_from_slice(&params.min_out.to_le_bytes());
    data.extend_from_slice(&expiration.to_le_bytes());

    let mut keys = vec![
        AccountMeta::new_readonly(*token_program_x, false),
        AccountMeta::new_readonly(*token_program_y, false),
        AccountMeta::new_readonly(pool_addresses.config, false),
        AccountMeta::new_readonly(pool_addresses.midprice_oracle, false),
        AccountMeta::new_readonly(pool_addresses.curve_meta, false),
        AccountMeta::new(pool_addresses.curve_prefabs, false),
        AccountMeta::new_readonly(pool_addresses.config, false), // authority = pool address PDA
        AccountMeta::new_readonly(*user, true),
        AccountMeta::new(user_source, false),
        AccountMeta::new(vault_source, false),
        AccountMeta::new(vault_dest, false),
        AccountMeta::new(user_dest, false),
        AccountMeta::new_readonly(fee_config_pda, false),
        AccountMeta::new(fee_recipient_ata, false),
        AccountMeta::new_readonly(sysvar::clock::id(), false),
        AccountMeta::new(pool_addresses.curve_updates, false),
    ];

    if spread_config_initialized {
        let (spread_config_pda, _) =
            get_spread_config_address(&pool_addresses.config, program_id);
        keys.push(AccountMeta::new_readonly(spread_config_pda, false));
        keys.push(AccountMeta::new_readonly(sysvar::instructions::id(), false));
    }

    Instruction {
        program_id: *program_id,
        accounts: keys,
        data,
    }
}

fn default_expiration() -> i64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs() as i64
        + 3600
}
