//! Canonical financial types for the Foxhunt HFT trading system.
//!
//! This module provides four newtype wrappers for financial arithmetic:
//!
//! - [`Price`] — non-negative fixed-point price (i64, 6 decimal places)
//! - [`Quantity`] — signed fixed-point quantity (i64, 6 decimal places, negative = short)
//! - [`Money`] — signed fixed-point monetary amount (i64, 6 decimal places, negative = loss)
//! - [`Ratio`] — validated f64 newtype (rejects NaN/Inf)
//!
//! All fixed-point types share `SCALE = 1_000_000` (6 decimal places).
//!
//! # Cross-type arithmetic
//!
//! - `Price * Quantity -> Money` (i128 intermediate to prevent overflow)
//! - `Money / Quantity -> Price` (i128 intermediate)

use crate::types::CommonTypeError;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign};
use std::str::FromStr;

/// Shared scale factor for all fixed-point types (6 decimal places).
pub const SCALE: i64 = 1_000_000;

// ---------------------------------------------------------------------------
// Price
// ---------------------------------------------------------------------------

/// Non-negative fixed-point price with 6 decimal places.
///
/// Internally stored as `i64` with a scale of 1,000,000.
/// Rejects negative, NaN, and Inf on construction from floats.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Price(i64);

impl Price {
    /// Zero price.
    pub const ZERO: Self = Self(0);

    /// The scale factor (10^6).
    pub const SCALE: i64 = SCALE;

    /// Create a `Price` from an `f64`. Rejects negative, NaN, and Inf.
    ///
    /// # Errors
    /// Returns [`CommonTypeError::InvalidPrice`] if `value` is negative, NaN, or infinite.
    #[allow(clippy::float_arithmetic)]
    #[allow(clippy::cast_possible_truncation)]
    pub fn from_f64(value: f64) -> Result<Self, CommonTypeError> {
        if !value.is_finite() {
            return Err(CommonTypeError::InvalidPrice {
                value: value.to_string(),
                reason: "value is NaN or infinite".to_owned(),
            });
        }
        if value < 0.0_f64 {
            return Err(CommonTypeError::InvalidPrice {
                value: value.to_string(),
                reason: "price cannot be negative".to_owned(),
            });
        }
        #[allow(clippy::cast_precision_loss)]
        let scaled = (value * SCALE as f64).round() as i64;
        Ok(Self(scaled))
    }

    /// Create a `Price` from an `f32` (ML boundary).
    ///
    /// # Errors
    /// Returns [`CommonTypeError::InvalidPrice`] if `value` is negative, NaN, or infinite.
    pub fn from_f32(value: f32) -> Result<Self, CommonTypeError> {
        Self::from_f64(f64::from(value))
    }

    /// Alias for [`from_f64`](Self::from_f64).
    ///
    /// # Errors
    /// Returns [`CommonTypeError::InvalidPrice`] on invalid input.
    pub fn new(value: f64) -> Result<Self, CommonTypeError> {
        Self::from_f64(value)
    }

    /// Convert to `f64`.
    #[must_use]
    #[allow(clippy::cast_precision_loss)]
    #[allow(clippy::float_arithmetic)]
    pub fn to_f64(self) -> f64 {
        self.0 as f64 / SCALE as f64
    }

    /// Convert to `f32`.
    #[must_use]
    #[allow(clippy::cast_possible_truncation)]
    pub fn to_f32(self) -> f32 {
        self.to_f64() as f32
    }

    /// Convert to [`Decimal`].
    #[must_use]
    pub fn to_decimal(self) -> Decimal {
        Decimal::new(self.0, 6)
    }

    /// Create from [`Decimal`].
    #[must_use]
    #[allow(clippy::cast_possible_truncation)]
    pub fn from_decimal(decimal: Decimal) -> Self {
        // Normalize to 6 decimal places and extract mantissa
        let mut d = decimal;
        d.rescale(6);
        #[allow(clippy::cast_precision_loss)]
        let mantissa = d.mantissa();
        // mantissa is i128; truncate to i64
        Self(mantissa as i64)
    }

    /// Get the raw internal `i64` value.
    #[must_use]
    pub const fn raw_value(self) -> i64 {
        self.0
    }

    /// Create from a raw `i64` (already scaled).
    #[must_use]
    pub const fn from_raw(value: i64) -> Self {
        Self(value)
    }

    /// Absolute value.
    #[must_use]
    #[allow(clippy::arithmetic_side_effects)]
    pub fn abs(self) -> Self {
        Self(self.0.saturating_abs())
    }

    /// Integer square root (approximation via f64).
    #[must_use]
    #[allow(clippy::float_arithmetic)]
    #[allow(clippy::cast_precision_loss)]
    #[allow(clippy::cast_possible_truncation)]
    pub fn sqrt(self) -> Self {
        let f = self.to_f64().sqrt();
        // Re-scale: sqrt(v / SCALE) * SCALE ≠ sqrt(v), so go through f64
        Self((f * SCALE as f64).round() as i64)
    }

    /// Returns `true` if the price is zero.
    #[must_use]
    pub const fn is_zero(self) -> bool {
        self.0 == 0
    }

    /// Returns `true` if the price is strictly positive.
    #[must_use]
    pub const fn is_positive(self) -> bool {
        self.0 > 0
    }

    /// Returns `true` if the price is strictly negative.
    #[must_use]
    pub const fn is_negative(self) -> bool {
        self.0 < 0
    }

    /// Scale this price by a [`Ratio`].
    #[must_use]
    #[allow(clippy::float_arithmetic)]
    #[allow(clippy::cast_precision_loss)]
    #[allow(clippy::cast_possible_truncation)]
    pub fn scale_by(self, ratio: Ratio) -> Self {
        let result = self.to_f64() * ratio.to_f64();
        Self((result * SCALE as f64).round() as i64)
    }
}

// -- Arithmetic impls for Price --

