Documentation
pub mod msgs {

    use cosmwasm_schema::{cw_serde, QueryResponses};

    use cosmwasm_std::{Addr, Decimal, Uint128};
    use cw20::Cw20ReceiveMsg;

    use crate::common::AssetType;

    use super::definitions::{PoolConfig, PoolData};

    #[cw_serde]
    pub struct InstantiateMsg {
        pub config: PoolConfig,
        pub lp_type: AssetType,
        pub code_id_cw20: Option<u64>,
    }

    #[cw_serde]
    pub enum ExecuteMsg {
        Deposit(DepositMsg),
        Receive(Cw20ReceiveMsg),
        Borrow(BorrowMsg),
        Repay(RepayMsg),
        Withdraw(WithdrawMsg),
        CloseExpiredLoan(CloseExpiredLoanMsg),
        Swap(SwapMsg),
        UpdateConfig(Box<UpdateConfigMsg>),
    }

    #[cw_serde]
    #[derive(QueryResponses)]
    pub enum QueryMsg {
        #[returns(PoolData)]
        PoolData {},
        #[returns(PoolConfig)]
        PoolConfig {},
        #[returns(LoanInfoResponse)]
        LoanInfo { id: u64 },
        #[returns(CurrentInterestsResponse)]
        CurrentInterests,
    }

    #[cw_serde]
    pub struct MigrateMsg {}

    #[cw_serde]
    pub enum Cw20HookMsg {
        Deposit(DepositMsg),
        Borrow(BorrowMsg),
        Withdraw(WithdrawMsg),
    }

    #[cw_serde]
    pub struct DepositMsg {}

    #[cw_serde]
    pub struct BorrowMsg {
        pub duration: u64,
    }

    #[cw_serde]
    pub struct RepayMsg {
        pub loan_id: u64,
    }

    #[cw_serde]
    pub struct WithdrawMsg {}

    #[cw_serde]
    pub struct CloseExpiredLoanMsg {
        pub loan_id: u64,
    }

    #[cw_serde]
    pub struct SwapMsg {}

    #[cw_serde]
    pub struct UpdateConfigMsg {
        pub config: PoolConfig,
        pub factory: Option<String>,
    }

    #[cw_serde]
    pub struct LoanInfoResponse {
        pub owner: Addr,
        pub loan_amount: Uint128,
        pub collateral_amount: Uint128,
        pub expiration_timestamp: u64,
        pub duration: u64,
    }

    #[cw_serde]
    pub struct CurrentInterestsResponse {
        pub borrow: Decimal,
        pub provide: Decimal,
    }
}

pub mod definitions {
    use cosmwasm_schema::cw_serde;
    use cosmwasm_std::{Addr, Decimal, StdError, StdResult, Timestamp, Uint128};
    use cw_asset::AssetInfo;
    use rhaki_cw_plus::{math::IntoDecimal, serde_value::IntoSerdeJsonString};

    use crate::{
        common::{PriceType, YEAR_IN_SECONDS},
        traits::AssertOwner,
    };

    #[cw_serde]
    pub struct PoolConfig {
        pub collateral: AssetInfo,
        pub core: AssetInfo,
        pub price_type: PriceType,
        pub interest_type: InterestType,
        pub loan_duration_type: LoanDurationType,
        pub initial_ltv: Decimal,
        pub allow_early_close_uncollateralized: bool,
        pub swap_info: SwapInfo,
        pub lp_info: AssetInfo,
    }

    impl PoolConfig {
        pub fn validate(&self) -> StdResult<()> {
            if self.initial_ltv > Decimal::one() {
                return Err(StdError::generic_err(
                    "initial_ltv must be lesser then or equal 1",
                ));
            }

            match self.loan_duration_type {
                LoanDurationType::Fixed(duration) => {
                    if duration == 0 {
                        return Err(StdError::generic_err(
                            "loan duration must be greater than 0",
                        ));
                    }
                }
                LoanDurationType::Variable { min, max, .. } => {
                    if min > max || max == 0 {
                        return Err(StdError::generic_err(
                            "variable loan min must be greater than max",
                        ));
                    }
                }
            }

            self.swap_info.validate()?;

            Ok(())
        }
    }

    #[cw_serde]
    pub enum InterestType {
        Quadratic { a: Decimal, c: Decimal },
    }

    impl InterestType {
        /// Return a Decimal, 30% => 0.3
        pub fn calculate_interest(&self, utilization_ratio: Decimal) -> Decimal {
            match self {
                Self::Quadratic { a, c } => {
                    (utilization_ratio.pow(2) * a + c) / 100_u128.into_decimal()
                }
            }
        }
    }

    impl IntoSerdeJsonString for InterestType {}

