"""Precision-safe value types for trading: Price, Quantity, Money.

These types enforce data integrity at the boundary — rejecting NaN, Infinity,
negative quantities, and arithmetic that would silently overflow. Backed by
Python's Decimal for exact representation of financial values.
"""

from __future__ import annotations

import math
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
from typing import Union

_Numeric = Union[int, float, str, Decimal]


def _to_decimal(value: _Numeric, name: str) -> Decimal:
    """Convert a numeric input to Decimal, rejecting NaN and Infinity."""
    if isinstance(value, float):
        if math.isnan(value) or math.isinf(value):
            raise ValueError(f"{name} must be finite, got {value}")
        value = Decimal(str(value))
    elif isinstance(value, (int, str)):
        try:
            value = Decimal(str(value))
        except InvalidOperation as exc:
            raise ValueError(f"{name}: invalid numeric value {value!r}") from exc
    elif not isinstance(value, Decimal):
        raise TypeError(f"{name} must be numeric, got {type(value).__name__}")

    if value.is_nan() or value.is_infinite():
        raise ValueError(f"{name} must be finite, got {value}")
    return value


class Price:
    """Immutable, precision-safe price value.

    Rejects NaN, Infinity at construction. Supports arithmetic with other
    Price/Quantity/Money types with type-safe returns.
    """

    __slots__ = ("_value", "_precision")

    def __init__(self, value: _Numeric, precision: int | None = None) -> None:
        self._value = _to_decimal(value, "Price")
        if precision is not None:
            if precision < 0 or precision > 18:
                raise ValueError(f"precision must be 0-18, got {precision}")
            quant = Decimal(10) ** -precision
            self._value = self._value.quantize(quant, rounding=ROUND_HALF_UP)
            self._precision = precision
        else:
            self._precision = max(0, -self._value.as_tuple().exponent)

    @classmethod
    def from_str(cls, s: str, precision: int | None = None) -> "Price":
        return cls(s, precision=precision)

    @classmethod
    def from_float(cls, f: float, precision: int = 2) -> "Price":
        return cls(f, precision=precision)

    @property
    def value(self) -> Decimal:
        return self._value

    @property
    def precision(self) -> int:
        return self._precision

    def as_float(self) -> float:
        return float(self._value)

    def __repr__(self) -> str:
        return f"Price('{self._value}')"

    def __str__(self) -> str:
        return str(self._value)

    def __float__(self) -> float:
        return float(self._value)

    def __eq__(self, other: object) -> bool:
        if isinstance(other, Price):
            return self._value == other._value
        if isinstance(other, (int, float, Decimal)):
            return self._value == _to_decimal(other, "compare")
        return NotImplemented

    def __lt__(self, other: object) -> bool:
        if isinstance(other, Price):
            return self._value < other._value
        if isinstance(other, (int, float, Decimal)):
            return self._value < _to_decimal(other, "compare")
        return NotImplemented

    def __le__(self, other: object) -> bool:
        if isinstance(other, Price):
            return self._value <= other._value
        if isinstance(other, (int, float, Decimal)):
            return self._value <= _to_decimal(other, "compare")
        return NotImplemented

    def __gt__(self, other: object) -> bool:
        if isinstance(other, Price):
            return self._value > other._value
        if isinstance(other, (int, float, Decimal)):
            return self._value > _to_decimal(other, "compare")
        return NotImplemented

    def __ge__(self, other: object) -> bool:
        if isinstance(other, Price):
            return self._value >= other._value
        if isinstance(other, (int, float, Decimal)):
            return self._value >= _to_decimal(other, "compare")
        return NotImplemented

    def __hash__(self) -> int:
        return hash(self._value)

    def __add__(self, other: object) -> "Price":
        if isinstance(other, Price):
            return Price(self._value + other._value)
        if isinstance(other, (int, float, Decimal)):
            return Price(self._value + _to_decimal(other, "add"))
        return NotImplemented

    def __sub__(self, other: object) -> "Price":
        if isinstance(other, Price):
            return Price(self._value - other._value)
        if isinstance(other, (int, float, Decimal)):
            return Price(self._value - _to_decimal(other, "sub"))
        return NotImplemented

    def __mul__(self, other: object) -> "Money":
        if isinstance(other, Quantity):
            return Money(self._value * other._value)
        if isinstance(other, (int, float, Decimal)):
            return Money(self._value * _to_decimal(other, "mul"))
        return NotImplemented

    def __neg__(self) -> "Price":
        return Price(-self._value)

    def __abs__(self) -> "Price":
        return Price(abs(self._value))

    def __bool__(self) -> bool:
        return self._value != 0


