"""``algebra-symbolic-simplified@v1`` π* — full-simplify variant.

Domain: ``symbolic-algebra``. The ``algebra-symbolic@v1`` sibling
runs ``sp.expand`` only — fast, polynomial+exponential identity
collapses, but trigonometric identities do not. This variant runs
``sp.simplify`` instead — slower, with unbounded CPU on
pathological inputs, but trig identity collapses (and many other
algebraic identities ``expand`` won't touch).

Why this variant exists
-----------------------
Ticket #000030 §2.3 deferred the trig surface to a separate version
because ``sp.simplify`` cost is unbounded — operators opt in when
they need it. Splitting the variant keeps the fast path fast for
operators who only care about polynomial canonicalization.

Equivalence classes preserved
-----------------------------
Everything ``algebra-symbolic@v1`` collapses, plus:

- ``"sin(x)**2 + cos(x)**2"`` ≡ ``"1"``
- ``"tan(x)*cos(x)"`` ≡ ``"sin(x)"``
- ``"exp(log(x))"`` ≡ ``"x"``
- ``"sin(x)*cos(y) + cos(x)*sin(y)"`` ≡ ``"sin(x + y)"``

Equivalence classes still kept distinct
---------------------------------------
``sp.simplify`` makes domain assumptions (e.g. ``log(exp(x))`` does
NOT collapse to ``x`` without an explicit positive-real assumption
on ``x``). Where the user's intent depends on the assumption, the
outputs stay distinct; this is correct behavior — symbol assumptions
are out of scope for Phase 1b.

Canonical bytes
---------------
``sp.srepr(sp.expand(sp.simplify(expr)))`` UTF-8 encoded.
``simplify`` collapses identities (trig, exp, log on real domains);
``expand`` then enforces a uniform polynomial form so two
algebraically equivalent inputs (``(x+1)**2`` and ``x**2 + 2*x + 1``)
emit the same canonical bytes. ``simplify`` alone would leave them
in different shapes (factored vs expanded) and break the
equivalence-class invariant.

Round-trip property
-------------------
Idempotent: ``simplify(simplify(x)) == simplify(x)`` is the SymPy
contract (verified empirically against the trig/exp surface).

Cost discipline
---------------
``sp.simplify`` is observed at 1-360 ms on common trig/exp inputs;
pathological inputs can grow exponentially. Operators must size for
the worst case before enabling on a hot path. There is no built-in
timeout in this Phase-1b implementation — the natural place for a
timeout wrap is the calling pipeline (preflight router has its own
budget), not inside the π*. Phase 3 (``calculus-integral@v1``) is
where the in-π* timeout pattern lands; Phase 1b leaves the
unbounded surface visible to operators.

Optional dependency
-------------------
Same as ``algebra-symbolic@v1``: SymPy via the ``[math]`` extras.
When SymPy is absent, :func:`canonicalize` raises
:class:`PiStarError`.

Source: ticket #000030 §2.3 deferred Phase 1b — landed 2026-05-09.
"""

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-simplified@v1 requires sympy; install with "
    "`pip install 'arborist[math]'`"
)


@dataclass
class AlgebraSymbolicSimplifiedV1:
    name: str = "algebra-symbolic-simplified"
    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-simplified@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-simplified@v1 input is empty")

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

        # Same Expr-vs-Boolean filter as algebra-symbolic@v1 — Symbol
        # confusingly inherits from Boolean, so the right rejection is
        # "not an Expr."
        if not isinstance(expr, sp.Expr):
            raise PiStarError(
                f"algebra-symbolic-simplified@v1 rejects boolean/relational "
                f"input {text!r}; use logic-kernel@v1 for that shape"
            )

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

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


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