use anchor_lang::{InstructionData, ToAccountMetas};
use solana_program::{
    instruction::{AccountMeta, Instruction},
    pubkey::Pubkey,
};

use crate::constants::KLEND_PROGRAM_ID;

/// A Kamino reserve paired with the oracle accounts required to refresh it.
///
/// Each reserve carries its own oracle config; populate only the oracles
/// the reserve is configured to use and leave the rest as `None`.
#[derive(Clone, Debug)]
pub struct KaminoReserveRefresh {
    pub reserve: Pubkey,
    pub pyth_oracle: Option<Pubkey>,
    pub switchboard_price_oracle: Option<Pubkey>,
    pub switchboard_twap_oracle: Option<Pubkey>,
    pub scope_prices: Option<Pubkey>,
}

pub struct RefreshKaminoParams {
    pub obligation: Pubkey,
    pub lending_market: Pubkey,
    /// Reserves for each deposited asset, in obligation deposit order.
    pub deposit_reserves: Vec<KaminoReserveRefresh>,
    /// Reserves for each borrowed asset, in obligation borrow order.
    pub borrow_reserves: Vec<KaminoReserveRefresh>,
}

/// Builds the full sequence of instructions required to refresh a Kamino
/// obligation: one `refresh_reserve` per unique reserve followed by a single
/// `refresh_obligation`.
///
/// Reserves that appear in both `deposit_reserves` and `borrow_reserves` are
/// only refreshed once. The obligation refresh receives deposit reserves
/// first, then borrow reserves, then one
/// [`get_kamino_referrer_token_state`](pyra_accounts::get_kamino_referrer_token_state)
/// PDA per borrow reserve — Pyra obligations always carry a referrer so klend
/// requires the trailing `borrow_count` referrer accounts.
pub fn refresh_kamino(params: &RefreshKaminoParams) -> Vec<Instruction> {
    let total = params
        .deposit_reserves
        .len()
        .saturating_add(params.borrow_reserves.len())
        .saturating_add(1); // obligation
    let mut instructions = Vec::with_capacity(total);

    let mut seen: Vec<Pubkey> = Vec::with_capacity(total);
    for reserve in params
        .deposit_reserves
        .iter()
        .chain(params.borrow_reserves.iter())
    {
        if seen.contains(&reserve.reserve) {
            continue;
        }
        seen.push(reserve.reserve);
        instructions.push(refresh_reserve_ix(params.lending_market, reserve));
    }

    instructions.push(refresh_obligation_ix(params));
    instructions
}

fn refresh_reserve_ix(lending_market: Pubkey, reserve: &KaminoReserveRefresh) -> Instruction {
    let accounts = crate::klend_program::client::accounts::RefreshReserve {
        reserve: reserve.reserve,
        lending_market,
        pyth_oracle: reserve.pyth_oracle,
        switchboard_price_oracle: reserve.switchboard_price_oracle,
        switchboard_twap_oracle: reserve.switchboard_twap_oracle,
        scope_prices: reserve.scope_prices,
    }
    .to_account_metas(None);

    let data = crate::klend_program::client::args::RefreshReserve {}.data();

    Instruction {
        program_id: KLEND_PROGRAM_ID,
        accounts,
        data,
    }
}

fn refresh_obligation_ix(params: &RefreshKaminoParams) -> Instruction {
    let mut accounts = crate::klend_program::client::accounts::RefreshObligation {
        lending_market: params.lending_market,
        obligation: params.obligation,
    }
    .to_account_metas(None);

    let remaining = params
        .deposit_reserves
        .len()
        .saturating_add(params.borrow_reserves.len())
        // One referrer_token_state per borrow reserve. Pyra obligations always
        // carry a referrer so klend's handler expects this trailing slice
        // whenever borrows exist.
        .saturating_add(params.borrow_reserves.len());
    accounts.reserve(remaining);
    for reserve in params
        .deposit_reserves
        .iter()
        .chain(params.borrow_reserves.iter())
    {
        accounts.push(AccountMeta::new(reserve.reserve, false));
    }
    for borrow in &params.borrow_reserves {
        let referrer_token_state = pyra_accounts::get_kamino_referrer_token_state(&borrow.reserve);
        accounts.push(AccountMeta::new(referrer_token_state, false));
    }

    let data = crate::klend_program::client::args::RefreshObligation {}.data();

    Instruction {
        program_id: KLEND_PROGRAM_ID,
        accounts,
        data,
    }
}

#[cfg(test)]
#[allow(
    clippy::allow_attributes,
    clippy::allow_attributes_without_reason,
    clippy::unwrap_used
)]
mod tests {
    use super::*;
    use solana_pubkey::pubkey;

