"""Source-only ActionReceipt witness operator — experimental.

The operated deed registry logs composition deeds; nothing before this module
verifies an **ActionReceipt** on intake and commits it to an append-only log.
This is that service, built on the existing substrate: the RFC 6962 Merkle
mechanics and JSONL persistence of :class:`bulla.registry.DeedLog`, and the
signed tree heads of :mod:`bulla.experimental.checkpoint`.

Scope, stated exactly (see ``glyph/data/evidence-contract.json``): an instance
can retain exact receipt bytes, commit them to an append-only Merkle log, and
serve operator-signed checkpoints and proofs. Operation does not establish
independence. The public manifest can report only ``NOT_REVIEWED``; external
control remains an attributable institutional determination.

Temporal claim: intake records ``received_at`` — the one label a witness can
honestly assert. It never upgrades the receipt's own ``timestamp`` claim
(unbound under wire v0.2/v0.3; see ``spec/action-receipt-v0.4-draft.md``).

Profile: ``bulla.receipt-witness/0.1-experimental``. Experimental tier — no
wire stability, plurality, availability, capital, production fitness, or
recourse is implied.
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import parse_qs, urlparse

from bulla.action_receipt import verify_receipt
from bulla.identity import LocalEd25519Signer
from bulla.receipt_parser import (
    ReceiptParseError,
    ReceiptParseLimits,
    parse_action_receipt_json,
)
from bulla.registry import Deed, DeedLog, _b2h, inclusion_proof, merkle_root
from bulla.experimental.checkpoint import (
    CheckpointArchive,
    WitnessCheckpoint,
    issue_checkpoint,
)

SCHEMA_VERSION = "0.1-experimental"
PROFILE = "bulla.receipt-witness/0.1-experimental"
_INTAKE_PARSE_LIMITS = ReceiptParseLimits()
_SUPPORTED_RECEIPT_PROFILES = ("0.2", "0.3", "0.4")

_DEFAULT_RETENTION = {
    "policy": "retain-exact-bytes",
    "duration": "pilot — until the pilot's declared teardown, minimum 90 days",
    "privacy": "receipts are submitted for retention; no fields are redacted, "
               "so submitters must not include undisclosable material",
}


def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")


class IntakeRefused(ValueError):
    """Raised when a submitted document fails receipt verification."""


def _is_sha256(value: object) -> bool:
    return (
        isinstance(value, str)
        and len(value) == 71
        and value.startswith("sha256:")
        and all(char in "0123456789abcdef" for char in value[7:])
    )


@dataclass(frozen=True)
class OperatorManifestConfig:
    """Public operator declarations bound into the served manifest.

    These values describe an operator's submitted configuration. They are not
    proof of organizational independence, availability, policy performance, or
    production fitness. ``externality_status`` is deliberately not caller
    configurable: an operator cannot certify its own independence.
    """

    endpoint: str
    source_kit_root: str
    retention_policy_sha256: str
    privacy_policy_sha256: str
    control_disclosure_sha256: str
    max_receipt_bytes: int = _INTAKE_PARSE_LIMITS.max_bytes

    def __post_init__(self) -> None:
        if not self.endpoint.startswith(("https://", "http://127.0.0.1", "http://localhost")):
            raise ValueError("operator endpoint must be HTTPS or an explicit loopback test endpoint")
        for label, value in (
            ("source_kit_root", self.source_kit_root),
            ("retention_policy_sha256", self.retention_policy_sha256),
            ("privacy_policy_sha256", self.privacy_policy_sha256),
            ("control_disclosure_sha256", self.control_disclosure_sha256),
        ):
            if not _is_sha256(value):
                raise ValueError(f"{label} must be sha256:<64 lowercase hex>")
        if (
            isinstance(self.max_receipt_bytes, bool)
            or not 1
            <= self.max_receipt_bytes
            <= _INTAKE_PARSE_LIMITS.max_bytes
        ):
            raise ValueError(
                f"max_receipt_bytes must be between 1 and {_INTAKE_PARSE_LIMITS.max_bytes}"
            )


@dataclass
class ReceiptWitness:
    """A verified-intake, append-only ActionReceipt witness (single operator).

    ``log_path`` holds the Merkle leaves (one deed per line); ``store_path``
    holds the exact submitted bytes keyed by attestation hash, with intake
    metadata. ``checkpoint_path`` holds authenticated heads plus their adjacent
    consistency proofs. All three stores survive process restarts.
    """

    operator_signer: LocalEd25519Signer
    log_id: str
    log_path: Path
    store_path: Path
    checkpoint_path: Path | None = None
    manifest_config: OperatorManifestConfig | None = None
    retention: dict = field(default_factory=lambda: dict(_DEFAULT_RETENTION))

    def __post_init__(self) -> None:
        self.log_path = Path(self.log_path)
        self.store_path = Path(self.store_path)
        self.checkpoint_path = Path(
            self.checkpoint_path
            if self.checkpoint_path is not None
            else self.store_path.with_name("checkpoints.jsonl")
        )
        self._log = DeedLog(self.log_path)
        self._checkpoints = CheckpointArchive(self.checkpoint_path)
        self._meta: dict[str, dict] = {}
        self._bytes: dict[str, bytes] = {}
        if self.store_path.exists():
            for line in self.store_path.read_text(encoding="utf-8").splitlines():
                if not line.strip():
                    continue
                rec = json.loads(line)
                if set(rec) != {
                    "attestation_hash",
                    "index",
                    "received_at",
                    "verified_to",
                    "receipt_json",
                }:
                    raise ValueError("receipt store entry has missing or unknown fields")
                if rec["attestation_hash"] in self._meta:
                    raise ValueError("receipt store contains a duplicate attestation")
                self._meta[rec["attestation_hash"]] = rec
                self._bytes[rec["attestation_hash"]] = rec["receipt_json"].encode("utf-8")
        if len(self._meta) != len(self._log):
            raise ValueError("receipt store and Merkle log have different member counts")

    # ── intake ────────────────────────────────────────────────────────────

    def intake(self, receipt_bytes: bytes) -> dict:
        """Verify a submitted ActionReceipt and commit it. Fails closed.

        Returns ``{attestation_hash, index, tree_size, root, received_at,
        verified_to}``. Idempotent on attestation hash: re-submission returns
        the original intake record rather than a second leaf.
        """
        try:
            doc = parse_action_receipt_json(
                receipt_bytes, limits=_INTAKE_PARSE_LIMITS
            ).to_dict()
        except ReceiptParseError as exc:
            raise IntakeRefused(f"not a JSON document: {exc}") from exc
        verdict = verify_receipt(doc)
        if not verdict.ok:
            raise IntakeRefused(
                "receipt failed verification: " + "; ".join(verdict.reasons or ("unspecified",))
            )
        hashes = doc.get("hashes") or {}
        attestation = hashes.get("attestation")
        content = hashes.get("content")
        if not attestation or not content:
            raise IntakeRefused("receipt lacks attestation/content hashes")
        existing = self._meta.get(attestation)
        if existing is not None:
            return {k: existing[k] for k in
                    ("attestation_hash", "index", "received_at", "verified_to")} | {
                    "tree_size": len(self._log), "root": self._log.root(), "duplicate": True}

        issuer = (doc.get("signature") or {}).get("issuer") or "unsigned"
        index = self._log.append(Deed(issuer, content, attestation))
        received_at = _now_iso()
        record = {
            "attestation_hash": attestation,
            "index": index,
            "received_at": received_at,
            "verified_to": verdict.verified_to,
            "receipt_json": receipt_bytes.decode("utf-8"),
        }
        self.store_path.parent.mkdir(parents=True, exist_ok=True)
        with self.store_path.open("a", encoding="utf-8") as handle:
            handle.write(json.dumps(record, sort_keys=True) + "\n")
        self._meta[attestation] = record
        self._bytes[attestation] = receipt_bytes
        return {
            "attestation_hash": attestation,
            "index": index,
            "tree_size": len(self._log),
            "root": self._log.root(),
            "received_at": received_at,
            "verified_to": verdict.verified_to,
        }

    # ── proofs and retrieval ─────────────────────────────────────────────

    def inclusion(self, attestation_hash: str) -> dict | None:
        record = self._log.inclusion_by_attestation(attestation_hash)
        if record is None:
            return None
        meta = self._meta.get(attestation_hash) or {}
        return {**record, "received_at": meta.get("received_at"),
                "verified_to": meta.get("verified_to")}

    def inclusion_at(self, attestation_hash: str, tree_size: int) -> dict | None:
        """Return inclusion against an explicitly retained historical size.

        The experimental operator keeps all leaves locally, so it can recompute
        the exact proof for a previously signed checkpoint without trusting a
        separately stored presentation.
        """
        meta = self._meta.get(attestation_hash)
        if meta is None:
            return None
        index = meta["index"]
        leaves = self._log._leaves()  # source-only operator over its local DeedLog
        if (
            isinstance(tree_size, bool)
            or tree_size < 1
            or tree_size > len(leaves)
            or index >= tree_size
        ):
            raise ValueError("historical inclusion size does not contain this receipt")
        prefix = leaves[:tree_size]
        return {
            "index": index,
            "tree_size": tree_size,
            "leaf": _b2h(prefix[index]),
            "proof": [_b2h(item) for item in inclusion_proof(prefix, index)],
            "root": _b2h(merkle_root(prefix)),
            "attestation_hash": attestation_hash,
            "received_at": meta["received_at"],
            "verified_to": meta["verified_to"],
        }

    def consistency(self, old_size: int) -> dict:
        return self._log.consistency(old_size)

    def receipt_bytes(self, attestation_hash: str) -> bytes | None:
        return self._bytes.get(attestation_hash)

    def intake_record(self, attestation_hash: str) -> dict | None:
        record = self._meta.get(attestation_hash)
        if record is None:
            return None
        return {
            key: record[key]
            for key in ("attestation_hash", "index", "received_at", "verified_to")
        }

    def intake_records(self) -> tuple[dict, ...]:
        """Return public intake metadata in committed log order."""
        return tuple(
            {
                key: record[key]
                for key in ("attestation_hash", "index", "received_at", "verified_to")
            }
            for record in sorted(self._meta.values(), key=lambda item: item["index"])
        )

    def root(self) -> str:
        return self._log.root()

    def __len__(self) -> int:
        return len(self._log)

    # ── signed heads ─────────────────────────────────────────────────────

    def checkpoint(self) -> WitnessCheckpoint:
        """Explicitly issue and persist a signed, append-only tree head."""
        previous = self._checkpoints.latest()
        head = issue_checkpoint(
            self._log,
            self.operator_signer,
            log_id=self.log_id,
            previous=previous,
            issued_at=_now_iso(),
        )
        consistency = self._log.consistency(previous.tree_size) if previous is not None else None
        self._checkpoints.append(head, consistency_from_previous=consistency)
        return head

    def latest_checkpoint(self) -> WitnessCheckpoint | None:
        """Return the most recently persisted head without issuing a new one."""
        return self._checkpoints.latest()

    def checkpoint_history(self) -> tuple[WitnessCheckpoint, ...]:
        return self._checkpoints.history()

    def adjacent_consistency(self, old_hash: str, new_hash: str) -> dict | None:
        record = self._checkpoints.adjacent_consistency(old_hash, new_hash)
        return dict(record) if record is not None else None

    def manifest(self) -> dict:
        """The public operator manifest. It never self-certifies independence."""
        config = self.manifest_config
        return {
            "profile": PROFILE,
            "schema_version": SCHEMA_VERSION,
            "operator_did": self.operator_signer.issuer,
            "log_id": self.log_id,
            "endpoint": config.endpoint if config else "NOT_PUBLISHED",
            "source_kit_root": config.source_kit_root if config else None,
            "retention_policy_sha256": config.retention_policy_sha256 if config else None,
            "privacy_policy_sha256": config.privacy_policy_sha256 if config else None,
            "control_disclosure_sha256": config.control_disclosure_sha256 if config else None,
            "supported_receipt_profiles": list(_SUPPORTED_RECEIPT_PROFILES),
            "resource_limits": {
                "max_receipt_bytes": (
                    config.max_receipt_bytes if config else _INTAKE_PARSE_LIMITS.max_bytes
                ),
            },
            "externality_status": "NOT_REVIEWED",
            "temporal_claims": {
                "received_at": "asserted by this witness at intake",
                "claimed_at": "actor-supplied; not bound into the signed occurrence "
                              "identity under wire v0.2/v0.3",
            },
        }


# ── transport: public operation is read-only ─────────────────────────────


def make_witness_server(witness: ReceiptWitness, host: str = "127.0.0.1", port: int = 0) -> ThreadingHTTPServer:
    """Read-only public operator surface.

    Receipt intake and checkpoint issuance are deliberately absent. Operators
    perform both through local commands, so a public GET or POST can never
    mutate the retained history.
    """

    class Handler(BaseHTTPRequestHandler):
        def _json(self, code: int, payload: dict) -> None:
            body = json.dumps(payload, sort_keys=True).encode("utf-8")
            self.send_response(code)
            self.send_header("Content-Type", "application/json")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)

        def do_GET(self) -> None:  # noqa: N802 — http.server API
            url = urlparse(self.path)
            query = parse_qs(url.query)
            if url.path == "/manifest":
                self._json(200, witness.manifest())
            elif url.path == "/root":
                self._json(200, {"root": witness.root(), "tree_size": len(witness)})
            elif url.path == "/inclusion":
                attestation = (query.get("attestation") or [""])[0]
                record = witness.inclusion(attestation)
                if record is None:
                    self._json(404, {"error": "unknown attestation hash"})
                else:
                    self._json(200, record)
            elif url.path == "/receipt":
                attestation = (query.get("attestation") or [""])[0]
                raw = witness.receipt_bytes(attestation)
                if raw is None:
                    self._json(404, {"error": "unknown attestation hash"})
                else:
                    self.send_response(200)
                    self.send_header("Content-Type", "application/json")
                    self.send_header("Content-Length", str(len(raw)))
                    self.end_headers()
                    self.wfile.write(raw)
            elif url.path == "/checkpoint/latest":
                head = witness.latest_checkpoint()
                self._json(
                    200 if head is not None else 404,
                    {"checkpoint": head.to_dict() if head is not None else None},
                )
            elif url.path == "/checkpoint/history":
                self._json(
                    200,
                    {"checkpoints": [head.to_dict() for head in witness.checkpoint_history()]},
                )
            elif url.path == "/checkpoint/consistency":
                old_hash = (query.get("from") or [""])[0]
                new_hash = (query.get("to") or [""])[0]
                record = witness.adjacent_consistency(old_hash, new_hash)
                self._json(
                    200 if record is not None else 404,
                    {"from": old_hash, "to": new_hash, "consistency": record},
                )
            else:
                self._json(404, {"error": "unknown route"})

        def do_POST(self) -> None:  # noqa: N802 — http.server API
            self._json(405, {"error": "public witness operation is read-only"})

        def log_message(self, *args: object) -> None:  # quiet test servers
            return

    return ThreadingHTTPServer((host, port), Handler)
