from decimal import ROUND_DOWN, Decimal, getcontext
from typing import Optional, TypeVar, Union

import keble_exceptions
from pydantic import BaseModel

# Set precision for decimal calculations
getcontext().prec = 28

T = TypeVar("T", bound="StdMoneyAmount")
P = TypeVar("P", bound="StdPercentage")

Number = Union[int, float, Decimal]


def _to_decimal(value: Number) -> Decimal:
    if isinstance(value, Decimal):
        return value
    if isinstance(value, float):
        return Decimal(str(value))
    return Decimal(value)


def _decimal_fmod(dividend: Decimal, divisor: Decimal) -> Decimal:
    if divisor == 0:
        raise ZeroDivisionError("Modulo by zero")

    quotient = (dividend / divisor).to_integral_value(rounding=ROUND_DOWN)
    return dividend - (quotient * divisor)


class StdMoneyAmount(BaseModel):
    value: int

    def __init__(self, value: int):
        super().__init__(value=value)

    def __add__(self, other):
        if isinstance(other, StdMoneyAmount):
            return StdMoneyAmount(value=self.value + other.value)
        elif isinstance(other, (int, float, Decimal)):
            return StdMoneyAmount(value=self.value + int(_to_decimal(other)))
        return NotImplemented

    def __sub__(self, other):
        if isinstance(other, StdMoneyAmount):
            return StdMoneyAmount(value=self.value - other.value)
        elif isinstance(other, (int, float, Decimal)):
            return StdMoneyAmount(value=self.value - int(_to_decimal(other)))
        return NotImplemented

    def __mul__(self, other):
        if isinstance(other, (int, float, Decimal)):
            result = _to_decimal(self.value) * _to_decimal(other)
            return StdMoneyAmount(value=int(result))
        return NotImplemented

    def __truediv__(self, other):
        if isinstance(other, StdMoneyAmount):
            # Dividing two StdMoneyAmount values gives their ratio
            if other.value == 0:
                raise ZeroDivisionError("Division by zero")
            return float(_to_decimal(self.value) / _to_decimal(other.value))
        elif isinstance(other, (int, float, Decimal)):
            other_decimal = _to_decimal(other)
            if other_decimal == 0:
                raise ZeroDivisionError("Division by zero")
            result = _to_decimal(self.value) / other_decimal
            return StdMoneyAmount(value=int(result))
        return NotImplemented

    def __mod__(self, other):
        if isinstance(other, (int, float, Decimal)):
            divisor = _to_decimal(other)
            result = _decimal_fmod(_to_decimal(self.value) / Decimal("100"), divisor)
            return float(result)
        return NotImplemented

    def __floordiv__(self, other):
        if isinstance(other, (int, float, Decimal)):
            other_decimal = _to_decimal(other)
            if other_decimal == 0:
                raise ZeroDivisionError("Division by zero")
            result = _to_decimal(self.value) // other_decimal
            return StdMoneyAmount(value=int(result))
        return NotImplemented

    def __pow__(self, other):
        if isinstance(other, (int, float, Decimal)):
            other_decimal = _to_decimal(other)
            result = _to_decimal(self.value) ** other_decimal
            return StdMoneyAmount(value=int(result))
        return NotImplemented

    def __neg__(self):
        return StdMoneyAmount(value=-self.value)

    def __abs__(self):
        return StdMoneyAmount(value=abs(self.value))

    def __lt__(self, other):
        if isinstance(other, StdMoneyAmount):
            return self.value < other.value
        elif isinstance(other, (int, float, Decimal)):
            return self.value < int(other)
        return NotImplemented

    def __le__(self, other):
        if isinstance(other, StdMoneyAmount):
            return self.value <= other.value
        elif isinstance(other, (int, float, Decimal)):
            return self.value <= int(other)
        return NotImplemented

    def __gt__(self, other):
        if isinstance(other, StdMoneyAmount):
            return self.value > other.value
        elif isinstance(other, (int, float, Decimal)):
            return self.value > int(other)
        return NotImplemented

    def __ge__(self, other):
        if isinstance(other, StdMoneyAmount):
            return self.value >= other.value
        elif isinstance(other, (int, float, Decimal)):
            return self.value >= int(other)
        return NotImplemented

    def __eq__(self, other):
        if isinstance(other, StdMoneyAmount):
            return self.value == other.value
        elif isinstance(other, (int, float, Decimal)):
            return self.value == int(other)
        return NotImplemented

    def __ne__(self, other):
        if isinstance(other, StdMoneyAmount):
            return self.value != other.value
        elif isinstance(other, (int, float, Decimal)):
            return self.value != int(other)
        return NotImplemented

    def __str__(self):
        return f"{self.value}"

    def __repr__(self):
        return f"MoneyAmount(value={self.value})"

    @property
    def true_value(self) -> float:
        # 10000 -> 100 USD, 10077 -> 100.77 USD
        # Return float for compatibility with existing tests
        return float(Decimal(self.value) / Decimal("100"))

    @classmethod
    def from_true_value(cls: type[T], *, float_money: float) -> T:
        decimal_money = _to_decimal(float_money)
        return cls(value=int(decimal_money * Decimal("100")))


