"""Money as canonical decimal STRINGS, with the ISO-4217 minor-units table PINNED in code.

Two rules, and both exist because a hash is only reproducible if the bytes are:

  * **No floats, ever.** `12500.00` is a string. A float would make the canonical bytes
    depend on the platform's IEEE-754 formatting, which would make the record hash
    non-reproducible on the counterparty's machine — and NEOedge's whole premise is that
    two independently-running edges produce comparable records.
  * **Exactly the currency's minor units.** EUR carries two decimals, JPY zero, BHD three.
    `"250000.0"` is not EUR and `"5000.00"` is not JPY. Without this rule two edges could
    write the same amount two ways and a formatting difference would be indistinguishable
    from a disagreement — which is exactly the failure this product exists to prevent.

The table is a **versioned constant**, never a runtime lookup: an external table that
changes under us would silently change what bytes we produce. Same discipline as the
Ledger's `MINOR_UNITS_VERSION`.
"""

from __future__ import annotations

import re
from decimal import Decimal, InvalidOperation

MINOR_UNITS_VERSION = "ISO4217-2026-07.v1"

# Only the exceptions are listed; everything not named here has two minor units.
# Pinned deliberately: adding a currency is a reviewed change, not a network call.
_MINOR_UNITS_EXCEPTIONS: dict[str, int] = {
    "BIF": 0, "CLP": 0, "DJF": 0, "GNF": 0, "ISK": 0, "JPY": 0, "KMF": 0, "KRW": 0,
    "PYG": 0, "RWF": 0, "UGX": 0, "UYI": 0, "VND": 0, "VUV": 0, "XAF": 0, "XOF": 0,
    "XPF": 0,
    "BHD": 3, "IQD": 3, "JOD": 3, "KWD": 3, "LYD": 3, "OMR": 3, "TND": 3,
    "CLF": 4, "UYW": 4,
}
_DEFAULT_MINOR_UNITS = 2

_CURRENCY_RE = re.compile(r"^[A-Z]{3}$")


class MoneyError(ValueError):
    """An amount or currency is not canonical. Fail-closed — never coerced, never rounded."""


def minor_units(currency: str) -> int:
    """Minor units for an ISO-4217 alpha-3 code, from the pinned table."""
    check_currency(currency)
    return _MINOR_UNITS_EXCEPTIONS.get(currency, _DEFAULT_MINOR_UNITS)


def check_currency(currency: str) -> str:
    if not isinstance(currency, str) or not _CURRENCY_RE.match(currency):
        raise MoneyError(f"currency must be ISO-4217 alpha-3 uppercase, got {currency!r}")
    return currency


def check_amount(amount: str, currency: str) -> str:
    """Validate a canonical amount string against its currency. Returns it unchanged.

    Always positive: direction is carried by the document kind (a credit memo reverses,
    a payment flows out), never by a sign inside the number. A sign in the string would
    give two ways to write the same fact.
    """
    m = minor_units(currency)
    if not isinstance(amount, str):
        raise MoneyError(f"amount must be a canonical decimal STRING, got {type(amount).__name__}")
    pattern = r"^(0|[1-9][0-9]*)$" if m == 0 else r"^(0|[1-9][0-9]*)\.[0-9]{%d}$" % m
    if not re.match(pattern, amount):
        expected = "no decimal point" if m == 0 else f"exactly {m} decimals"
        raise MoneyError(
            f"amount {amount!r} is not canonical for {currency} ({expected}, no thousands "
            f"separators, no leading zeros, no sign)"
        )
    return amount


def to_minor(amount: str, currency: str) -> int:
    """Exact integer minor units — the only form arithmetic is ever done in."""
    check_amount(amount, currency)
    return int(amount.replace(".", ""))


def from_minor(value: int, currency: str) -> str:
    """Format integer minor units back to the canonical string for `currency`."""
    if not isinstance(value, int) or isinstance(value, bool) or value < 0:
        raise MoneyError(f"minor-unit value must be a non-negative int, got {value!r}")
    m = minor_units(currency)
    if m == 0:
        return str(value)
    digits = str(value).rjust(m + 1, "0")
    return f"{digits[:-m]}.{digits[-m:]}"


def equal(a: str, b: str, currency: str) -> bool:
    """Exact comparison in minor units. Never a float subtraction, never a tolerance."""
    return to_minor(a, currency) == to_minor(b, currency)


def parse_native(value, currency: str) -> str:
    """Convert an amount as an ERP handed it over into the canonical string.

    Adapters call this, and only adapters. It accepts what real systems emit — an int in
    minor units, a `Decimal`, a plain numeric string with the right scale — and refuses a
    float outright. A float that reached here already lost precision somewhere upstream;
    quantizing it would hide that loss behind a canonical-looking string.
    """
    m = minor_units(currency)
    if isinstance(value, bool):
        raise MoneyError("a bool is not an amount")
    if isinstance(value, float):
        raise MoneyError(
            "a float is not an accepted amount — read the ERP field as a string or as "
            "integer minor units; a float has already lost precision by the time it is here"
        )
    if isinstance(value, int):
        return from_minor(value, currency)
    if isinstance(value, Decimal):
        text = format(value, "f")
    elif isinstance(value, str):
        text = value.strip()
    else:
        raise MoneyError(f"cannot read an amount from {type(value).__name__}")
    try:
        dec = Decimal(text)
    except InvalidOperation as exc:
        raise MoneyError(f"not a decimal amount: {text!r}") from exc
    if dec < 0:
        raise MoneyError(f"amounts are always positive; direction lives in the document kind: {text!r}")
    quantum = Decimal(1).scaleb(-m)
    if dec != dec.quantize(quantum):
        raise MoneyError(
            f"{text!r} carries more precision than {currency} has minor units ({m}) — "
            "refusing to round an ERP amount"
        )
    return check_amount(format(dec.quantize(quantum), "f"), currency)