impl Add for Price {
    type Output = Self;
    #[allow(clippy::arithmetic_side_effects)]
    fn add(self, rhs: Self) -> Self::Output {
        Self(self.0.saturating_add(rhs.0))
    }
}

impl AddAssign for Price {
    #[allow(clippy::arithmetic_side_effects)]
    fn add_assign(&mut self, rhs: Self) {
        self.0 = self.0.saturating_add(rhs.0);
    }
}

impl Sub for Price {
    type Output = Self;
    #[allow(clippy::arithmetic_side_effects)]
    fn sub(self, rhs: Self) -> Self::Output {
        Self(self.0.saturating_sub(rhs.0))
    }
}

impl SubAssign for Price {
    #[allow(clippy::arithmetic_side_effects)]
    fn sub_assign(&mut self, rhs: Self) {
        self.0 = self.0.saturating_sub(rhs.0);
    }
}

impl Mul<i64> for Price {
    type Output = Self;
    #[allow(clippy::arithmetic_side_effects)]
    fn mul(self, rhs: i64) -> Self::Output {
        Self(self.0.saturating_mul(rhs))
    }
}

impl Div<i64> for Price {
    type Output = Self;
    #[allow(clippy::arithmetic_side_effects)]
    #[allow(clippy::integer_division)]
    fn div(self, rhs: i64) -> Self::Output {
        if rhs == 0 {
            return Self::ZERO;
        }
        Self(self.0 / rhs)
    }
}

impl Neg for Price {
    type Output = Self;
    #[allow(clippy::arithmetic_side_effects)]
    fn neg(self) -> Self::Output {
        Self(self.0.saturating_neg())
    }
}

impl Mul<f64> for Price {
    type Output = Result<Self, CommonTypeError>;
    #[allow(clippy::float_arithmetic)]
    fn mul(self, rhs: f64) -> Self::Output {
        if !rhs.is_finite() {
            return Err(CommonTypeError::ConversionError {
                message: format!("cannot multiply price by {rhs}"),
            });
        }
        Self::from_f64(self.to_f64() * rhs)
    }
}

impl Div<f64> for Price {
    type Output = Result<Self, CommonTypeError>;
    #[allow(clippy::float_arithmetic)]
    fn div(self, rhs: f64) -> Self::Output {
        if rhs == 0.0_f64 {
            return Err(CommonTypeError::ConversionError {
                message: "cannot divide price by zero".to_owned(),
            });
        }
        if !rhs.is_finite() {
            return Err(CommonTypeError::ConversionError {
                message: format!("cannot divide price by {rhs}"),
            });
        }
        Self::from_f64(self.to_f64() / rhs)
    }
}

impl From<Decimal> for Price {
    fn from(decimal: Decimal) -> Self {
        Self::from_decimal(decimal)
    }
}

impl From<Price> for Decimal {
    fn from(price: Price) -> Self {
        price.to_decimal()
    }
}

impl PartialEq<f64> for Price {
    #[allow(clippy::float_arithmetic)]
    fn eq(&self, other: &f64) -> bool {
        (self.to_f64() - other).abs() < 1e-6
    }
}

impl PartialOrd<f64> for Price {
    fn partial_cmp(&self, other: &f64) -> Option<std::cmp::Ordering> {
        self.to_f64().partial_cmp(other)
    }
}

impl fmt::Display for Price {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:.6}", self.to_f64())
    }
}

impl FromStr for Price {
    type Err = CommonTypeError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let parsed = s
            .parse::<f64>()
            .map_err(|e| CommonTypeError::InvalidPrice {
                value: s.to_owned(),
                reason: format!("cannot parse as price: {e}"),
            })?;
        Self::from_f64(parsed)
    }
}

impl Default for Price {
    fn default() -> Self {
        Self::ZERO
    }
}

// ---------------------------------------------------------------------------
// Quantity
// ---------------------------------------------------------------------------

/// Signed fixed-point quantity with 6 decimal places.
///
/// Negative values represent short positions.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Quantity(i64);

impl Quantity {
    /// Zero quantity.
    pub const ZERO: Self = Self(0);

    /// The scale factor (10^6).
    pub const SCALE: i64 = SCALE;

    /// Create a `Quantity` from an `f64`. Allows negative (shorts). Rejects NaN/Inf.
    ///
    /// # Errors
    /// Returns [`CommonTypeError::InvalidQuantity`] if `value` is NaN or infinite.
    #[allow(clippy::float_arithmetic)]
    #[allow(clippy::cast_possible_truncation)]
    pub fn from_f64(value: f64) -> Result<Self, CommonTypeError> {
        if !value.is_finite() {
            return Err(CommonTypeError::InvalidQuantity {
                value: value.to_string(),
                reason: "value is NaN or infinite".to_owned(),
            });
        }
        #[allow(clippy::cast_precision_loss)]
        let scaled = (value * SCALE as f64).round() as i64;
        Ok(Self(scaled))
    }

    /// Create a `Quantity` from an `f32`.
    ///
    /// # Errors
    /// Returns [`CommonTypeError::InvalidQuantity`] if `value` is NaN or infinite.
    pub fn from_f32(value: f32) -> Result<Self, CommonTypeError> {
        Self::from_f64(f64::from(value))
    }

    /// Convert to `f64`.
    #[must_use]
    #[allow(clippy::cast_precision_loss)]
    #[allow(clippy::float_arithmetic)]
    pub fn to_f64(self) -> f64 {
        self.0 as f64 / SCALE as f64
    }

    /// Convert to `f32`.
    #[must_use]
    #[allow(clippy::cast_possible_truncation)]
    pub fn to_f32(self) -> f32 {
        self.to_f64() as f32
    }

    /// Convert to [`Decimal`].
    #[must_use]
    pub fn to_decimal(self) -> Decimal {
        Decimal::new(self.0, 6)
    }

    /// Create from a whole number of units (e.g., 5 contracts). Multiplies by `SCALE`.
    #[must_use]
    #[allow(clippy::arithmetic_side_effects)]
    pub fn from_i64(value: i64) -> Self {
        Self(value.saturating_mul(SCALE))
    }

