"""``time-series-quantized@v1`` π* — temporal signal canonicalizer.

Domain: ``time-series``. Implements the SQD-whitepaper-§13.5 plan
that ticket #000015 §1.7 reserved: resample at a declared rate,
quantize per declared Δ_v, encode as a committed integer vector
with header (dt, dv, count, t0).

Activates the cross-modality discipline for sensor / temporal-signal
data — alongside text + claim_lattice + memory + code + arithmetic +
logic, this is the **seventh** real carrier domain in the registry.

Input format
------------
UTF-8 JSON object::

    {
      "dt": <number>,           # time quantization step (required)
      "dv": <number>,           # value quantization step (required)
      "samples": [[t, v], ...]  # array of (timestamp, value) pairs
    }

- ``dt`` and ``dv`` MUST be positive numbers (int or float).
- ``samples`` is sorted by timestamp during canonicalization;
  duplicate timestamps (within Δ_t resolution) are dropped (last
  value wins).
- Empty ``samples`` is allowed (canonicalizes to an empty series).

Canonical output
----------------
UTF-8 text of shape::

    dt=<dt>;dv=<dv>;n=<count>;t0=<first-quantized-timestamp>:<v0>|<v1>|<v2>|...

Where each ``v_i`` is an integer ``round(value / dv)`` with
ties-to-even rounding (Python's default banker's rounding).

Equivalence classes preserved
-----------------------------
- Timestamp jitter within Δ_t resolution: same canonical.
- Value jitter within Δ_v / 2: same canonical (banker's rounding).
- Out-of-order samples: same canonical (sorted on input).
- Different presentation (e.g., trailing whitespace in JSON) but
  same logical data: same canonical.

Equivalence classes kept distinct
---------------------------------
- Different ``dt`` or ``dv`` declarations (the quantization grid is
  part of identity).
- Sample sequences that differ in any quantized value or timestamp.

Round-trip property
-------------------
Projective. The output is the canonical text form, not valid JSON
input — re-canonicalizing the canonical bytes raises PiStarError.

Versioning
----------
``time-series-quantized@v1`` pins this format. Any change to the
serialization, rounding rule, or sort order requires a new version.

Source: ticket #000015 §1.7 reserved this stub; this commit
graduates it. With ``arithmetic@v1``, ``logic-kernel@v1``, and now
``time-series-quantized@v1``, three of the four originally-stubbed
modalities are real. ``tabular-pinned@v1`` remains the last stub.
"""

from __future__ import annotations

import json
from dataclasses import dataclass

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


@dataclass
class TimeSeriesQuantizedV1:
    name: str = "time-series-quantized"
    version: str = "v1"
    domain: str = "time-series"

    def canonicalize(self, raw: bytes) -> bytes:
        if not isinstance(raw, (bytes, bytearray)):
            raise PiStarError(
                "time-series-quantized@v1 expects bytes; got "
                f"{type(raw).__name__}"
            )
        try:
            text = raw.decode("utf-8", errors="surrogatepass")
        except UnicodeDecodeError as exc:  # pragma: no cover
            raise PiStarError(f"input not valid UTF-8: {exc}") from exc

        try:
            obj = json.loads(text)
        except json.JSONDecodeError as exc:
            raise PiStarError(
                f"time-series-quantized@v1 input is not valid JSON: {exc}"
            ) from exc

        if not isinstance(obj, dict):
            raise PiStarError(
                "time-series-quantized@v1 input must be a JSON object"
            )
        for required in ("dt", "dv", "samples"):
            if required not in obj:
                raise PiStarError(
                    f"time-series-quantized@v1 missing required field {required!r}"
                )
        dt = obj["dt"]
        dv = obj["dv"]
        if not isinstance(dt, (int, float)) or dt <= 0:
            raise PiStarError(
                f"time-series-quantized@v1 dt must be positive number; got {dt!r}"
            )
        if not isinstance(dv, (int, float)) or dv <= 0:
            raise PiStarError(
                f"time-series-quantized@v1 dv must be positive number; got {dv!r}"
            )
        samples = obj["samples"]
        if not isinstance(samples, list):
            raise PiStarError(
                "time-series-quantized@v1 samples must be a JSON array"
            )

        # Quantize each sample to integer (t_idx, v_idx) pairs.
        quantized: list[tuple[int, int]] = []
        for i, pair in enumerate(samples):
            if not isinstance(pair, list) or len(pair) != 2:
                raise PiStarError(
                    f"sample[{i}] must be a [timestamp, value] pair"
                )
            t, v = pair
            if not isinstance(t, (int, float)) or not isinstance(v, (int, float)):
                raise PiStarError(
                    f"sample[{i}] timestamp and value must be numbers"
                )
            t_idx = int(round(t / dt))
            v_idx = int(round(v / dv))
            quantized.append((t_idx, v_idx))

        # Sort by timestamp; on collision, last value wins.
        quantized.sort(key=lambda p: p[0])
        if not quantized:
            return _serialize(dt, dv, t0=0, values=[]).encode("utf-8")

        # Dedupe by timestamp keeping the last value seen.
        deduped: dict[int, int] = {}
        for t_idx, v_idx in quantized:
            deduped[t_idx] = v_idx
        sorted_indices = sorted(deduped.keys())
        t0 = sorted_indices[0]
        values = [deduped[i] for i in sorted_indices]
        return _serialize(dt, dv, t0=t0, values=values).encode("utf-8")


def _serialize(dt, dv, *, t0: int, values: list[int]) -> str:
    # Format dt and dv canonically: int → "1", float → its repr (which
    # is the shortest round-trip representation).
    dt_s = _num(dt)
    dv_s = _num(dv)
    n = len(values)
    body = "|".join(str(v) for v in values)
    return f"dt={dt_s};dv={dv_s};n={n};t0={t0}:{body}"


def _num(x) -> str:
    if isinstance(x, int) or (isinstance(x, float) and x.is_integer()):
        return str(int(x))
    return repr(x)


register(TimeSeriesQuantizedV1())
