"""Private evidence persistence for the disposable DualCoach v1.1.1 run."""

from __future__ import annotations

import json
import os
import stat
import tempfile
from collections.abc import Mapping
from dataclasses import dataclass
from pathlib import Path
from typing import Final, TypeAlias, assert_never, override

_GENERATED_NAMES: Final = frozenset(
    {
        "candidate-binding.json",
        "capability-issuance.json",
        "child-after-restart.json",
        "child-before-crash.json",
        "cleanup.json",
        "crash-restart-state.json",
        "disposable-e2e-red.txt",
        "failure.json",
        "fake-api-calls.jsonl",
        "model-provider-calls.json",
        "network-boundary.json",
        "offsets.json",
        "pass.json",
        "process-events.json",
        "process-events.jsonl",
        "publication-counts.json",
        "receipt-assertions.json",
        "transcript.txt",
    }
)
JsonScalar: TypeAlias = str | int | float | bool | None
JsonValue: TypeAlias = JsonScalar | list["JsonValue"] | dict[str, "JsonValue"]


@dataclass(frozen=True, slots=True)
class EvidenceError(RuntimeError):
    reason: str

    @override
    def __str__(self) -> str:
        return self.reason


def prepare_evidence_dir(path: Path) -> None:
    """Create a private directory and remove stale generated alternatives."""
    path.mkdir(mode=0o700, parents=True, exist_ok=True)
    _ = path.chmod(0o700)
    for name in _GENERATED_NAMES:
        (path / name).unlink(missing_ok=True)


def write_private(path: Path, payload: bytes) -> None:
    """Atomically replace an artifact via an exclusive mode-0600 temporary."""
    path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
    _ = path.parent.chmod(0o700)
    descriptor, temporary_name = tempfile.mkstemp(
        dir=path.parent,
        prefix=f".{path.name}.",
        suffix=".tmp",
    )
    temporary = Path(temporary_name)
    try:
        os.fchmod(descriptor, 0o600)
        with os.fdopen(descriptor, "wb", closefd=True) as stream:
            stream.write(payload)
            stream.flush()
            os.fsync(stream.fileno())
        os.replace(temporary, path)
    finally:
        temporary.unlink(missing_ok=True)


def write_json(path: Path, payload: Mapping[str, JsonValue]) -> None:
    write_private(
        path,
        (json.dumps(payload, sort_keys=True, separators=(",", ":")) + "\n").encode(),
    )


def write_text(path: Path, text: str) -> None:
    write_private(path, text.encode("utf-8"))


def verify_evidence(evidence_dir: Path, transcript: Path, marker: str) -> None:
    """Prove privacy, loopback isolation, final marker, and cleanup."""
    if stat.S_IMODE(evidence_dir.stat().st_mode) != 0o700:
        raise EvidenceError("evidence directory is not private")
    for artifact in (*evidence_dir.iterdir(), transcript):
        if (
            artifact.is_symlink()
            or not artifact.is_file()
            or stat.S_IMODE(artifact.stat().st_mode) != 0o600
        ):
            raise EvidenceError(f"evidence artifact is not private: {artifact.name}")
    if transcript.read_text(encoding="utf-8").splitlines()[-1] != marker:
        raise EvidenceError("canonical transcript marker is missing")
    result_transcript = evidence_dir / "transcript.txt"
    if result_transcript.read_text(encoding="utf-8").splitlines()[-1] != marker:
        raise EvidenceError("result transcript marker is missing")
    cleanup = json_object(
        parse_json((evidence_dir / "cleanup.json").read_bytes()),
        "cleanup evidence",
    )
    boundary = json_object(
        parse_json((evidence_dir / "network-boundary.json").read_bytes()),
        "network boundary evidence",
    )
    cleanup_ok = bool(cleanup) and all(value is True for value in cleanup.values())
    loopback_ok = boundary == {
        "bind_host": "127.0.0.1",
        "bot_api_host": "127.0.0.1",
        "loopback_only": True,
        "model_api_host": "127.0.0.1",
    }
    if not cleanup_ok or not loopback_ok:
        raise EvidenceError("cleanup or loopback evidence is invalid")


def _narrow_json_value(value: JsonValue) -> JsonValue:
    """Recursively retain only JSON primitives, lists, and string-keyed objects."""
    match value:
        case str() | int() | float() | bool() | None:
            return value
        case list(items):
            return [_narrow_json_value(item) for item in items]
        case dict(entries):
            normalized: dict[str, JsonValue] = {}
            for key, item in entries.items():
                if not isinstance(key, str):
                    raise EvidenceError("JSON object keys must be strings")
                normalized[key] = _narrow_json_value(item)
            return normalized
        case unreachable:
            assert_never(unreachable)


def parse_json(payload: bytes | str) -> JsonValue:
    """Parse one JSON boundary into the recursive evidence value type."""
    return _narrow_json_value(json.loads(payload))


def json_object(value: JsonValue, label: str) -> dict[str, JsonValue]:
    """Require a parsed JSON value to be an object before field access."""
    if not isinstance(value, dict):
        raise EvidenceError(label)
    return value
