"""JSON serialisation of the statement `Node` tree.

This is a presentation concern only: it does not touch `statement.view`, does
not change what any node's `value` *is*, and does not reinterpret the domain.
It decides how an already-computed `Node` is written onto the wire.
"""

from __future__ import annotations

from decimal import Decimal, InvalidOperation

from statement.view import Node

# Roles whose `value` is a monetary amount, per the Node contract in
# docs/STATEMENT.md ("issued_amount is the invoice total...", "value [is]
# its signed delta", "recorded demand", "invoice total as currently
# corrected"). Every other role's value is an identifier or a literal
# ("pending", a run id, a record id) and must be left alone.
_MONEY_ROLES = {"line", "issued_amount", "correction", "run", "amount_due"}


def _as_money(value: str) -> str:
    """Render `value` as an exact, two-decimal-place decimal string.

    `Node.value` for a money role is already `str(<some FloatField>)` (see
    statement/view.py), so as text it can read "24.0" rather than "24.00", or
    carry binary-float noise (e.g. "20.099999999999998"). Routing it through
    `Decimal` -- never `float()` -- fixes how that number is *printed* here
    without touching the model field or the domain calculation that produced
    it. If a value ever fails to parse, the original string is returned
    unchanged rather than raising: this layer is read-only and must not turn
    an unexpected upstream value into a 500.
    """
    try:
        return str(Decimal(value).quantize(Decimal("0.01")))
    except InvalidOperation:
        return value


def serialize_node(node: Node) -> dict:
    """Recursively serialise a `Node` into a JSON-safe dict.

    Shape: {"role": str, "label": str|None, "value": str|None, "ref": str|None,
    "collapsed": bool, "children": [...]}. `value` is always a `str` or
    `None` -- never a `float` -- because `Node.value` is typed `str | None`
    upstream and every value at this layer stays a Python `str`, which the
    JSON encoder writes as a quoted string, never a bare number.
    """
    value = node.value
    if value is not None and node.role in _MONEY_ROLES:
        value = _as_money(value)
    return {
        "role": node.role,
        "label": node.label or None,
        "value": value,
        "ref": node.ref,
        "collapsed": node.collapsed,
        "children": [serialize_node(child) for child in node.children],
    }
