use std::cmp::max;

use af_move_type::MoveType;
use af_utilities::IFixed;
use num_traits::Zero as _;

use crate::clearing_house::ClearingHouse;
use crate::{MarketParams, MarketState, Position};

pub const B9_SCALING: u64 = 1000000000;

#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum Error {
    #[error(transparent)]
    FromAfUtilities(#[from] af_utilities::types::errors::Error),
    #[error("Overflow when converting types")]
    Overflow,
    #[error("Not enough precision to represent price")]
    Precision,
    #[error("Division by zero")]
    DivisionByZero,
}

/// Convenience trait to convert to/from units used in the orderbook.
pub trait OrderBookUnits {
    /// Size in the balance9 adjusted for market's lot size to fixed-point number.
    ///
    /// # Panics
    ///
    /// If `self.lot_size() == 0`
    fn size_to_ifixed(&self, size: u64) -> IFixed {
        let size = (size / self.lot_size()) * self.lot_size();
        IFixed::from_balance_with_scaling(size, B9_SCALING.into())
    }

    /// Price in the balance9 adjusted for market's tick size to fixed-point number.
    ///
    /// # Panics
    ///
    /// If `self.tick_size() == 0`
    fn price_to_ifixed(&self, price: u64) -> IFixed {
        let price = (price / self.tick_size()) * self.tick_size();
        IFixed::from_balance_with_scaling(price, B9_SCALING.into())
    }

    /// The price in balance9 closest to the desired value, adjusted for tick_size.
    fn ifixed_to_price(&self, ifixed: IFixed) -> Result<u64, Error> {
        if ifixed.is_zero() {
            return Ok(0);
        }
        if self.tick_size() == 0 {
            return Err(Error::DivisionByZero);
        }

        let price = IFixed::try_into_balance_with_scaling(ifixed, B9_SCALING.into())?;
        let price = (price / self.tick_size()) * self.tick_size();

        Ok(price)
    }

    fn ifixed_to_size(&self, ifixed: IFixed) -> Result<u64, Error> {
        if ifixed.is_zero() {
            return Ok(0);
        }
        if self.lot_size() == 0 {
            return Err(Error::DivisionByZero);
        }

        let size = IFixed::try_into_balance_with_scaling(ifixed, B9_SCALING.into())?;
        let size = (size / self.lot_size()) * self.lot_size();

        Ok(size)
    }

    // NOTE: these could be updated to return NonZeroU64 ensuring division by zero errors are
    // impossible.
    fn lot_size(&self) -> u64;
    fn tick_size(&self) -> u64;
}

impl OrderBookUnits for MarketParams {
    fn lot_size(&self) -> u64 {
        self.core_params.lot_size
    }

    fn tick_size(&self) -> u64 {
        self.core_params.tick_size
    }
}

impl<T: MoveType> OrderBookUnits for ClearingHouse<T> {
    fn lot_size(&self) -> u64 {
        self.market_params.core_params.lot_size
    }

    fn tick_size(&self) -> u64 {
        self.market_params.core_params.tick_size
    }
}

impl<T: MoveType> ClearingHouse<T> {
    /// Convenience method for computing a position's liquidation price.
    ///
    /// Forwards to [`Position::liquidation_price`].
    pub fn liquidation_price(&self, pos: &Position, coll_price: IFixed) -> Option<IFixed> {
        pos.liquidation_price(
            coll_price,
            self.market_state.cum_funding_rate_long,
            self.market_state.cum_funding_rate_short,
            self.market_params.core_params.margin_ratio_maintenance,
        )
    }
}

impl MarketParams {
    /// The initial and maintenance margin requirements for a certain notional, in the same units.
    pub fn margin_requirements(&self, notional: IFixed) -> (IFixed, IFixed) {
        let min_margin = notional * self.core_params.margin_ratio_initial;
        let liq_margin = notional * self.core_params.margin_ratio_maintenance;
        (min_margin, liq_margin)
    }
}

impl MarketState {
    /// Convenience method for computing a position's unrealized funding.
    ///
    /// Forwards to [`Position::unrealized_funding`].
    pub fn unrealized_funding(&self, pos: &Position) -> IFixed {
        pos.unrealized_funding(self.cum_funding_rate_long, self.cum_funding_rate_short)
    }
}

impl Position {
    /// At which index price the position should be (partially) liquidated, assuming all the input
    /// variables stay the same.
    pub fn liquidation_price(
        &self,
        coll_price: IFixed,
        cum_funding_rate_long: IFixed,
        cum_funding_rate_short: IFixed,
        maintenance_margin_ratio: IFixed,
    ) -> Option<IFixed> {
        let coll = self.collateral * coll_price;
        let ufunding = self.unrealized_funding(cum_funding_rate_long, cum_funding_rate_short);
        let quote = self.quote_asset_notional_amount;
        let base = self.base_asset_amount;
        let bids_net_abs = (base + self.bids_quantity).abs();
        let asks_net_abs = (base - self.asks_quantity).abs();
        let max_abs_net_base = max(bids_net_abs, asks_net_abs);

        let denominator = max_abs_net_base * maintenance_margin_ratio - base;

        if denominator.is_zero() {
            None
        } else {
            let numerator = coll + ufunding - quote;

            Some(numerator / denominator)
        }
    }

