"""``code-py-ast@v1`` π* — Python source → canonical AST S-expression.

Domain: ``code``. First non-text canonical projection registered in
arborist. Activates the cross-modality discipline that ticket
#000015 spelled out — until now it was only theoretical (text +
claim_lattice + memory carriers).

Algorithm
---------
1. Parse ``raw`` UTF-8 bytes via stdlib :mod:`ast`.
2. Walk every node, emit a deterministic S-expression of the form
   ``(NodeType field1=val1 field2=val2 ...)`` where:
   - ``_fields`` are walked in **lexical order** (stable across
     Python versions).
   - Source positions (``lineno``, ``col_offset``,
     ``end_lineno``, ``end_col_offset``, ``type_comment``) are NOT
     in ``_fields`` so they're naturally excluded.
   - Lists serialize as ``[elem0 elem1 ...]``.
   - Constants serialize via :func:`repr` (Python guarantees
     deterministic repr for primitives).
3. Result is UTF-8-encoded bytes.

Equivalence classes preserved
-----------------------------
- Whitespace + indentation differences (don't enter AST).
- Comments (don't enter AST).
- String quote style (``'x'`` vs ``"x"`` parse to identical
  ``Constant`` nodes).
- Numeric literal formatting (``1`` vs ``01_000`` collapse to
  identical ``Constant`` value, modulo Python parser).
- Trailing semicolons.

Equivalence classes kept distinct
---------------------------------
- Identifier names (``Name.id`` differs).
- Operator types (``Add`` vs ``Sub`` distinct ``BinOp.op`` nodes).
- Argument order in calls (positional order is semantic).

Round-trip property
-------------------
This π* is **projective**, not invertible — canonical bytes are an
S-expression, NOT valid Python source. Re-canonicalizing the
canonical output is undefined (the S-expression isn't Python). v1
runners that need round-trip stability test on the *original raw*
input only.

Versioning
----------
``code-py-ast@v1`` pins this algorithm. Any change (sort order,
field exclusion, list bracket style, literal repr) requires a
new version number — never mutate v1 in place.

Source: ticket #000015 §2.1 declared the stub; this commit lands
the first real non-text canonicalizer.
"""

from __future__ import annotations

import ast
from dataclasses import dataclass

from arborist.pi_star.protocol import PiStarError
from arborist.pi_star.registry import register


@dataclass
class CodePyAstV1:
    name: str = "code-py-ast"
    version: str = "v1"
    domain: str = "code"

    def canonicalize(self, raw: bytes) -> bytes:
        if not isinstance(raw, (bytes, bytearray)):
            raise PiStarError(
                "code-py-ast@v1 expects bytes; got "
                f"{type(raw).__name__}"
            )
        try:
            source = raw.decode("utf-8", errors="surrogatepass")
        except UnicodeDecodeError as exc:  # pragma: no cover — defensive
            raise PiStarError(f"input not valid UTF-8: {exc}") from exc
        try:
            tree = ast.parse(source)
        except SyntaxError as exc:
            raise PiStarError(
                f"code-py-ast@v1 input is not valid Python: {exc}"
            ) from exc
        return _canonical_sexp(tree).encode("utf-8", errors="surrogatepass")


def _canonical_sexp(node) -> str:
    """Walk an AST; emit a deterministic S-expression. Field order
    is lexical so the encoding is stable regardless of how the AST
    constructor populated attributes."""
    if isinstance(node, ast.AST):
        cls_name = type(node).__name__
        # Walk _fields in lexical order. ast nodes set this tuple at
        # class-definition time; using sorted() makes us robust if
        # Python ever reorders.
        parts = []
        for fname in sorted(node._fields):
            fval = getattr(node, fname, None)
            parts.append(f"{fname}={_canonical_sexp(fval)}")
        return f"({cls_name} {' '.join(parts)})" if parts else f"({cls_name})"
    if isinstance(node, list):
        return "[" + " ".join(_canonical_sexp(x) for x in node) + "]"
    if node is None:
        return "None"
    if isinstance(node, bool):
        # bool BEFORE int (bool is a subclass of int).
        return "True" if node else "False"
    if isinstance(node, (int, float, complex)):
        return repr(node)
    if isinstance(node, str):
        return repr(node)
    if isinstance(node, bytes):
        return repr(node)
    # Catch-all for anything we didn't anticipate. repr() is
    # deterministic for stdlib types.
    return repr(node)


register(CodePyAstV1())
