"""Verifier harness: one verdict schema, one results writer, no boilerplate.

A ``verify.py`` builds a :class:`Harness`, records findings through the
``check_*`` methods, then calls :meth:`Harness.finish`.  The JSON it writes
is the canonical machine-readable results schema; the exit code is 0 only
on ``ACCEPT``.  The harness never decides mathematics — every finding's
truth value is produced by the caller's own (ideally interval-rigorous)
computation.
"""

from __future__ import annotations

import json
import sys
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path

__all__ = [
    "Finding",
    "Harness",
    "Verdict",
    "add_repo_python_path",
    "repo_root",
]


def repo_root(start: str | Path | None = None) -> Path:
    """Locate the RH repository root from a file path or the CWD."""
    probe = Path(start).resolve() if start else Path.cwd().resolve()
    if probe.is_file():
        probe = probe.parent
    for candidate in (probe, *probe.parents):
        if (candidate / "bin" / "rhc.py").is_file() and (candidate / "canon").is_dir():
            return candidate
    raise RuntimeError(f"no RH repository root above {probe}")


def add_repo_python_path(start: str | Path | None = None) -> Path:
    """Put ``<root>/python`` on ``sys.path`` so ``rh_verify`` imports.

    Replaces the per-script ``sys.path.insert`` hack.  Idempotent.
    """
    root = repo_root(start)
    python_dir = str(root / "python")
    if python_dir not in sys.path:
        sys.path.insert(0, python_dir)
    return root


class Verdict(str, Enum):
    ACCEPT = "ACCEPT"
    REJECT = "REJECT"
    INCONCLUSIVE = "INCONCLUSIVE"


@dataclass(frozen=True)
class Finding:
    name: str
    claim: str
    passed: bool
    observed: str | None = None
    bound: str | None = None


@dataclass
class Harness:
    """Collects findings for one verifier run."""

    experiment: str
    claim: str
    scope: str = ""
    precision: dict = field(default_factory=dict)
    findings: list[Finding] = field(default_factory=list)

    def check(
        self,
        passed: bool,
        name: str,
        claim: str,
        *,
        observed=None,
        bound=None,
    ) -> bool:
        """Record one finding whose truth the caller already established."""
        self.findings.append(
            Finding(
                name=name,
                claim=claim,
                passed=bool(passed),
                observed=None if observed is None else str(observed),
                bound=None if bound is None else str(bound),
            )
        )
        return bool(passed)

    def check_close(self, actual, expected, tolerance, name: str) -> bool:
        """Exploration-tier closeness check on plain numeric values."""
        error = abs(actual - expected)
        return self.check(
            error < tolerance,
            name,
            f"|{name} - expected| < {tolerance}",
            observed=error,
            bound=tolerance,
        )

    def check_enclosure_positive(self, value, name: str) -> bool:
        """Record positivity of an ``arb`` enclosure (rigorous if input is)."""
        return self.check(
            bool(value > 0),
            name,
            f"{name} > 0 as an interval enclosure",
            observed=value,
        )

    @property
    def passed(self) -> int:
        return sum(1 for finding in self.findings if finding.passed)

    @property
    def failed(self) -> int:
        return sum(1 for finding in self.findings if not finding.passed)

    def verdict(self) -> Verdict:
        if self.failed:
            return Verdict.REJECT
        if not self.findings:
            return Verdict.INCONCLUSIVE
        return Verdict.ACCEPT

    def to_json(self, conclusion: str) -> dict:
        return {
            "status": "ok",
            "verdict": self.verdict().value,
            "experiment": self.experiment,
            "claim": self.claim,
            "scope": self.scope,
            "precision": self.precision,
            "checks": {"passed": self.passed, "failed": self.failed},
            "findings": [
                {
                    "name": finding.name,
                    "claim": finding.claim,
                    "passed": finding.passed,
                    "observed": finding.observed,
                    "bound": finding.bound,
                }
                for finding in self.findings
            ],
            "conclusion": conclusion,
        }

    def finish(
        self,
        conclusion: str,
        *,
        results_path: str | Path | None = None,
        quiet: bool = False,
    ) -> int:
        """Write results, print the summary, return the process exit code."""
        payload = self.to_json(conclusion)
        if results_path is not None:
            path = Path(results_path)
            path.parent.mkdir(parents=True, exist_ok=True)
            path.write_text(
                json.dumps(payload, indent=2, sort_keys=True) + "\n"
            )
        if not quiet:
            print(f"{self.experiment}: {self.claim}")
            for finding in self.findings:
                marker = "PASS" if finding.passed else "FAIL"
                detail = (
                    f" (observed {finding.observed})"
                    if finding.observed is not None and not finding.passed
                    else ""
                )
                print(f"  [{marker}] {finding.name}: {finding.claim}{detail}")
            print(
                f"verdict: {payload['verdict']} "
                f"({self.passed} passed, {self.failed} failed)"
            )
        return 0 if self.verdict() is Verdict.ACCEPT else 1