class StdPercentage(BaseModel):
    value: int  # Represent 100% as 10000

    def __init__(self, value: int):
        super().__init__(value=value)

    def __add__(self, other):
        if isinstance(other, StdPercentage):
            return StdPercentage(value=self.value + other.value)
        elif isinstance(other, (int, float, Decimal)):
            other_decimal = _to_decimal(other)
            return StdPercentage(
                value=self.value + int(other_decimal * Decimal("10000"))
            )
        return NotImplemented

    def __sub__(self, other):
        if isinstance(other, StdPercentage):
            return StdPercentage(value=self.value - other.value)
        elif isinstance(other, (int, float, Decimal)):
            other_decimal = _to_decimal(other)
            return StdPercentage(
                value=self.value - int(other_decimal * Decimal("10000"))
            )
        return NotImplemented

    def __mul__(self, other):
        if isinstance(other, (int, float, Decimal)):
            other_decimal = _to_decimal(other)
            result = _to_decimal(self.value) * other_decimal
            return StdPercentage(value=int(result))
        return NotImplemented

    def __truediv__(self, other):
        if isinstance(other, (int, float, Decimal)):
            other_decimal = _to_decimal(other)
            if other_decimal == 0:
                raise ZeroDivisionError("Division by zero")
            result = _to_decimal(self.value) / other_decimal
            return StdPercentage(value=int(result))
        return NotImplemented

    def __mod__(self, other):
        if isinstance(other, (int, float, Decimal)):
            divisor_bps = _to_decimal(other)
            result_percent = _decimal_fmod(
                _to_decimal(self.value) / Decimal("100"),
                divisor_bps / Decimal("100"),
            )
            return float(result_percent)
        return NotImplemented

    def __floordiv__(self, other):
        if isinstance(other, (int, float, Decimal)):
            other_decimal = _to_decimal(other)
            if other_decimal == 0:
                raise ZeroDivisionError("Division by zero")
            result = _to_decimal(self.value) // other_decimal
            return StdPercentage(value=int(result))
        return NotImplemented

    def __pow__(self, other):
        if isinstance(other, (int, float, Decimal)):
            other_decimal = (
                Decimal(str(other)) if not isinstance(other, Decimal) else other
            )
            result = Decimal(self.value) ** other_decimal
            return StdPercentage(value=int(result))
        return NotImplemented

    def __neg__(self):
        return StdPercentage(value=-self.value)

    def __abs__(self):
        return StdPercentage(value=abs(self.value))

    def __lt__(self, other):
        if isinstance(other, StdPercentage):
            return self.value < other.value
        elif isinstance(other, (int, float, Decimal)):
            return self.value < int(_to_decimal(other) * Decimal("10000"))
        return NotImplemented

    def __le__(self, other):
        if isinstance(other, StdPercentage):
            return self.value <= other.value
        elif isinstance(other, (int, float, Decimal)):
            return self.value <= int(_to_decimal(other) * Decimal("10000"))
        return NotImplemented

    def __gt__(self, other):
        if isinstance(other, StdPercentage):
            return self.value > other.value
        elif isinstance(other, (int, float, Decimal)):
            return self.value > int(_to_decimal(other) * Decimal("10000"))
        return NotImplemented

    def __ge__(self, other):
        if isinstance(other, StdPercentage):
            return self.value >= other.value
        elif isinstance(other, (int, float, Decimal)):
            return self.value >= int(_to_decimal(other) * Decimal("10000"))
        return NotImplemented

    def __eq__(self, other):
        if isinstance(other, StdPercentage):
            return self.value == other.value
        elif isinstance(other, (int, float, Decimal)):
            return self.value == int(_to_decimal(other) * Decimal("10000"))
        return NotImplemented

    def __ne__(self, other):
        if isinstance(other, StdPercentage):
            return self.value != other.value
        elif isinstance(other, (int, float, Decimal)):
            return self.value != int(_to_decimal(other) * Decimal("10000"))
        return NotImplemented

    def __str__(self):
        decimal_value = (_to_decimal(self.value) / Decimal("100")).quantize(
            Decimal("0.01")
        )
        return f"{decimal_value}%"

    def __repr__(self):
        return f"Percentage(value={self.value})"

    @property
    def true_value(self) -> float:
        # 100% == 10000 integer,
        # or 1 in float == 10000 integer,
        # 50% == 5000 integer
        # or 0.5 in float == 5000 integer
        # Return float for compatibility with existing tests
        return float(_to_decimal(self.value) / Decimal("10000"))

    @classmethod
    def from_true_value(
        cls: type[P],
        *,
        float_percent: Optional[float] = None,
        string_percent: Optional[str] = None,
    ) -> P:
        if float_percent is not None:
            decimal_percent = _to_decimal(float_percent)
            return cls(value=int(decimal_percent * Decimal("10000")))
        elif string_percent is not None:
            decimal_percent = Decimal(string_percent.strip().rstrip("%"))
            return cls(value=int(decimal_percent * Decimal("100")))
        else:
            raise keble_exceptions.ServerSideMissingParams(
                alert_admin=True, missing_params="float_percent or string_percent"
            )

    @property
    def string(self) -> str:
        decimal_value = (_to_decimal(self.value) / Decimal("100")).quantize(
            Decimal("0.01")
        )
        _str = format(decimal_value, "f")
        if "." in _str:
            _str = _str.rstrip("0").rstrip(".")
        return f"{_str}%"

    @property
    def four_digits_string(self) -> str:
        """non stripped version of string"""
        decimal_value = (_to_decimal(self.value) / Decimal("100")).quantize(
            Decimal("0.01")
        )
        return f"{decimal_value}%"
