"""End-to-end demo for ``function-sampled@v1`` (#000030 Phase 7).

Closes the loop on opencompletion's ``activity24-math-plot.yaml``:
SymPy expression → quantized integer-vector signature (canonical
bytes) → optional matplotlib PNG (a downstream view of the
canonical evidence).

The canonical bytes are the proof: two function-sampled outputs
that match byte-for-byte represent the same function on the same
quantization grid. The PNG is just a human-readable rendering of
those same bytes — different DPIs / colormaps / axis spans don't
change identity.

Usage::

    python -m bench.scripts.demo_plot \\
        --expr 'sin(x)' --x-min 0 --x-max 6.283 --n-samples 200 --dv 0.01 \\
        --png /tmp/sin.png

    # Or via Make (PNG optional):
    make demo-plot Q='sin(x)'
    make demo-plot Q='x**2 + 2*x + 1' PNG=/tmp/parabola.png

The matplotlib import is gated — the canonical bytes always print
to stdout; the PNG is written only when ``--png`` is provided AND
matplotlib is importable.
"""

from __future__ import annotations

import argparse
import hashlib
import json
import sys
from pathlib import Path


def _build_payload(
    expr: str, x: str, x_min: float, x_max: float,
    n_samples: int, dv: float,
) -> bytes:
    return json.dumps({
        "f": expr, "x": x,
        "x_min": x_min, "x_max": x_max,
        "n_samples": n_samples, "dv": dv,
    }).encode("utf-8")


def _maybe_render_png(
    *, expr: str, x_var: str, x_min: float, x_max: float,
    n_samples: int, png_path: Path,
) -> None:
    """Render a PNG via matplotlib for human-readable inspection.
    No-op if matplotlib isn't installed."""
    try:
        import matplotlib
        matplotlib.use("Agg")
        import matplotlib.pyplot as plt
        import sympy as sp
    except ImportError:
        print(
            "warning: matplotlib + sympy required for PNG render; "
            "skipping --png",
            file=sys.stderr,
        )
        return
    x_sym = sp.Symbol(x_var)
    f_expr = sp.sympify(expr, locals={x_var: x_sym})
    f = sp.lambdify(x_sym, f_expr, "math")
    step = (x_max - x_min) / (n_samples - 1)
    xs = [x_min + i * step for i in range(n_samples)]
    ys = []
    for xi in xs:
        try:
            yi = f(xi)
            if isinstance(yi, complex):
                yi = yi.real
            ys.append(float(yi))
        except (ValueError, ZeroDivisionError, OverflowError):
            ys.append(float("nan"))
    plt.figure(figsize=(8, 5))
    plt.plot(xs, ys, linewidth=2, color="#3366cc")
    plt.title(f"y = {expr}")
    plt.xlabel(x_var)
    plt.ylabel("y")
    plt.grid(True, alpha=0.3)
    png_path.parent.mkdir(parents=True, exist_ok=True)
    plt.tight_layout()
    plt.savefig(png_path, dpi=120)
    plt.close()


def main(argv: list[str] | None = None) -> int:
    p = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
    p.add_argument("--expr", required=True, help="SymPy expression in the variable --x")
    p.add_argument("--x", default="x", help="independent-variable name (default 'x')")
    p.add_argument("--x-min", dest="x_min", type=float, default=-3.14159)
    p.add_argument("--x-max", dest="x_max", type=float, default=3.14159)
    p.add_argument("--n-samples", dest="n_samples", type=int, default=200)
    p.add_argument("--dv", type=float, default=0.01,
                   help="value-quantization step (smaller = tighter)")
    p.add_argument("--png", type=Path, default=None,
                   help="optional PNG output path (requires matplotlib)")
    args = p.parse_args(argv)

    from arborist.pi_star import PiStarError, get
    try:
        ps = get("function-sampled@v1")
    except KeyError:
        print(
            "error: function-sampled@v1 not registered (sympy missing?); "
            "install with `pip install arborist[math]`",
            file=sys.stderr,
        )
        return 2

    payload = _build_payload(
        args.expr, args.x, args.x_min, args.x_max,
        args.n_samples, args.dv,
    )
    try:
        canonical_bytes = ps.canonicalize(payload)
    except PiStarError as exc:
        print(f"error: {exc}", file=sys.stderr)
        return 1

    digest = hashlib.sha256(canonical_bytes).hexdigest()

    out = {
        "expr": args.expr,
        "x": args.x,
        "grid": {
            "x_min": args.x_min,
            "x_max": args.x_max,
            "n_samples": args.n_samples,
            "dv": args.dv,
        },
        "canonical_bytes_sha256": digest,
        "canonical_bytes_preview": canonical_bytes.decode("utf-8")[:200] + (
            "…" if len(canonical_bytes) > 200 else ""
        ),
        "canonical_bytes_total_chars": len(canonical_bytes),
        "pi_star_ref": "function-sampled@v1",
    }

    if args.png is not None:
        _maybe_render_png(
            expr=args.expr, x_var=args.x,
            x_min=args.x_min, x_max=args.x_max,
            n_samples=args.n_samples,
            png_path=args.png,
        )
        out["png_path"] = str(args.png)

    print(json.dumps(out, indent=2, ensure_ascii=False))
    return 0


if __name__ == "__main__":
    sys.exit(main())