    /// Entry price of the position's contracts; in the same units as the oracle index price.
    ///
    /// This function returns `None` if the position has no open contracts, i.e., if
    /// [`Self::base_asset_amount`] is zero.
    pub fn entry_price(&self) -> Option<IFixed> {
        if self.base_asset_amount.is_zero() {
            return None;
        }
        Some(self.quote_asset_notional_amount / self.base_asset_amount)
    }

    /// The funding yet to be settled in this position given the market's current cumulative
    /// fundings.
    ///
    /// The return value is in the same quote currency that the index price uses. E.g., if the
    /// index price is USD/BTC, then the unrealized funding is in USD units.
    pub fn unrealized_funding(
        &self,
        cum_funding_rate_long: IFixed,
        cum_funding_rate_short: IFixed,
    ) -> IFixed {
        if self.base_asset_amount.is_neg() {
            unrealized_funding(
                cum_funding_rate_short,
                self.cum_funding_rate_short,
                self.base_asset_amount,
            )
        } else {
            unrealized_funding(
                cum_funding_rate_long,
                self.cum_funding_rate_long,
                self.base_asset_amount,
            )
        }
    }

    /// Unrealized PnL given an index price.
    ///
    /// The returned value is in the same currency as what the index price is quoted at. E.g., if
    /// the index price is a ratio of BTC/ETH, then the PnL is in BTC units.
    pub fn unrealized_pnl(&self, price: IFixed) -> IFixed {
        (self.base_asset_amount * price) - self.quote_asset_notional_amount
    }

    /// Total position value used for risk calculations.
    ///
    /// The returned value is in the same currency as what the index price is quoted at. E.g., if
    /// the index price is a ratio of BTC/ETH, then the PnL is in BTC units.
    pub fn notional(&self, price: IFixed) -> IFixed {
        let size = self.base_asset_amount;
        let bids_net_abs = (size + self.bids_quantity).abs();
        let asks_net_abs = (size - self.asks_quantity).abs();
        let max_abs_net_base = max(bids_net_abs, asks_net_abs);
        max_abs_net_base * price
    }
}

fn unrealized_funding(
    cum_funding_rate_now: IFixed,
    cum_funding_rate_before: IFixed,
    size: IFixed,
) -> IFixed {
    if cum_funding_rate_now == cum_funding_rate_before {
        return IFixed::zero();
    };

    (cum_funding_rate_now - cum_funding_rate_before) * (-size)
}

#[cfg(test)]
mod tests {
    use std::num::NonZeroU64;

    use af_utilities::Balance9;
    use proptest::prelude::*;
    use test_strategy::proptest;

    use super::*;

    impl OrderBookUnits for (u64, u64) {
        fn lot_size(&self) -> u64 {
            self.0
        }

        fn tick_size(&self) -> u64 {
            self.1
        }
    }

    #[test]
    fn orderbook_units() {
        let mut units = (10_000_000, 1_000_000);
        let mut ifixed: IFixed;

        ifixed = u64::MAX.into();
        ifixed += IFixed::from_inner(1.into());
        insta::assert_snapshot!(ifixed, @"18446744073709551615.000000000000000001");
        let err = units.ifixed_to_size(ifixed).unwrap_err();
        insta::assert_snapshot!(err, @"Overflow when converting types");

        // Values smaller than 1 balance9 get cast to 0
        ifixed = IFixed::from_inner(1.into());
        insta::assert_snapshot!(ifixed, @"0.000000000000000001");
        let ok = units.ifixed_to_size(ifixed).unwrap();
        assert_eq!(ok, 0);

        // Values smaller than 1 balance9 get cast to 0
        ifixed = IFixed::from_inner(1.into());
        insta::assert_snapshot!(ifixed, @"0.000000000000000001");
        let ok = units.ifixed_to_price(ifixed).unwrap();
        assert_eq!(ok, 0);

        ifixed = 0.0.try_into().unwrap();
        insta::assert_snapshot!(ifixed, @"0.0");
        let ok = units.ifixed_to_price(ifixed).unwrap();
        assert_eq!(ok, 0);

        ifixed = 0.1.try_into().unwrap();
        insta::assert_snapshot!(ifixed, @"0.1");
        let ok = units.ifixed_to_price(ifixed).unwrap();
        assert_eq!(ok, 0_100000000);

        // `ifixed_to_price` truncates
        ifixed = 0.15.try_into().unwrap();
        insta::assert_snapshot!(ifixed, @"0.15");
        let ok = units.ifixed_to_price(ifixed).unwrap();
        assert_eq!(ok, 0_150000000);

        ifixed = units.price_to_ifixed(0);
        insta::assert_snapshot!(ifixed, @"0.0");

        // Can handle a large price
        units = (1, u64::MAX);
        let ok = units.price_to_ifixed(u64::MAX);
        insta::assert_snapshot!(ok, @"18446744073.709551615");

        // Can handle a large lot size
        units = (u64::MAX, 1);
        let ok = units.size_to_ifixed(u64::MAX);
        insta::assert_snapshot!(ok, @"18446744073.709551615");
    }