    /// Create from a raw `i64` (already scaled).
    #[must_use]
    pub const fn from_raw(value: i64) -> Self {
        Self(value)
    }

    /// Get the raw internal `i64` value.
    #[must_use]
    pub const fn raw_value(self) -> i64 {
        self.0
    }

    /// Absolute value.
    #[must_use]
    #[allow(clippy::arithmetic_side_effects)]
    pub fn abs(self) -> Self {
        Self(self.0.saturating_abs())
    }

    /// Returns `true` if the quantity is zero.
    #[must_use]
    pub const fn is_zero(self) -> bool {
        self.0 == 0
    }

    /// Returns `true` if the quantity is strictly positive.
    #[must_use]
    pub const fn is_positive(self) -> bool {
        self.0 > 0
    }

    /// Returns `true` if the quantity is strictly negative (short).
    #[must_use]
    pub const fn is_negative(self) -> bool {
        self.0 < 0
    }
}

// -- Arithmetic impls for Quantity --

impl Add for Quantity {
    type Output = Self;
    #[allow(clippy::arithmetic_side_effects)]
    fn add(self, rhs: Self) -> Self::Output {
        Self(self.0.saturating_add(rhs.0))
    }
}

impl AddAssign for Quantity {
    #[allow(clippy::arithmetic_side_effects)]
    fn add_assign(&mut self, rhs: Self) {
        self.0 = self.0.saturating_add(rhs.0);
    }
}

impl Sub for Quantity {
    type Output = Self;
    #[allow(clippy::arithmetic_side_effects)]
    fn sub(self, rhs: Self) -> Self::Output {
        Self(self.0.saturating_sub(rhs.0))
    }
}

impl SubAssign for Quantity {
    #[allow(clippy::arithmetic_side_effects)]
    fn sub_assign(&mut self, rhs: Self) {
        self.0 = self.0.saturating_sub(rhs.0);
    }
}

impl Mul<i64> for Quantity {
    type Output = Self;
    #[allow(clippy::arithmetic_side_effects)]
    fn mul(self, rhs: i64) -> Self::Output {
        Self(self.0.saturating_mul(rhs))
    }
}

impl Div<i64> for Quantity {
    type Output = Self;
    #[allow(clippy::arithmetic_side_effects)]
    #[allow(clippy::integer_division)]
    fn div(self, rhs: i64) -> Self::Output {
        if rhs == 0 {
            return Self::ZERO;
        }
        Self(self.0 / rhs)
    }
}

impl Neg for Quantity {
    type Output = Self;
    #[allow(clippy::arithmetic_side_effects)]
    fn neg(self) -> Self::Output {
        Self(self.0.saturating_neg())
    }
}

impl fmt::Display for Quantity {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:.6}", self.to_f64())
    }
}

impl Default for Quantity {
    fn default() -> Self {
        Self::ZERO
    }
}

// ---------------------------------------------------------------------------
// Money
// ---------------------------------------------------------------------------

/// Signed fixed-point monetary amount with 6 decimal places.
///
/// Negative values represent losses. Rejects NaN/Inf.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Money(i64);

impl Money {
    /// Zero money.
    pub const ZERO: Self = Self(0);

    /// The scale factor (10^6).
    pub const SCALE: i64 = SCALE;

    /// Create from `f64`. Allows negative (losses). Rejects NaN/Inf.
    ///
    /// # Errors
    /// Returns [`CommonTypeError::ConversionError`] if `value` is NaN or infinite.
    #[allow(clippy::float_arithmetic)]
    #[allow(clippy::cast_possible_truncation)]
    pub fn from_f64(value: f64) -> Result<Self, CommonTypeError> {
        if !value.is_finite() {
            return Err(CommonTypeError::ConversionError {
                message: format!("money value is NaN or infinite: {value}"),
            });
        }
        #[allow(clippy::cast_precision_loss)]
        let scaled = (value * SCALE as f64).round() as i64;
        Ok(Self(scaled))
    }

    /// Convert to `f64`.
    #[must_use]
    #[allow(clippy::cast_precision_loss)]
    #[allow(clippy::float_arithmetic)]
    pub fn to_f64(self) -> f64 {
        self.0 as f64 / SCALE as f64
    }

    /// Convert to [`Decimal`].
    #[must_use]
    pub fn to_decimal(self) -> Decimal {
        Decimal::new(self.0, 6)
    }

    /// Create from a raw `i64` (already scaled).
    #[must_use]
    pub const fn from_raw(value: i64) -> Self {
        Self(value)
    }

    /// Get the raw internal `i64` value.
    #[must_use]
    pub const fn raw_value(self) -> i64 {
        self.0
    }

    /// Absolute value.
    #[must_use]
    #[allow(clippy::arithmetic_side_effects)]
    pub fn abs(self) -> Self {
        Self(self.0.saturating_abs())
    }

    /// Returns `true` if the amount is zero.
    #[must_use]
    pub const fn is_zero(self) -> bool {
        self.0 == 0
    }

    /// Returns `true` if the amount is strictly positive.
    #[must_use]
    pub const fn is_positive(self) -> bool {
        self.0 > 0
    }

    /// Returns `true` if the amount is strictly negative.
    #[must_use]
    pub const fn is_negative(self) -> bool {
        self.0 < 0
    }
}

// -- Arithmetic impls for Money --

impl Add for Money {
    type Output = Self;
    #[allow(clippy::arithmetic_side_effects)]
    fn add(self, rhs: Self) -> Self::Output {
        Self(self.0.saturating_add(rhs.0))
    }
}

impl AddAssign for Money {
    #[allow(clippy::arithmetic_side_effects)]
    fn add_assign(&mut self, rhs: Self) {
        self.0 = self.0.saturating_add(rhs.0);
    }
}