class Quantity:
    """Immutable, non-negative quantity value.

    Rejects NaN, Infinity, and negative values at construction.
    """

    __slots__ = ("_value", "_precision")

    def __init__(self, value: _Numeric, precision: int | None = None) -> None:
        self._value = _to_decimal(value, "Quantity")
        if self._value < 0:
            raise ValueError(f"Quantity cannot be negative, got {self._value}")
        if precision is not None:
            if precision < 0 or precision > 18:
                raise ValueError(f"precision must be 0-18, got {precision}")
            quant = Decimal(10) ** -precision
            self._value = self._value.quantize(quant, rounding=ROUND_HALF_UP)
            self._precision = precision
        else:
            self._precision = max(0, -self._value.as_tuple().exponent)

    @classmethod
    def from_int(cls, n: int) -> "Quantity":
        return cls(n, precision=0)

    @classmethod
    def from_float(cls, f: float, precision: int = 2) -> "Quantity":
        return cls(f, precision=precision)

    @property
    def value(self) -> Decimal:
        return self._value

    @property
    def precision(self) -> int:
        return self._precision

    def as_float(self) -> float:
        return float(self._value)

    def __repr__(self) -> str:
        return f"Quantity('{self._value}')"

    def __str__(self) -> str:
        return str(self._value)

    def __float__(self) -> float:
        return float(self._value)

    def __int__(self) -> int:
        return int(self._value)

    def __eq__(self, other: object) -> bool:
        if isinstance(other, Quantity):
            return self._value == other._value
        if isinstance(other, (int, float, Decimal)):
            return self._value == _to_decimal(other, "compare")
        return NotImplemented

    def __lt__(self, other: object) -> bool:
        if isinstance(other, Quantity):
            return self._value < other._value
        if isinstance(other, (int, float, Decimal)):
            return self._value < _to_decimal(other, "compare")
        return NotImplemented

    def __le__(self, other: object) -> bool:
        if isinstance(other, Quantity):
            return self._value <= other._value
        if isinstance(other, (int, float, Decimal)):
            return self._value <= _to_decimal(other, "compare")
        return NotImplemented

    def __gt__(self, other: object) -> bool:
        if isinstance(other, Quantity):
            return self._value > other._value
        if isinstance(other, (int, float, Decimal)):
            return self._value > _to_decimal(other, "compare")
        return NotImplemented

    def __ge__(self, other: object) -> bool:
        if isinstance(other, Quantity):
            return self._value >= other._value
        if isinstance(other, (int, float, Decimal)):
            return self._value >= _to_decimal(other, "compare")
        return NotImplemented

    def __hash__(self) -> int:
        return hash(self._value)

    def __add__(self, other: object) -> "Quantity":
        if isinstance(other, Quantity):
            return Quantity(self._value + other._value)
        if isinstance(other, (int, float, Decimal)):
            return Quantity(self._value + _to_decimal(other, "add"))
        return NotImplemented

    def __sub__(self, other: object) -> "Quantity":
        if isinstance(other, Quantity):
            return Quantity(self._value - other._value)
        if isinstance(other, (int, float, Decimal)):
            return Quantity(self._value - _to_decimal(other, "sub"))
        return NotImplemented

    def __mul__(self, other: object) -> "Quantity":
        if isinstance(other, (int, float, Decimal)):
            return Quantity(self._value * _to_decimal(other, "mul"))
        return NotImplemented

    def __bool__(self) -> bool:
        return self._value != 0