    #[test]
    #[should_panic]
    fn zero_lot_and_tick() {
        (0u64, 0u64).price_to_ifixed(1);
    }

    #[test]
    #[should_panic]
    fn zero_lot() {
        (0u64, 1u64).size_to_ifixed(1);
    }

    #[test]
    #[should_panic]
    fn zero_tick() {
        assert_eq!((1u64, 0u64).price_to_ifixed(1), IFixed::zero());
    }

    #[test]
    #[should_panic]
    fn ifixed_to_price() {
        (1u64, 0u64).ifixed_to_price(IFixed::one()).expect("Panics");
    }

    // #[derive(Arbitrary, Debug)]
    // struct Contracts {
    //     size: NonZeroU64,
    //     price: NonZeroU64,
    //     short: bool,
    // }

    impl Position {
        // fn from_contracts(
        //     collateral: IFixed,
        //     contracts: Contracts,
        //     params: &impl OrderBookUnits,
        // ) -> Self {
        //     let mut base = params.size_to_ifixed(contracts.size.into());
        //     if contracts.short {
        //         base = -base;
        //     }
        //     let mut quote = params.size_to_ifixed(contracts.price.into());
        //     if contracts.short {
        //         quote = -quote;
        //     }
        //     Self {
        //         collateral,
        //         base_asset_amount: base,
        //         quote_asset_notional_amount: quote,
        //         cum_funding_rate_long: 0.into(),
        //         cum_funding_rate_short: 0.into(),
        //         asks_quantity: 0.into(),
        //         bids_quantity: 0.into(),
        //         pending_orders: 0,
        //         maker_fee: 1.into(),
        //         taker_fee: 1.into(),
        //         initial_margin_ratio: 1.into(),
        //     }
        // }

        fn empty(collateral: IFixed) -> Self {
            Self {
                collateral,
                base_asset_amount: 0.into(),
                quote_asset_notional_amount: 0.into(),
                cum_funding_rate_long: 0.into(),
                cum_funding_rate_short: 0.into(),
                asks_quantity: 0.into(),
                bids_quantity: 0.into(),
                pending_orders: 0,
                maker_fee: 1.into(),
                taker_fee: 1.into(),
                initial_margin_ratio: 1.into(),
            }
        }
    }

    // #[proptest]
    // fn liquidation_price_is_positive(
    //     contracts: Contracts,
    //     #[strategy(0.0001..=1e12)] coll_price: f64,
    //     #[strategy(0.0001..=0.5)] maintenance_margin_ratio: f64,
    //     #[strategy(1..=1_000_000_000_u64)] lot_size: u64,
    //     #[strategy(1..=#lot_size)] tick_size: u64,
    // ) {
    //     println!("Contracts: {:?}", contracts);
    //     println!("Coll price: {:?}", coll_price);
    //     println!("MMR: {:?}", maintenance_margin_ratio);
    //     println!("Lot size: {:?}", lot_size);
    //     println!("Tick size: {:?}", tick_size);

    //     let position = Position::from_contracts(1.into(), contracts, &(lot_size, tick_size));
    //     println!("Position: {:?}", position);

    //     let liq_price = position
    //         .liquidation_price(
    //             coll_price.try_into().unwrap(),
    //             IFixed::zero(),
    //             IFixed::zero(),
    //             maintenance_margin_ratio.try_into().unwrap(),
    //         )
    //         .unwrap();
    //     dbg!(liq_price.to_string());
    //     assert!(liq_price > IFixed::zero());
    // }

    #[proptest]
    fn liquidation_price_none(
        #[strategy(any::<NonZeroU64>())]
        #[map(|x: NonZeroU64| Balance9::from_inner(x.get()))]
        collateral: Balance9,
        #[strategy(0.0001..=1e12)] coll_price: f64,
    ) {
        let position = Position::empty(collateral.into());
        let liq_price = position.liquidation_price(
            coll_price.try_into().unwrap(),
            IFixed::zero(),
            IFixed::zero(),
            0.001.try_into().unwrap(),
        );
        assert_eq!(liq_price, None);
    }
}
