import decimal as dec
import typing as t

import urnparse

DECIMABLE: t.TypeAlias = int | str | dec.Decimal

_100 = dec.Decimal(100)


class Percent(dec.Decimal):
    """
    Decimal-based percentage value (in percentage points).

    Percent wraps ``decimal.Decimal`` to represent values like ``25%`` as the
    numeric value ``25`` (percentage points). It provides helpers to convert to
    and from fractional ratios and restricts arithmetic to reduce type errors.

    Notes
    -----
    - Construction accepts any type ``Decimal`` does (e.g. ``int``, ``str``,
      ``Decimal``). There is currently no range enforcement; negative values or
      values above 100 are allowed.
    - Prefer passing strings (e.g. ``"12.5"``) or ``Decimal`` objects rather
      than floats to avoid binary floating point artifacts.

    Examples
    --------
    >>> from kit_up.tools.types.numeric import Percent
    >>> p = Percent(25)
    >>> str(p)
    '25%'
    >>> repr(p)
    'Percent(25)'

    Create from a fractional ratio (0–1) via ``from_ratio``:
    >>> Percent.from_ratio('0.25')
    Percent(25)
    >>> Percent.from_ratio('0.075')
    Percent(7.5)

    Convert back to a fractional ratio using ``ratio``/``as_fraction``:
    >>> Percent('12.5').ratio
    Decimal('0.125')
    >>> Percent(40).as_fraction()
    Decimal('0.4')

    Arithmetic: add/subtract only with ``Percent``; multiply/divide by scalars:
    >>> Percent('12.5') + Percent(2.5)
    Percent(15)
    >>> Percent(50) - Percent(12)
    Percent(38)
    >>> Percent(10) * 3
    Percent(30)
    >>> Percent(10) / 4
    Percent(2.5)

    Invalid operations raise ``TypeError``:
    >>> Percent(10) + 5
    Traceback (most recent call last):
    ...
    TypeError: Can only add Percent to Percent.

    """

    def __new__(cls, value: DECIMABLE, context=None) -> dec.Decimal:
        # TODO: >= 0
        # if not (0 <= value <= 100):
        #     raise ValueError(f"Percent must be between 0 and 1: {value}")
        if isinstance(value, str) and value.endswith("%"):
            value = value.rstrip("%")
        return super().__new__(cls, value, context)

    # --- URN helpers ---

    def as_urn(self) -> urnparse.URN8141:
        """
        Return a URN-encoded representation of the percent value.

        Format: ``urn:percent:<value>`` where ``<value>`` is the decimal
        percentage points, without the trailing ``%``.

        Examples
        --------
        >>> Percent('12.5').as_urn().to_string()
        'urn:percent:12.5'
        >>> Percent(0).as_urn().to_string()
        'urn:percent:0'

        """
        numeric = super().__str__()  # raw Decimal string without '%'
        return urnparse.URN8141.from_string(f"urn:percent:{numeric}")

    @classmethod
    def from_urn(cls, urn: str | urnparse.URN8141) -> t.Self:
        """
        Create a Percent from a URN.

        Accepts either a string or a parsed ``URN8141``. Expects namespace
        identifier ``percent`` and uses the NSS as the decimal value.

        Examples
        --------
        >>> Percent.from_urn('urn:percent:7.5')
        Percent(7.5)
        >>> u = urnparse.URN8141.from_string('urn:percent:42')
        >>> Percent.from_urn(u)
        Percent(42)

        """
        # Normalize to string form to avoid relying on library internals
        if isinstance(urn, urnparse.URN8141):
            s = urn.to_string() if hasattr(urn, "to_string") else str(urn)
        else:
            s = str(urn)

        if not s.startswith("urn:"):
            raise ValueError(f"Invalid URN: {s!r}")

        # 'urn:<nid>:<nss>' — keep full NSS even if it contains colons
        try:
            _, nid, nss = s.split(":", 2)
        except ValueError as e:  # not enough parts
            raise ValueError(f"Invalid URN format: {s!r}") from e

        if nid != "percent":
            raise ValueError(f"Unsupported URN namespace for Percent: {nid}")

        value_str = nss
        return cls(value=value_str)

    @classmethod
    def from_ratio(cls, value: DECIMABLE, context=None) -> t.Self:
        # TODO: >= 0
        # if not (0 <= value <= 1):
        #     raise ValueError(f"Ratio must be between 0 and 1: {value}")
        return cls(value=dec.Decimal(value) * _100, context=context)

    @property
    def ratio(self):
        return self / _100

    def apply(self, value: DECIMABLE) -> dec.Decimal:
        """
        Apply this percent to a numeric value.

        Multiplies the given ``value`` by this percent and divides by 100,
        returning a ``Decimal`` for precision.

        Examples
        --------
        >>> Percent(25).apply(200)
        Decimal('50')
        >>> Percent('12.5').apply('200')
        Decimal('25')

        """
        base = dec.Decimal(value)
        # Use Decimal arithmetic directly to avoid returning a Percent here.
        return base * (dec.Decimal(self) / _100)

    # --- Arithmetic: only with Percent ---

    def __add__(self, other):
        if not isinstance(other, Percent):
            raise TypeError("Can only add Percent to Percent.")
        result = super().__add__(other)
        # return Percent(self + other)
        return Percent(result)

    def __sub__(self, other):
        if not isinstance(other, Percent):
            raise TypeError("Can only subtract Percent from Percent.")
        result = super().__sub__(other)
        return Percent(result)

    # Multiplication/division allowed with numeric scalars
    def __mul__(self, other):
        if isinstance(other, (int, float, dec.Decimal)):
            return Percent(super().__mul__(dec.Decimal(other)))
        raise TypeError("Can only multiply Percent by a numeric scalar.")

    def __truediv__(self, other):
        if isinstance(other, (int, float, dec.Decimal)):
            return Percent(super().__truediv__(dec.Decimal(other)))
        raise TypeError("Can only divide Percent by a numeric scalar.")

    # --- Display helpers ---

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

    def __repr__(self):
        return f"{self.__class__.__name__}({super().__str__()})"

    def as_fraction(self):
        """Return fractional equivalent (0–1 range)."""
        return self / _100
