"""``algebra-symbolic@v1`` π* — symbolic-algebra canonicalizer.

Domain: ``symbolic-algebra``. Implements the symbolic layer above
``arithmetic@v1`` (which is closed-form rational arithmetic with no
identifiers): parse an algebraic expression with letters via SymPy,
expand it polynomially, and serialize as a deterministic
S-expression string.

Why this matters
----------------
Ticket #000030 §1: arborist's math substrate covers closed-form
arithmetic (``arithmetic@v1``) and propositional logic
(``logic-kernel@v1``) but has no symbolic-algebra layer. Questions
shaped like ``simplify (x+1)**2`` or ``expand x*(x-1)`` fall through
to RAG even though they have closed-form deterministic answers.
This π* gives the canonical-projection preflight an algebra route.

Equivalence classes preserved
-----------------------------
Polynomial identity:

- ``"(x+1)**2"`` ≡ ``"x**2 + 2*x + 1"`` ≡ ``"x*x + 2*x + 1"``
- ``"(a+b)*(a-b)"`` ≡ ``"a**2 - b**2"``
- ``"2*x + 3*x"`` ≡ ``"5*x"``

Equivalence classes kept distinct
---------------------------------
Trigonometric identities are NOT collapsed by ``sp.expand`` alone:

- ``"sin(x)**2 + cos(x)**2"`` ≢ ``"1"`` under this π*. Use the
  ``algebra-symbolic-simplified@v1`` variant (Phase 1b, future
  ticket) when trig collapse is needed; ``sp.simplify`` is
  exponentially expensive on pathological inputs and lives behind
  an explicit opt-in.

Exponential identity (``exp(a+b) = exp(a)*exp(b)``) IS collapsed —
``sp.expand`` walks exp() across sums by default. That asymmetry
between trig and exp is a SymPy convention this π* inherits.

Canonical bytes
---------------
``sp.srepr(sp.expand(expr))`` UTF-8 encoded. ``srepr`` is the
S-expression representation SymPy emits for repr-stability across
internal printer changes; ``str(expr)`` ordering can shift between
SymPy versions and is not a reliable canonical form.

Round-trip property
-------------------
Idempotent: ``canonicalize(canonicalize(x))`` matches
``canonicalize(x)`` for any input that succeeds. ``sympify``
accepts ``srepr`` output as input, and ``expand`` of an already-
expanded form returns the same AST.

Optional dependency
-------------------
SymPy is part of the ``[math]`` extras (``pip install
'arborist[math]'``). When SymPy is absent, this module's
:func:`canonicalize` raises :class:`PiStarError` rather than
crashing at import. Tests gate on :func:`pytest.importorskip`.

Versioning
----------
``algebra-symbolic@v1`` pins this combination of (SymPy parser,
``expand``, ``srepr``). Any of these changing semantics warrants
an ``algebra-symbolic@v2`` rather than mutating v1.

Source: ticket #000030 §2.2 Phase 1.
"""

from __future__ import annotations

from dataclasses import dataclass

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


try:
    import sympy as sp  # type: ignore[import-not-found]
except ImportError:  # pragma: no cover — optional extra
    sp = None  # type: ignore[assignment]


_SYMPY_REQUIRED_MSG = (
    "algebra-symbolic@v1 requires sympy; install with "
    "`pip install 'arborist[math]'`"
)


@dataclass
class AlgebraSymbolicV1:
    name: str = "algebra-symbolic"
    version: str = "v1"
    domain: str = "symbolic-algebra"

    def canonicalize(self, raw: bytes) -> bytes:
        if sp is None:
            raise PiStarError(_SYMPY_REQUIRED_MSG)
        if not isinstance(raw, (bytes, bytearray)):
            raise PiStarError(
                "algebra-symbolic@v1 expects bytes; got "
                f"{type(raw).__name__}"
            )
        try:
            text = 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
        text = text.strip()
        if not text:
            raise PiStarError("algebra-symbolic@v1 input is empty")

        try:
            expr = sp.sympify(text)
        except (sp.SympifyError, SyntaxError, TypeError) as exc:
            raise PiStarError(
                f"algebra-symbolic@v1 cannot parse {text!r}: {exc}"
            ) from exc

        # Reject relationals (`x > 0`) and boolean operators (`x & y`,
        # `Eq(x, 1)`) — those belong on the logic route. SymPy's
        # ``Symbol`` confusingly inherits from ``Boolean`` (so a bare
        # ``x`` is a Boolean too), so the right rejection is "not an
        # Expr": every algebraic expression is an Expr; relationals
        # and BooleanFunctions are not.
        if not isinstance(expr, sp.Expr):
            raise PiStarError(
                f"algebra-symbolic@v1 rejects boolean/relational input "
                f"{text!r}; use logic-kernel@v1 for that shape"
            )

        try:
            canonical = sp.expand(expr)
        except (TypeError, ValueError) as exc:
            raise PiStarError(
                f"algebra-symbolic@v1 expand failed on {text!r}: {exc}"
            ) from exc

        return sp.srepr(canonical).encode("utf-8")


if sp is not None:
    register(AlgebraSymbolicV1())