    fn reserve(pk: Pubkey) -> KaminoReserveRefresh {
        KaminoReserveRefresh {
            reserve: pk,
            pyth_oracle: None,
            switchboard_price_oracle: None,
            switchboard_twap_oracle: None,
            scope_prices: None,
        }
    }

    #[test]
    fn builds_one_refresh_per_reserve_plus_obligation() {
        let obligation = pubkey!("d4A2prbA2whesmvHaL88BH6Ewn5N4bTSjm4GiKy2eSi");
        let lending_market = pubkey!("7u3HeHxYDLhnCoErrtycNokbQYbWGzLs6JSDqGAv5PfF");
        let deposit_a = pubkey!("Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD");
        let deposit_b = pubkey!("AdtRGGhmqvom3Fk5H2LTnGMfcXmUQdhSz8aPJGfhFqHj");
        let borrow_a = pubkey!("DUcTi3rDyS5QEmZ4BNRBejtArmDCWaPYGfN44vBJXKL5");

        let ixs = refresh_kamino(&RefreshKaminoParams {
            obligation,
            lending_market,
            deposit_reserves: vec![reserve(deposit_a), reserve(deposit_b)],
            borrow_reserves: vec![reserve(borrow_a)],
        });

        // 3 refresh_reserve + 1 refresh_obligation
        assert_eq!(ixs.len(), 4);
        for ix in &ixs {
            assert_eq!(ix.program_id, KLEND_PROGRAM_ID);
        }

        // Final ix is refresh_obligation: lending_market, obligation, then
        // deposit reserves, borrow reserves, then one referrer_token_state
        // per borrow reserve.
        let obligation_ix = &ixs[3];
        assert_eq!(obligation_ix.accounts.len(), 2 + 3 + 1);
        assert_eq!(obligation_ix.accounts[0].pubkey, lending_market);
        assert!(!obligation_ix.accounts[0].is_writable);
        assert_eq!(obligation_ix.accounts[1].pubkey, obligation);
        assert!(obligation_ix.accounts[1].is_writable);
        assert_eq!(obligation_ix.accounts[2].pubkey, deposit_a);
        assert!(obligation_ix.accounts[2].is_writable);
        assert_eq!(obligation_ix.accounts[3].pubkey, deposit_b);
        assert_eq!(obligation_ix.accounts[4].pubkey, borrow_a);
        assert_eq!(
            obligation_ix.accounts[5].pubkey,
            pyra_accounts::get_kamino_referrer_token_state(&borrow_a),
        );
        assert!(obligation_ix.accounts[5].is_writable);
    }

    #[test]
    fn refresh_reserve_carries_oracles() {
        let lending_market = pubkey!("7u3HeHxYDLhnCoErrtycNokbQYbWGzLs6JSDqGAv5PfF");
        let reserve_pk = pubkey!("Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD");
        let pyth = pubkey!("AdtRGGhmqvom3Fk5H2LTnGMfcXmUQdhSz8aPJGfhFqHj");
        let scope = pubkey!("3NJYftD5sjVfxFkKFgU7bbtBBhBsBYAg1CEhJkJ4iY3H");

        let ixs = refresh_kamino(&RefreshKaminoParams {
            obligation: pubkey!("d4A2prbA2whesmvHaL88BH6Ewn5N4bTSjm4GiKy2eSi"),
            lending_market,
            deposit_reserves: vec![KaminoReserveRefresh {
                reserve: reserve_pk,
                pyth_oracle: Some(pyth),
                switchboard_price_oracle: None,
                switchboard_twap_oracle: None,
                scope_prices: Some(scope),
            }],
            borrow_reserves: vec![],
        });

        assert_eq!(ixs.len(), 2);
        let refresh_ix = &ixs[0];
        // reserve writable, lending_market read-only
        assert_eq!(refresh_ix.accounts[0].pubkey, reserve_pk);
        assert!(refresh_ix.accounts[0].is_writable);
        assert_eq!(refresh_ix.accounts[1].pubkey, lending_market);
        assert!(!refresh_ix.accounts[1].is_writable);
        // Oracle pubkeys appear; absent ones replaced by klend program id sentinel
        let pks: Vec<Pubkey> = refresh_ix.accounts.iter().map(|m| m.pubkey).collect();
        assert!(pks.contains(&pyth));
        assert!(pks.contains(&scope));
    }