impl Sub for Money {
    type Output = Self;
    #[allow(clippy::arithmetic_side_effects)]
    fn sub(self, rhs: Self) -> Self::Output {
        Self(self.0.saturating_sub(rhs.0))
    }
}

impl SubAssign for Money {
    #[allow(clippy::arithmetic_side_effects)]
    fn sub_assign(&mut self, rhs: Self) {
        self.0 = self.0.saturating_sub(rhs.0);
    }
}

impl Mul<i64> for Money {
    type Output = Self;
    #[allow(clippy::arithmetic_side_effects)]
    fn mul(self, rhs: i64) -> Self::Output {
        Self(self.0.saturating_mul(rhs))
    }
}

impl Div<i64> for Money {
    type Output = Self;
    #[allow(clippy::arithmetic_side_effects)]
    #[allow(clippy::integer_division)]
    fn div(self, rhs: i64) -> Self::Output {
        if rhs == 0 {
            return Self::ZERO;
        }
        Self(self.0 / rhs)
    }
}

impl Neg for Money {
    type Output = Self;
    #[allow(clippy::arithmetic_side_effects)]
    fn neg(self) -> Self::Output {
        Self(self.0.saturating_neg())
    }
}

impl fmt::Display for Money {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:.6}", self.to_f64())
    }
}

impl Default for Money {
    fn default() -> Self {
        Self::ZERO
    }
}

// ---------------------------------------------------------------------------
// Ratio
// ---------------------------------------------------------------------------

/// Validated `f64` newtype. Rejects NaN and Inf on construction.
///
/// Division by zero returns `Ratio(0.0)` instead of panicking.
#[derive(Debug, Clone, Copy, PartialOrd, Serialize, Deserialize)]
pub struct Ratio(f64);

impl Ratio {
    /// Zero ratio.
    pub const ZERO: Self = Self(0.0);

    /// One ratio.
    pub const ONE: Self = Self(1.0);

    /// Create a new `Ratio`, rejecting NaN and Inf.
    ///
    /// # Errors
    /// Returns [`CommonTypeError::ConversionError`] if `value` is NaN or infinite.
    pub fn new(value: f64) -> Result<Self, CommonTypeError> {
        if !value.is_finite() {
            return Err(CommonTypeError::ConversionError {
                message: format!("ratio value is NaN or infinite: {value}"),
            });
        }
        Ok(Self(value))
    }

    /// Create from a raw `f64` without validation. For const/trusted contexts.
    #[must_use]
    pub const fn from_raw(value: f64) -> Self {
        Self(value)
    }

    /// Convert to `f64`.
    #[must_use]
    pub const fn to_f64(self) -> f64 {
        self.0
    }

    /// Convert to `f32`.
    #[must_use]
    #[allow(clippy::cast_possible_truncation)]
    pub fn to_f32(self) -> f32 {
        self.0 as f32
    }

    /// Absolute value.
    #[must_use]
    #[allow(clippy::float_arithmetic)]
    pub fn abs(self) -> Self {
        Self(self.0.abs())
    }

    /// Square root (returns 0.0 for negative inputs).
    #[must_use]
    #[allow(clippy::float_arithmetic)]
    pub fn sqrt(self) -> Self {
        if self.0 < 0.0 {
            return Self(0.0);
        }
        Self(self.0.sqrt())
    }

    /// Natural logarithm. Returns `Ratio(0.0)` for non-positive inputs
    /// (preserves the finite invariant — `ln(-1)` would be NaN).
    #[must_use]
    #[allow(clippy::float_arithmetic)]
    pub fn ln(self) -> Self {
        if self.0 <= 0.0 {
            return Self(0.0);
        }
        Self(self.0.ln())
    }

    /// Exponential (`e^self`). Clamps to `f64::MAX` to preserve the finite invariant.
    #[must_use]
    #[allow(clippy::float_arithmetic)]
    pub fn exp(self) -> Self {
        let result = self.0.exp();
        if result.is_finite() {
            Self(result)
        } else {
            Self(f64::MAX)
        }
    }

    /// Raise to an integer power. Clamps to `f64::MAX`/`f64::MIN` to preserve finite invariant.
    #[must_use]
    #[allow(clippy::float_arithmetic)]
    pub fn powi(self, n: i32) -> Self {
        let result = self.0.powi(n);
        if result.is_finite() {
            Self(result)
        } else if result.is_sign_positive() {
            Self(f64::MAX)
        } else {
            Self(f64::MIN)
        }
    }

    /// Clamp value to `[min, max]`.
    #[must_use]
    #[allow(clippy::float_arithmetic)]
    pub fn clamp(self, min: f64, max: f64) -> Self {
        Self(self.0.clamp(min, max))
    }

    /// Returns `true` if the ratio is strictly positive.
    #[must_use]
    pub fn is_positive(self) -> bool {
        self.0 > 0.0
    }

    /// Returns `true` if the ratio is strictly negative.
    #[must_use]
    pub fn is_negative(self) -> bool {
        self.0 < 0.0
    }
}

// -- Arithmetic impls for Ratio --

/// Clamp an f64 result to preserve the finite invariant.
/// Returns `f64::MAX` for positive infinity, `f64::MIN` for negative infinity.
#[inline]
fn clamp_finite(value: f64) -> f64 {
    if value.is_finite() {
        value
    } else if value.is_sign_positive() {
        f64::MAX
    } else {
        f64::MIN
    }
}

impl Add for Ratio {
    type Output = Self;
    #[allow(clippy::float_arithmetic)]
    fn add(self, rhs: Self) -> Self::Output {
        Self(clamp_finite(self.0 + rhs.0))
    }
}

impl AddAssign for Ratio {
    #[allow(clippy::float_arithmetic)]
    fn add_assign(&mut self, rhs: Self) {
        self.0 = clamp_finite(self.0 + rhs.0);
    }
}

impl Sub for Ratio {
    type Output = Self;
    #[allow(clippy::float_arithmetic)]
    fn sub(self, rhs: Self) -> Self::Output {
        Self(clamp_finite(self.0 - rhs.0))
    }
}