class Money:
    """Immutable monetary value with currency tracking.

    Supports both positive and negative amounts (for P&L, debits, etc.).
    """

    __slots__ = ("_value", "_currency")

    def __init__(self, value: _Numeric, currency: str = "USD") -> None:
        self._value = _to_decimal(value, "Money")
        self._currency = currency.upper()

    @classmethod
    def zero(cls, currency: str = "USD") -> "Money":
        return cls(0, currency)

    @property
    def value(self) -> Decimal:
        return self._value

    @property
    def currency(self) -> str:
        return self._currency

    def as_float(self) -> float:
        return float(self._value)

    def __repr__(self) -> str:
        return f"Money('{self._value}', '{self._currency}')"

    def __str__(self) -> str:
        return f"{self._value} {self._currency}"

    def __float__(self) -> float:
        return float(self._value)

    def __eq__(self, other: object) -> bool:
        if isinstance(other, Money):
            return self._value == other._value and self._currency == other._currency
        if isinstance(other, (int, float, Decimal)):
            return self._value == _to_decimal(other, "compare")
        return NotImplemented

    def __lt__(self, other: object) -> bool:
        if isinstance(other, Money):
            if self._currency != other._currency:
                raise ValueError(f"Cannot compare {self._currency} with {other._currency}")
            return self._value < other._value
        if isinstance(other, (int, float, Decimal)):
            return self._value < _to_decimal(other, "compare")
        return NotImplemented

    def __le__(self, other: object) -> bool:
        if isinstance(other, Money):
            if self._currency != other._currency:
                raise ValueError(f"Cannot compare {self._currency} with {other._currency}")
            return self._value <= other._value
        if isinstance(other, (int, float, Decimal)):
            return self._value <= _to_decimal(other, "compare")
        return NotImplemented

    def __gt__(self, other: object) -> bool:
        if isinstance(other, Money):
            if self._currency != other._currency:
                raise ValueError(f"Cannot compare {self._currency} with {other._currency}")
            return self._value > other._value
        if isinstance(other, (int, float, Decimal)):
            return self._value > _to_decimal(other, "compare")
        return NotImplemented

    def __ge__(self, other: object) -> bool:
        if isinstance(other, Money):
            if self._currency != other._currency:
                raise ValueError(f"Cannot compare {self._currency} with {other._currency}")
            return self._value >= other._value
        if isinstance(other, (int, float, Decimal)):
            return self._value >= _to_decimal(other, "compare")
        return NotImplemented

    def __hash__(self) -> int:
        return hash((self._value, self._currency))

    def __add__(self, other: object) -> "Money":
        if isinstance(other, Money):
            if self._currency != other._currency:
                raise ValueError(f"Cannot add {self._currency} and {other._currency}")
            return Money(self._value + other._value, self._currency)
        if isinstance(other, (int, float, Decimal)):
            return Money(self._value + _to_decimal(other, "add"), self._currency)
        return NotImplemented

    def __sub__(self, other: object) -> "Money":
        if isinstance(other, Money):
            if self._currency != other._currency:
                raise ValueError(f"Cannot subtract {self._currency} and {other._currency}")
            return Money(self._value - other._value, self._currency)
        if isinstance(other, (int, float, Decimal)):
            return Money(self._value - _to_decimal(other, "sub"), self._currency)
        return NotImplemented

    def __mul__(self, other: object) -> "Money":
        if isinstance(other, (int, float, Decimal)):
            return Money(self._value * _to_decimal(other, "mul"), self._currency)
        return NotImplemented

    def __neg__(self) -> "Money":
        return Money(-self._value, self._currency)

    def __abs__(self) -> "Money":
        return Money(abs(self._value), self._currency)

    def __bool__(self) -> bool:
        return self._value != 0