    #[test]
    fn deduplicates_reserves_across_deposits_and_borrows() {
        let obligation = pubkey!("d4A2prbA2whesmvHaL88BH6Ewn5N4bTSjm4GiKy2eSi");
        let lending_market = pubkey!("7u3HeHxYDLhnCoErrtycNokbQYbWGzLs6JSDqGAv5PfF");
        let shared = pubkey!("Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD");
        let borrow_only = pubkey!("AdtRGGhmqvom3Fk5H2LTnGMfcXmUQdhSz8aPJGfhFqHj");

        let ixs = refresh_kamino(&RefreshKaminoParams {
            obligation,
            lending_market,
            deposit_reserves: vec![reserve(shared)],
            borrow_reserves: vec![reserve(shared), reserve(borrow_only)],
        });

        // shared refreshed once, borrow_only refreshed once, + obligation
        assert_eq!(ixs.len(), 3);

        // Obligation refresh contains: market + obligation + 1 deposit reserve +
        // 2 borrow reserves + 2 referrer_token_state PDAs (one per borrow,
        // including the shared reserve since it is borrowed in this obligation).
        let obligation_ix = &ixs[2];
        assert_eq!(obligation_ix.accounts.len(), 2 + 3 + 2);
        assert_eq!(obligation_ix.accounts[2].pubkey, shared);
        assert_eq!(obligation_ix.accounts[3].pubkey, shared);
        assert_eq!(obligation_ix.accounts[4].pubkey, borrow_only);
        assert_eq!(
            obligation_ix.accounts[5].pubkey,
            pyra_accounts::get_kamino_referrer_token_state(&shared),
        );
        assert_eq!(
            obligation_ix.accounts[6].pubkey,
            pyra_accounts::get_kamino_referrer_token_state(&borrow_only),
        );
    }

    #[test]
    fn deposit_only_obligation_omits_referrer_token_states() {
        let obligation = pubkey!("d4A2prbA2whesmvHaL88BH6Ewn5N4bTSjm4GiKy2eSi");
        let lending_market = pubkey!("7u3HeHxYDLhnCoErrtycNokbQYbWGzLs6JSDqGAv5PfF");
        let deposit_a = pubkey!("Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD");

        let ixs = refresh_kamino(&RefreshKaminoParams {
            obligation,
            lending_market,
            deposit_reserves: vec![reserve(deposit_a)],
            borrow_reserves: vec![],
        });

        // 1 refresh_reserve + 1 refresh_obligation
        assert_eq!(ixs.len(), 2);

        // Obligation refresh: lending_market + obligation + 1 deposit, no
        // referrer accounts because there are no borrows.
        let obligation_ix = &ixs[1];
        assert_eq!(obligation_ix.accounts.len(), 2 + 1);
        assert_eq!(obligation_ix.accounts[2].pubkey, deposit_a);
    }

    #[test]
    fn multiple_borrows_emit_one_referrer_each_in_order() {
        let obligation = pubkey!("d4A2prbA2whesmvHaL88BH6Ewn5N4bTSjm4GiKy2eSi");
        let lending_market = pubkey!("7u3HeHxYDLhnCoErrtycNokbQYbWGzLs6JSDqGAv5PfF");
        let borrow_a = pubkey!("DUcTi3rDyS5QEmZ4BNRBejtArmDCWaPYGfN44vBJXKL5");
        let borrow_b = pubkey!("AdtRGGhmqvom3Fk5H2LTnGMfcXmUQdhSz8aPJGfhFqHj");

        let ixs = refresh_kamino(&RefreshKaminoParams {
            obligation,
            lending_market,
            deposit_reserves: vec![],
            borrow_reserves: vec![reserve(borrow_a), reserve(borrow_b)],
        });

        let obligation_ix = ixs.last().unwrap();
        // market + obligation + 2 borrows + 2 referrer_token_states
        assert_eq!(obligation_ix.accounts.len(), 2 + 2 + 2);
        // Trailing referrer slots in same order as borrow reserves.
        assert_eq!(
            obligation_ix.accounts[4].pubkey,
            pyra_accounts::get_kamino_referrer_token_state(&borrow_a),
        );
        assert_eq!(
            obligation_ix.accounts[5].pubkey,
            pyra_accounts::get_kamino_referrer_token_state(&borrow_b),
        );
        // Referrer accounts are writable (klend writes cumulative_rewards).
        assert!(obligation_ix.accounts[4].is_writable);
        assert!(obligation_ix.accounts[5].is_writable);
    }

    #[test]
    fn empty_reserves_produce_just_obligation_refresh() {
        let ixs = refresh_kamino(&RefreshKaminoParams {
            obligation: pubkey!("d4A2prbA2whesmvHaL88BH6Ewn5N4bTSjm4GiKy2eSi"),
            lending_market: pubkey!("7u3HeHxYDLhnCoErrtycNokbQYbWGzLs6JSDqGAv5PfF"),
            deposit_reserves: vec![],
            borrow_reserves: vec![],
        });

        assert_eq!(ixs.len(), 1);
        assert_eq!(ixs[0].accounts.len(), 2);
    }
}