impl SubAssign for Ratio {
    #[allow(clippy::float_arithmetic)]
    fn sub_assign(&mut self, rhs: Self) {
        self.0 = clamp_finite(self.0 - rhs.0);
    }
}

impl Mul for Ratio {
    type Output = Self;
    #[allow(clippy::float_arithmetic)]
    fn mul(self, rhs: Self) -> Self::Output {
        Self(clamp_finite(self.0 * rhs.0))
    }
}

impl Div for Ratio {
    type Output = Self;
    #[allow(clippy::float_arithmetic)]
    fn div(self, rhs: Self) -> Self::Output {
        if rhs.0 == 0.0 {
            return Self(0.0);
        }
        Self(clamp_finite(self.0 / rhs.0))
    }
}

impl Neg for Ratio {
    type Output = Self;
    #[allow(clippy::float_arithmetic)]
    fn neg(self) -> Self::Output {
        Self(-self.0)
    }
}

impl PartialEq for Ratio {
    #[allow(clippy::float_arithmetic)]
    fn eq(&self, other: &Self) -> bool {
        (self.0 - other.0).abs() < f64::EPSILON
    }
}

impl fmt::Display for Ratio {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl Default for Ratio {
    fn default() -> Self {
        Self::ZERO
    }
}

// ---------------------------------------------------------------------------
// Cross-type arithmetic
// ---------------------------------------------------------------------------

/// Saturating conversion from i128 to i64. Clamps to `i64::MAX`/`i64::MIN` on overflow.
#[inline]
#[allow(clippy::cast_possible_truncation)]
fn saturating_i128_to_i64(value: i128) -> i64 {
    value.clamp(i128::from(i64::MIN), i128::from(i64::MAX)) as i64
}

/// `Price * Quantity -> Money` using i128 intermediate to avoid overflow.
impl Mul<Quantity> for Price {
    type Output = Money;
    #[allow(clippy::arithmetic_side_effects)]
    fn mul(self, rhs: Quantity) -> Self::Output {
        let wide = i128::from(self.0) * i128::from(rhs.0);
        // Both are scaled by SCALE, so product is scaled by SCALE^2.
        // Divide by SCALE to get back to SCALE.
        let result = wide / i128::from(SCALE);
        Money(saturating_i128_to_i64(result))
    }
}

/// `Money / Quantity -> Price` using i128 intermediate to avoid overflow.
impl Div<Quantity> for Money {
    type Output = Price;
    #[allow(clippy::arithmetic_side_effects)]
    fn div(self, rhs: Quantity) -> Self::Output {
        if rhs.0 == 0 {
            return Price::ZERO;
        }
        // Multiply by SCALE first to keep precision, then divide.
        let wide = i128::from(self.0) * i128::from(SCALE);
        let result = wide / i128::from(rhs.0);
        Price(saturating_i128_to_i64(result))
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;

    // ===== Price tests =====

    #[test]
    fn price_roundtrip_f64() {
        let p = Price::from_f64(123.456789).unwrap();
        let diff = (p.to_f64() - 123.456789).abs();
        assert!(diff < 1e-6, "diff was {diff}");
    }

    #[test]
    fn price_rejects_negative() {
        assert!(Price::from_f64(-1.0).is_err());
    }

    #[test]
    fn price_rejects_nan() {
        assert!(Price::from_f64(f64::NAN).is_err());
    }

    #[test]
    fn price_rejects_inf() {
        assert!(Price::from_f64(f64::INFINITY).is_err());
        assert!(Price::from_f64(f64::NEG_INFINITY).is_err());
    }

    #[test]
    fn price_zero_constant() {
        assert_eq!(Price::ZERO, Price::from_f64(0.0).unwrap());
        assert!(Price::ZERO.is_zero());
    }

    #[test]
    fn price_add_sub() {
        let a = Price::from_f64(10.5).unwrap();
        let b = Price::from_f64(3.25).unwrap();
        let sum = a + b;
        assert!((sum.to_f64() - 13.75).abs() < 1e-6);
        let diff = a - b;
        assert!((diff.to_f64() - 7.25).abs() < 1e-6);
    }

    #[test]
    fn price_mul_i64() {
        let p = Price::from_f64(5.0).unwrap();
        let result = p * 3;
        assert!((result.to_f64() - 15.0).abs() < 1e-6);
    }

    #[test]
    fn price_div_i64() {
        let p = Price::from_f64(15.0).unwrap();
        let result = p / 3;
        assert!((result.to_f64() - 5.0).abs() < 1e-6);
    }

    #[test]
    fn price_div_i64_by_zero() {
        let p = Price::from_f64(10.0).unwrap();
        let result = p / 0;
        assert_eq!(result, Price::ZERO);
    }

    #[test]
    fn price_decimal_roundtrip() {
        let p = Price::from_f64(42.123456).unwrap();
        let d = p.to_decimal();
        let p2 = Price::from_decimal(d);
        assert_eq!(p, p2);
    }

    #[test]
    fn price_display() {
        let p = Price::from_f64(1.5).unwrap();
        let s = format!("{p}");
        assert_eq!(s, "1.500000");
    }

    #[test]
    fn price_from_str() {
        let p: Price = "42.5".parse().unwrap();
        assert!((p.to_f64() - 42.5).abs() < 1e-6);
    }

    #[test]
    fn price_from_str_invalid() {
        assert!("abc".parse::<Price>().is_err());
        assert!("-1.0".parse::<Price>().is_err());
    }

    #[test]
    fn price_ordering() {
        let a = Price::from_f64(1.0).unwrap();
        let b = Price::from_f64(2.0).unwrap();
        assert!(a < b);
        assert!(b > a);
        assert_eq!(a, a);
    }

    #[test]
    fn price_abs() {
        // Price should always be non-negative from_f64, but abs works on raw
        let p = Price::from_raw(-5_000_000);
        assert_eq!(p.abs().raw_value(), 5_000_000);
    }

    #[test]
    fn price_to_f32_from_f32() {
        let p = Price::from_f32(10.5_f32).unwrap();
        let f = p.to_f32();
        assert!((f - 10.5_f32).abs() < 1e-4);
    }

    #[test]
    fn price_partial_eq_f64() {
        let p = Price::from_f64(42.0).unwrap();
        assert_eq!(p, 42.0_f64);
    }

    #[test]
    fn price_partial_ord_f64() {
        let p = Price::from_f64(10.0).unwrap();
        assert!(p < 20.0_f64);
        assert!(p > 5.0_f64);
    }

    #[test]
    fn price_mul_f64() {
        let p = Price::from_f64(10.0).unwrap();
        let result = (p * 2.5).unwrap();
        assert!((result.to_f64() - 25.0).abs() < 1e-6);
    }

    #[test]
    fn price_div_f64_by_zero() {
        let p = Price::from_f64(10.0).unwrap();
        assert!((p / 0.0_f64).is_err());
    }

    #[test]
    fn price_sqrt() {
        let p = Price::from_f64(9.0).unwrap();
        let s = p.sqrt();
        assert!((s.to_f64() - 3.0).abs() < 1e-6);
    }

    #[test]
    fn price_neg() {
        let p = Price::from_f64(10.0).unwrap();
        let n = -p;
        assert_eq!(n.raw_value(), -10_000_000);
    }

    #[test]
    fn price_default() {
        assert_eq!(Price::default(), Price::ZERO);
    }

    #[test]
    fn price_add_assign() {
        let mut p = Price::from_f64(1.0).unwrap();
        p += Price::from_f64(2.0).unwrap();
        assert!((p.to_f64() - 3.0).abs() < 1e-6);
    }

    #[test]
    fn price_sub_assign() {
        let mut p = Price::from_f64(5.0).unwrap();
        p -= Price::from_f64(2.0).unwrap();
        assert!((p.to_f64() - 3.0).abs() < 1e-6);
    }

    #[test]
    fn price_is_positive_negative() {
        let p = Price::from_f64(1.0).unwrap();
        assert!(p.is_positive());
        assert!(!p.is_negative());
        assert!(!Price::ZERO.is_positive());
    }

    #[test]
    fn price_from_into_decimal() {
        let d = Decimal::new(42_123_456, 6);
        let p: Price = d.into();
        let d2: Decimal = p.into();
        assert_eq!(d, d2);
    }

    // ===== Quantity tests =====

    #[test]
    fn quantity_roundtrip_f64() {
        let q = Quantity::from_f64(100.5).unwrap();
        let diff = (q.to_f64() - 100.5).abs();
        assert!(diff < 1e-6, "diff was {diff}");
    }

    #[test]
    fn quantity_allows_negative() {
        let q = Quantity::from_f64(-5.0).unwrap();
        assert!(q.is_negative());
        assert!((q.to_f64() - (-5.0)).abs() < 1e-6);
    }

    #[test]
    fn quantity_rejects_nan() {
        assert!(Quantity::from_f64(f64::NAN).is_err());
    }

    #[test]
    fn quantity_rejects_inf() {
        assert!(Quantity::from_f64(f64::INFINITY).is_err());
    }

    #[test]
    fn quantity_from_i64() {
        let q = Quantity::from_i64(5);
        assert!((q.to_f64() - 5.0).abs() < 1e-6);
        let q_neg = Quantity::from_i64(-3);
        assert!((q_neg.to_f64() - (-3.0)).abs() < 1e-6);
    }

    #[test]
    fn quantity_add_sub() {
        let a = Quantity::from_f64(10.0).unwrap();
        let b = Quantity::from_f64(3.0).unwrap();
        let sum = a + b;
        assert!((sum.to_f64() - 13.0).abs() < 1e-6);
        let diff = a - b;
        assert!((diff.to_f64() - 7.0).abs() < 1e-6);
    }

    #[test]
    fn quantity_abs() {
        let q = Quantity::from_f64(-5.0).unwrap();
        assert!((q.abs().to_f64() - 5.0).abs() < 1e-6);
    }

    #[test]
    fn quantity_mul_div_i64() {
        let q = Quantity::from_f64(4.0).unwrap();
        let m = q * 3;
        assert!((m.to_f64() - 12.0).abs() < 1e-6);
        let d = q / 2;
        assert!((d.to_f64() - 2.0).abs() < 1e-6);
    }

    #[test]
    fn quantity_neg() {
        let q = Quantity::from_f64(7.0).unwrap();
        let n = -q;
        assert!((n.to_f64() - (-7.0)).abs() < 1e-6);
    }

    #[test]
    fn quantity_zero() {
        assert!(Quantity::ZERO.is_zero());
        assert_eq!(Quantity::default(), Quantity::ZERO);
    }

    #[test]
    fn quantity_display() {
        let q = Quantity::from_f64(2.5).unwrap();
        assert_eq!(format!("{q}"), "2.500000");
    }

    #[test]
    fn quantity_to_decimal() {
        let q = Quantity::from_i64(10);
        let d = q.to_decimal();
        assert_eq!(d, Decimal::new(10_000_000, 6));
    }

    // ===== Money tests =====

    #[test]
    fn money_roundtrip_f64() {
        let m = Money::from_f64(999.99).unwrap();
        let diff = (m.to_f64() - 999.99).abs();
        assert!(diff < 1e-6, "diff was {diff}");
    }

    #[test]
    fn money_allows_negative() {
        let m = Money::from_f64(-100.0).unwrap();
        assert!(m.is_negative());
        assert!((m.to_f64() - (-100.0)).abs() < 1e-6);
    }

    #[test]
    fn money_rejects_nan() {
        assert!(Money::from_f64(f64::NAN).is_err());
    }

    #[test]
    fn money_rejects_inf() {
        assert!(Money::from_f64(f64::INFINITY).is_err());
    }

    #[test]
    fn money_add_sub() {
        let a = Money::from_f64(100.0).unwrap();
        let b = Money::from_f64(25.5).unwrap();
        let sum = a + b;
        assert!((sum.to_f64() - 125.5).abs() < 1e-6);
        let diff = a - b;
        assert!((diff.to_f64() - 74.5).abs() < 1e-6);
    }

    #[test]
    fn money_mul_div_i64() {
        let m = Money::from_f64(10.0).unwrap();
        let result = m * 3;
        assert!((result.to_f64() - 30.0).abs() < 1e-6);
        let result = m / 2;
        assert!((result.to_f64() - 5.0).abs() < 1e-6);
    }

    #[test]
    fn money_div_by_zero() {
        let m = Money::from_f64(10.0).unwrap();
        let result = m / 0;
        assert_eq!(result, Money::ZERO);
    }

    #[test]
    fn money_abs() {
        let m = Money::from_f64(-50.0).unwrap();
        assert!((m.abs().to_f64() - 50.0).abs() < 1e-6);
    }

    #[test]
    fn money_zero() {
        assert!(Money::ZERO.is_zero());
        assert_eq!(Money::default(), Money::ZERO);
    }

    #[test]
    fn money_display() {
        let m = Money::from_f64(42.0).unwrap();
        assert_eq!(format!("{m}"), "42.000000");
    }

    #[test]
    fn money_to_decimal() {
        let m = Money::from_f64(100.0).unwrap();
        let d = m.to_decimal();
        assert_eq!(d, Decimal::new(100_000_000, 6));
    }

    #[test]
    fn money_from_raw() {
        let m = Money::from_raw(5_000_000);
        assert!((m.to_f64() - 5.0).abs() < 1e-6);
    }

    // ===== Cross-type tests =====

    #[test]
    fn price_times_quantity_equals_money() {
        let price = Price::from_f64(100.0).unwrap();
        let qty = Quantity::from_i64(5);
        let money: Money = price * qty;
        assert!((money.to_f64() - 500.0).abs() < 1e-6);
    }

    #[test]
    fn price_times_negative_quantity() {
        let price = Price::from_f64(50.0).unwrap();
        let qty = Quantity::from_f64(-2.0).unwrap();
        let money: Money = price * qty;
        assert!((money.to_f64() - (-100.0)).abs() < 1e-6);
    }

    #[test]
    fn money_div_quantity_equals_price() {
        let money = Money::from_f64(500.0).unwrap();
        let qty = Quantity::from_i64(5);
        let price: Price = money / qty;
        assert!((price.to_f64() - 100.0).abs() < 1e-6);
    }

    #[test]
    fn money_div_zero_quantity() {
        let money = Money::from_f64(500.0).unwrap();
        let qty = Quantity::ZERO;
        let price: Price = money / qty;
        assert_eq!(price, Price::ZERO);
    }

    #[test]
    fn cross_type_roundtrip() {
        // price * qty = money, then money / qty should get price back
        let price = Price::from_f64(42.5).unwrap();
        let qty = Quantity::from_i64(10);
        let money: Money = price * qty;
        let price_back: Price = money / qty;
        assert!((price_back.to_f64() - 42.5).abs() < 1e-6);
    }

    // ===== Ratio tests =====

    #[test]
    fn ratio_rejects_nan() {
        assert!(Ratio::new(f64::NAN).is_err());
    }

    #[test]
    fn ratio_rejects_inf() {
        assert!(Ratio::new(f64::INFINITY).is_err());
        assert!(Ratio::new(f64::NEG_INFINITY).is_err());
    }

    #[test]
    fn ratio_allows_negative() {
        let r = Ratio::new(-0.5).unwrap();
        assert!(r.is_negative());
        assert!((r.to_f64() - (-0.5)).abs() < f64::EPSILON);
    }

    #[test]
    fn ratio_arithmetic() {
        let a = Ratio::new(3.0).unwrap();
        let b = Ratio::new(2.0).unwrap();
        assert!(((a + b).to_f64() - 5.0).abs() < f64::EPSILON);
        assert!(((a - b).to_f64() - 1.0).abs() < f64::EPSILON);
        assert!(((a * b).to_f64() - 6.0).abs() < f64::EPSILON);
        assert!(((a / b).to_f64() - 1.5).abs() < f64::EPSILON);
    }

    #[test]
    fn ratio_div_by_zero() {
        let a = Ratio::new(5.0).unwrap();
        let b = Ratio::ZERO;
        assert_eq!((a / b).to_f64(), 0.0);
    }

    #[test]
    fn ratio_sqrt() {
        let r = Ratio::new(9.0).unwrap();
        assert!((r.sqrt().to_f64() - 3.0).abs() < 1e-10);
    }

    #[test]
    fn ratio_sqrt_negative() {
        let r = Ratio::new(-4.0).unwrap();
        assert_eq!(r.sqrt().to_f64(), 0.0);
    }

    #[test]
    fn ratio_ln_exp_roundtrip() {
        let r = Ratio::new(2.5).unwrap();
        let back = r.ln().exp();
        assert!((back.to_f64() - 2.5).abs() < 1e-10);
    }

    #[test]
    fn ratio_powi() {
        let r = Ratio::new(3.0).unwrap();
        assert!((r.powi(2).to_f64() - 9.0).abs() < 1e-10);
    }

    #[test]
    fn ratio_clamp() {
        let r = Ratio::new(10.0).unwrap();
        let clamped = r.clamp(0.0, 5.0);
        assert!((clamped.to_f64() - 5.0).abs() < f64::EPSILON);

        let r2 = Ratio::new(-3.0).unwrap();
        let clamped2 = r2.clamp(0.0, 5.0);
        assert!(clamped2.to_f64().abs() < f64::EPSILON);
    }

    #[test]
    fn ratio_neg() {
        let r = Ratio::new(1.5).unwrap();
        let n = -r;
        assert!((n.to_f64() - (-1.5)).abs() < f64::EPSILON);
    }

    #[test]
    fn ratio_default() {
        assert_eq!(Ratio::default(), Ratio::ZERO);
    }

    #[test]
    fn ratio_display() {
        let r = Ratio::new(0.5).unwrap();
        assert_eq!(format!("{r}"), "0.5");
    }

    #[test]
    fn ratio_constants() {
        assert_eq!(Ratio::ZERO.to_f64(), 0.0);
        assert_eq!(Ratio::ONE.to_f64(), 1.0);
    }

    #[test]
    fn ratio_add_assign() {
        let mut r = Ratio::new(1.0).unwrap();
        r += Ratio::new(0.5).unwrap();
        assert!((r.to_f64() - 1.5).abs() < f64::EPSILON);
    }

    #[test]
    fn ratio_sub_assign() {
        let mut r = Ratio::new(2.0).unwrap();
        r -= Ratio::new(0.5).unwrap();
        assert!((r.to_f64() - 1.5).abs() < f64::EPSILON);
    }

    #[test]
    fn ratio_to_f32() {
        let r = Ratio::new(1.5).unwrap();
        assert!((r.to_f32() - 1.5_f32).abs() < 1e-6);
    }

    // ===== Price::scale_by tests =====

    #[test]
    fn price_scale_by_ratio() {
        let p = Price::from_f64(100.0).unwrap();
        let r = Ratio::new(0.5).unwrap();
        let result = p.scale_by(r);
        assert!((result.to_f64() - 50.0).abs() < 1e-6);
    }

    #[test]
    fn price_scale_by_negative_ratio() {
        let p = Price::from_f64(100.0).unwrap();
        let r = Ratio::new(-0.1).unwrap();
        let result = p.scale_by(r);
        assert!((result.to_f64() - (-10.0)).abs() < 1e-6);
    }

    // ===== Ratio invariant preservation =====

    #[test]
    fn ratio_ln_negative_returns_zero() {
        let r = Ratio::new(-1.0).unwrap();
        assert_eq!(r.ln().to_f64(), 0.0); // ln(-1) would be NaN; we return 0.0
    }

    #[test]
    fn ratio_ln_zero_returns_zero() {
        assert_eq!(Ratio::ZERO.ln().to_f64(), 0.0); // ln(0) would be -Inf; we return 0.0
    }

    #[test]
    fn ratio_exp_overflow_clamps() {
        let r = Ratio::new(1000.0).unwrap();
        let result = r.exp();
        assert!(result.to_f64().is_finite()); // would be Inf; clamped to f64::MAX
        assert_eq!(result.to_f64(), f64::MAX);
    }

    #[test]
    fn ratio_powi_overflow_clamps() {
        let r = Ratio::new(1e200).unwrap();
        let result = r.powi(2);
        assert!(result.to_f64().is_finite()); // would be Inf; clamped
    }

    // ===== Ratio finite invariant on arithmetic =====

    #[test]
    fn ratio_add_overflow_clamps() {
        let big = Ratio::from_raw(f64::MAX);
        let result = big + big;
        assert!(result.to_f64().is_finite());
        assert_eq!(result.to_f64(), f64::MAX);
    }

    #[test]
    fn ratio_sub_overflow_clamps() {
        let big_pos = Ratio::from_raw(f64::MAX);
        let big_neg = Ratio::from_raw(f64::MIN);
        let result = big_neg - big_pos;
        assert!(result.to_f64().is_finite());
        assert_eq!(result.to_f64(), f64::MIN);
    }

    #[test]
    fn ratio_mul_overflow_clamps() {
        let big = Ratio::from_raw(f64::MAX);
        let two = Ratio::new(2.0).unwrap();
        let result = big * two;
        assert!(result.to_f64().is_finite());
        assert_eq!(result.to_f64(), f64::MAX);
    }

    #[test]
    fn ratio_div_overflow_clamps() {
        let big = Ratio::from_raw(f64::MAX);
        let tiny = Ratio::new(0.5).unwrap();
        let result = big / tiny;
        assert!(result.to_f64().is_finite());
        assert_eq!(result.to_f64(), f64::MAX);
    }

    #[test]
    fn ratio_add_assign_overflow_clamps() {
        let mut r = Ratio::from_raw(f64::MAX);
        r += Ratio::from_raw(f64::MAX);
        assert!(r.to_f64().is_finite());
    }

    #[test]
    fn ratio_sub_assign_overflow_clamps() {
        let mut r = Ratio::from_raw(f64::MIN);
        r -= Ratio::from_raw(f64::MAX);
        assert!(r.to_f64().is_finite());
    }

    // ===== Cross-type i128→i64 saturation =====

    #[test]
    fn cross_type_extreme_values_saturate() {
        let max_price = Price::from_raw(i64::MAX);
        let large_qty = Quantity::from_raw(i64::MAX);
        let money: Money = max_price * large_qty;
        // Should saturate to i64::MAX instead of wrapping
        assert_eq!(money.raw_value(), i64::MAX);
    }

    #[test]
    fn cross_type_negative_extreme_saturates() {
        let max_price = Price::from_raw(i64::MAX);
        let neg_qty = Quantity::from_raw(i64::MIN);
        let money: Money = max_price * neg_qty;
        // Should saturate to i64::MIN instead of wrapping
        assert_eq!(money.raw_value(), i64::MIN);
    }

    #[test]
    fn money_div_quantity_extreme_saturates() {
        let max_money = Money::from_raw(i64::MAX);
        let tiny_qty = Quantity::from_raw(1); // 0.000001 units
        let price: Price = max_money / tiny_qty;
        // Result would overflow i64; should saturate
        assert_eq!(price.raw_value(), i64::MAX);
    }

    // ===== Module-level SCALE =====

    #[test]
    fn scale_constant() {
        assert_eq!(SCALE, 1_000_000);
        assert_eq!(Price::SCALE, SCALE);
        assert_eq!(Quantity::SCALE, SCALE);
        assert_eq!(Money::SCALE, SCALE);
    }
}