    #[cw_serde]
    pub enum LoanDurationType {
        Fixed(u64),
        Variable {
            min: u64,
            max: u64,
            interest_multiplier_at_max: Decimal,
        },
    }

    impl IntoSerdeJsonString for LoanDurationType {}

    impl LoanDurationType {
        pub fn assert_duration(&self, duration: u64) -> StdResult<()> {
            match self {
                LoanDurationType::Fixed(fix) => {
                    if *fix != duration {
                        return Err(StdError::generic_err("InvalidDuration"));
                    }
                }
                LoanDurationType::Variable { min, max, .. } => {
                    if duration < *min || duration > *max {
                        return Err(StdError::generic_err("InvalidDuration"));
                    }
                }
            }

            Ok(())
        }

        pub fn get_multiplier(&self, duration: u64) -> Decimal {
            match self {
                LoanDurationType::Fixed(..) => Decimal::one(),
                LoanDurationType::Variable {
                    min,
                    max,
                    interest_multiplier_at_max,
                } => {
                    let ratio = Decimal::from_ratio(duration - min, max - min);
                    Decimal::one() + (interest_multiplier_at_max - Decimal::one()) * ratio
                }
            }
        }
    }

    #[cw_serde]
    pub struct PoolData {
        pub core_avaiable: Uint128,
        pub core_locked: Uint128,
        pub collateral_avaiable: Uint128,
        pub collateral_locked: Uint128,
        pub global_index: Decimal,
        pub last_update: u64,
        pub factory: Addr,
        pub loan_counter: u64,
    }

    impl PoolData {
        pub fn new(current_timestamp: Timestamp, factory: Addr) -> PoolData {
            PoolData {
                last_update: current_timestamp.seconds(),
                core_avaiable: Uint128::zero(),
                core_locked: Uint128::zero(),
                collateral_avaiable: Uint128::zero(),
                collateral_locked: Uint128::zero(),
                global_index: Decimal::zero(),
                factory,
                loan_counter: 0,
            }
        }

        pub fn total_value_in_core(&self, price_collateral_in_core: Decimal) -> Uint128 {
            self.core_avaiable
                + self.collateral_avaiable * price_collateral_in_core
                + self.core_locked
            // DEPRECATED
            // + std::cmp::min(
            //     self.core_locked,
            //     self.collateral_locked * price_collateral_in_core,
            // )
        }

        pub fn utilization_ratio(&self) -> Decimal {
            if self.core_locked + self.core_avaiable > Uint128::zero() {
                Decimal::from_ratio(self.core_locked, self.core_locked + self.core_avaiable)
            } else {
                Decimal::zero()
            }
        }

        pub fn update_global_index(&mut self, current_timestamp: &Timestamp, config: &PoolConfig) {
            let interest = self.current_interest(config);

            let year_passed = Decimal::from_ratio(
                current_timestamp.seconds() - self.last_update,
                YEAR_IN_SECONDS,
            );

            self.global_index += interest * year_passed;
            self.last_update = current_timestamp.seconds()
        }

        pub fn current_interest(&self, config: &PoolConfig) -> Decimal {
            config
                .interest_type
                .calculate_interest(self.utilization_ratio())
        }
    }

    impl AssertOwner for PoolData {
        fn get_admin(&self) -> Addr {
            self.factory.clone()
        }
    }

    #[cw_serde]
    pub enum SwapInfo {
        Disabled,
        OnlyFromcore { fee: Decimal },
    }

    impl IntoSerdeJsonString for SwapInfo {}

    impl SwapInfo {
        pub fn assert_input(&self, input: AssetInfo, config: &PoolConfig) -> StdResult<()> {
            match self {
                SwapInfo::Disabled => return Err(StdError::generic_err("Swap not enable")),
                SwapInfo::OnlyFromcore { .. } => {
                    if input != config.core {
                        return Err(StdError::generic_err(format!(
                            "Wrong asset, expected: {}, received: {}",
                            config.core, input
                        )));
                    }
                }
            }

            Ok(())
        }

        pub fn validate(&self) -> StdResult<()> {
            match self {
                SwapInfo::Disabled => Ok(()),
                SwapInfo::OnlyFromcore { fee } => {
                    if *fee > Decimal::one() {
                        return Err(StdError::generic_err(
                            "close_position_fee must be lesser then or equal 1",
                        ));
                    }
                    Ok(())
                }
            }
        }

        pub fn get_fee(&self) -> StdResult<Decimal> {
            match self {
                SwapInfo::Disabled => Err(StdError::generic_err("Swap not enable")),
                SwapInfo::OnlyFromcore { fee } => Ok(*fee),
            }
        }
    }
}