"""Fail-closed validation tiering, checkpoint, and resume control.

The module deliberately owns only orchestration state.  Product execution and
receipt production stay outside this boundary so that the checkpoint ledger
can independently validate what may be reused after an interruption.
"""

from __future__ import annotations

import hashlib
import json
import os
import pathlib
import re
import uuid
from datetime import datetime, timezone
from typing import Any, Mapping, Sequence

from . import validation_tiering


PLAN_SCHEMA = "reliability.validation-plan.v1"
EVENT_SCHEMA = "reliability.validation-event.v1"
HEX64 = re.compile(r"^[0-9a-f]{64}$")
TIERS = ("T0", "T1", "T2", "T3", "T4")
REQUIRED_BINDING_DIGESTS = (
    "source_tree_sha256",
    "candidate_sha256",
    "runner_sha256",
    "orchestrator_sha256",
    "authority_sha256",
)


class ValidationStateError(RuntimeError):
    """Raised whenever state cannot be proven safe to consume or extend."""


def canonical_bytes(value: Any) -> bytes:
    try:
        return json.dumps(
            value,
            ensure_ascii=False,
            sort_keys=True,
            separators=(",", ":"),
            allow_nan=False,
        ).encode("utf-8")
    except (TypeError, ValueError, OverflowError, RecursionError) as exc:
        raise ValidationStateError("canonical_json_failed") from exc


def canonical_digest(value: Any) -> str:
    return hashlib.sha256(canonical_bytes(value)).hexdigest()


def receipt_digest(receipt: Mapping[str, Any]) -> str:
    payload = dict(receipt)
    payload.pop("receipt_sha256", None)
    return canonical_digest(payload)


def readiness_digest(readiness: Mapping[str, Any]) -> str:
    payload = dict(readiness)
    payload.pop("readiness_sha256", None)
    return canonical_digest(payload)


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


def _load_json(path: pathlib.Path) -> Any:
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except (OSError, UnicodeError, json.JSONDecodeError) as exc:
        raise ValidationStateError(f"json_load_failed:{path.name}") from exc


def _write_new_json(path: pathlib.Path, value: Any) -> None:
    raw = (
        json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2, allow_nan=False)
        + "\n"
    ).encode("utf-8")
    path.parent.mkdir(parents=True, exist_ok=True)
    flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
    try:
        descriptor = os.open(path, flags, 0o600)
    except FileExistsError as exc:
        raise ValidationStateError(f"append_target_exists:{path.name}") from exc
    try:
        with os.fdopen(descriptor, "wb") as stream:
            stream.write(raw)
            stream.flush()
            os.fsync(stream.fileno())
    except Exception:
        # A partial append is intentionally left visible.  Verification will
        # fail closed instead of silently skipping a possibly durable event.
        raise


def _validate_bindings(bindings: Any) -> dict[str, Any]:
    if not isinstance(bindings, Mapping):
        raise ValidationStateError("bindings_not_mapping")
    normalized = dict(bindings)
    missing = [name for name in REQUIRED_BINDING_DIGESTS if name not in normalized]
    if missing:
        raise ValidationStateError("bindings_missing:" + ",".join(missing))
    for name in REQUIRED_BINDING_DIGESTS:
        if not isinstance(normalized[name], str) or HEX64.fullmatch(normalized[name]) is None:
            raise ValidationStateError("binding_digest_invalid:" + name)
    for name, value in normalized.items():
        if not isinstance(name, str) or not name or value in (None, ""):
            raise ValidationStateError("binding_value_invalid")
    canonical_bytes(normalized)
    return normalized


def _validate_tasks(tasks: Any) -> list[str]:
    if not isinstance(tasks, Sequence) or isinstance(tasks, (str, bytes)):
        raise ValidationStateError("tasks_not_sequence")
    normalized = list(tasks)
    if not normalized or any(not isinstance(item, str) or not item for item in normalized):
        raise ValidationStateError("task_id_invalid")
    if len(set(normalized)) != len(normalized):
        raise ValidationStateError("task_id_duplicate")
    return normalized


def _parse_utc(value: Any, field: str) -> datetime:
    if not isinstance(value, str) or not value:
        raise ValidationStateError(field + "_invalid")
    try:
        parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
    except ValueError as exc:
        raise ValidationStateError(field + "_invalid") from exc
    if parsed.tzinfo is None:
        raise ValidationStateError(field + "_timezone_missing")
    return parsed.astimezone(timezone.utc)


def _validate_readiness(value: Any, *, enforce_fresh: bool = False) -> dict[str, Any]:
    if not isinstance(value, Mapping):
        raise ValidationStateError("readiness_not_mapping")
    readiness = dict(value)
    required = {
        "status",
        "checked_at",
        "valid_until",
        "environment_fingerprint",
        "cleanup_state",
        "task_owned_container_count",
        "orphan_process_count",
        "zombie_process_count",
    }
    if not required <= set(readiness):
        raise ValidationStateError("readiness_contract_incomplete")
    if readiness["status"] != "PASS" or readiness["cleanup_state"] != "PASS":
        raise ValidationStateError("readiness_not_pass")
    checked_at = _parse_utc(readiness["checked_at"], "readiness_checked_at")
    valid_until = _parse_utc(readiness["valid_until"], "readiness_valid_until")
    lifetime = (valid_until - checked_at).total_seconds()
    if lifetime <= 0 or lifetime > 3600:
        raise ValidationStateError("readiness_validity_window_invalid")
    if enforce_fresh:
        now = datetime.now(timezone.utc)
        if checked_at > now.replace(microsecond=0) and (checked_at - now).total_seconds() > 60:
            raise ValidationStateError("readiness_from_future")
        if now >= valid_until:
            raise ValidationStateError("readiness_expired")
    if HEX64.fullmatch(str(readiness["environment_fingerprint"])) is None:
        raise ValidationStateError("environment_fingerprint_invalid")
    for field in ("task_owned_container_count", "orphan_process_count", "zombie_process_count"):
        if readiness[field] != 0:
            raise ValidationStateError("readiness_cleanup_nonzero:" + field)
    actual = readiness_digest(readiness)
    supplied = readiness.get("readiness_sha256")
    if supplied is not None and supplied != actual:
        raise ValidationStateError("readiness_digest_mismatch")
    readiness["readiness_sha256"] = actual
    return readiness


class CheckpointStore:
    """Append-only checkpoint ledger with strict resume semantics."""

    def __init__(self, root: pathlib.Path | str):
        self.root = pathlib.Path(root).absolute()
        self.plan_path = self.root / "plan.json"
        self.events_path = self.root / "events"
        if self.root.is_symlink():
            raise ValidationStateError("state_root_symlink_forbidden")
        if self.events_path.exists() and self.events_path.is_symlink():
            raise ValidationStateError("events_symlink_forbidden")

    @classmethod
    def initialize(
        cls,
        root: pathlib.Path | str,
        *,
        policy: Any,
        change: Any,
        bindings: Any,
        tasks: Sequence[str] | None = None,
    ) -> "CheckpointStore":
        store = cls(root)
        if store.plan_path.exists() or store.events_path.exists():
            raise ValidationStateError("state_already_initialized")
        policy_check = validation_tiering.validate_policy(policy)
        if not policy_check["valid"]:
            raise ValidationStateError("policy_invalid:" + policy_check["reason"])
        tier_plan = validation_tiering.build_plan(change, policy)
        normalized_tasks = _validate_tasks(tasks if tasks is not None else tier_plan["gates"])
        normalized_bindings = _validate_bindings(bindings)
        payload = {
            "schema": PLAN_SCHEMA,
            "created_at": _utc_now(),
            "policy_digest": policy_check["actual_digest"],
            "classifier_version": policy["classifier_version"],
            "tier": tier_plan["tier"],
            "reasons": tier_plan["reasons"],
            "fail_closed": tier_plan["fail_closed"],
            "terminal_certification": tier_plan["terminal_certification"],
            "tasks": normalized_tasks,
            "bindings": normalized_bindings,
            "change_sha256": canonical_digest(change),
        }
        payload["plan_id"] = canonical_digest(payload)
        store.root.mkdir(parents=True, exist_ok=False)
        store.events_path.mkdir()
        _write_new_json(store.plan_path, payload)
        store.inspect()
        return store

    def _load_plan(self) -> dict[str, Any]:
        if self.plan_path.is_symlink():
            raise ValidationStateError("plan_symlink_forbidden")
        plan = _load_json(self.plan_path)
        if not isinstance(plan, Mapping) or plan.get("schema") != PLAN_SCHEMA:
            raise ValidationStateError("plan_schema_invalid")
        plan = dict(plan)
        supplied = plan.pop("plan_id", None)
        if not isinstance(supplied, str) or supplied != canonical_digest(plan):
            raise ValidationStateError("plan_digest_mismatch")
        plan["plan_id"] = supplied
        if plan.get("tier") not in TIERS:
            raise ValidationStateError("plan_tier_invalid")
        plan["tasks"] = _validate_tasks(plan.get("tasks"))
        plan["bindings"] = _validate_bindings(plan.get("bindings"))
        return plan

    def _event_files(self) -> list[pathlib.Path]:
        if not self.events_path.is_dir():
            raise ValidationStateError("events_directory_missing")
        children = sorted(self.events_path.iterdir(), key=lambda item: item.name)
        for child in children:
            if child.is_symlink() or not child.is_file() or re.fullmatch(r"\d{8}\.json", child.name) is None:
                raise ValidationStateError("unexpected_event_entry:" + child.name)
        return children

    def _validate_receipt(
        self,
        receipt_value: Any,
        *,
        plan: Mapping[str, Any],
        expected_task: str,
        session: Mapping[str, Any] | None,
        predecessor: str,
        identities: dict[str, set[str]],
    ) -> dict[str, Any]:
        if not isinstance(receipt_value, Mapping):
            raise ValidationStateError("receipt_not_mapping")
        receipt = dict(receipt_value)
        required = {
            "task_id",
            "status",
            "complete",
            "immutable",
            "cleanup_state",
            "run_id",
            "challenge_nonce",
            "process_identity_nonce",
            "session_id",
            "readiness_sha256",
            "bindings",
            "predecessor_receipt_sha256",
            "evidence_sha256",
            "receipt_sha256",
        }
        if not required <= set(receipt):
            raise ValidationStateError("receipt_contract_incomplete")
        if receipt["task_id"] != expected_task:
            raise ValidationStateError("receipt_out_of_order")
        if receipt["status"] not in {"PASS", "FAIL", "BLOCKED"}:
            raise ValidationStateError("receipt_status_invalid")
        if receipt["complete"] is not True or receipt["immutable"] is not True:
            raise ValidationStateError("receipt_not_complete_immutable")
        if receipt["cleanup_state"] != "PASS":
            raise ValidationStateError("receipt_cleanup_not_pass")
        if receipt["bindings"] != plan["bindings"]:
            raise ValidationStateError("receipt_binding_mismatch")
        if receipt["predecessor_receipt_sha256"] != predecessor:
            raise ValidationStateError("receipt_predecessor_mismatch")
        if HEX64.fullmatch(str(receipt["evidence_sha256"])) is None:
            raise ValidationStateError("receipt_evidence_digest_invalid")
        if session is None:
            raise ValidationStateError("session_required_before_result")
        if receipt["session_id"] != session["session_id"]:
            raise ValidationStateError("receipt_session_mismatch")
        if receipt["readiness_sha256"] != session["readiness"]["readiness_sha256"]:
            raise ValidationStateError("receipt_readiness_mismatch")
        for field in ("run_id", "challenge_nonce", "process_identity_nonce"):
            value = receipt[field]
            if not isinstance(value, str) or not value:
                raise ValidationStateError("receipt_identity_invalid:" + field)
            if value in identities[field]:
                raise ValidationStateError("receipt_identity_reused:" + field)
        if receipt["receipt_sha256"] != receipt_digest(receipt):
            raise ValidationStateError("receipt_digest_mismatch")
        return receipt

    def inspect(self) -> dict[str, Any]:
        plan = self._load_plan()
        tasks = plan["tasks"]
        task_index = 0
        previous_event = "ROOT"
        predecessor_receipt = "ROOT"
        current_session: dict[str, Any] | None = None
        passed_receipts: list[str] = []
        attempts = 0
        identities = {
            "run_id": set(),
            "challenge_nonce": set(),
            "process_identity_nonce": set(),
        }
        files = self._event_files()
        for sequence, path in enumerate(files):
            if path.name != f"{sequence:08d}.json":
                raise ValidationStateError("event_sequence_gap")
            event = _load_json(path)
            if not isinstance(event, Mapping) or event.get("schema") != EVENT_SCHEMA:
                raise ValidationStateError("event_schema_invalid")
            event = dict(event)
            event_sha256 = event.pop("event_sha256", None)
            if event.get("sequence") != sequence or event.get("plan_id") != plan["plan_id"]:
                raise ValidationStateError("event_identity_mismatch")
            if event.get("previous_event_sha256") != previous_event:
                raise ValidationStateError("event_chain_mismatch")
            if event_sha256 != canonical_digest(event):
                raise ValidationStateError("event_digest_mismatch")
            event["event_sha256"] = event_sha256
            event_type = event.get("event_type")
            if event_type == "SESSION":
                readiness = _validate_readiness(event.get("readiness"), enforce_fresh=False)
                if readiness != event.get("readiness"):
                    raise ValidationStateError("stored_readiness_not_canonical")
                session_id = event.get("session_id")
                if not isinstance(session_id, str) or not session_id:
                    raise ValidationStateError("session_id_invalid")
                if any(
                    _load_json(previous).get("session_id") == session_id
                    for previous in files[:sequence]
                ):
                    raise ValidationStateError("session_id_reused")
                current_session = event
            elif event_type == "RESULT":
                if task_index >= len(tasks):
                    raise ValidationStateError("result_after_completion")
                receipt = self._validate_receipt(
                    event.get("receipt"),
                    plan=plan,
                    expected_task=tasks[task_index],
                    session=current_session,
                    predecessor=predecessor_receipt,
                    identities=identities,
                )
                for field in identities:
                    identities[field].add(receipt[field])
                attempts += 1
                if receipt["status"] == "PASS":
                    predecessor_receipt = receipt["receipt_sha256"]
                    passed_receipts.append(predecessor_receipt)
                    task_index += 1
            else:
                raise ValidationStateError("event_type_invalid")
            previous_event = str(event_sha256)
        complete = task_index == len(tasks)
        return {
            "verdict": "PASS",
            "plan_id": plan["plan_id"],
            "tier": plan["tier"],
            "task_count": len(tasks),
            "completed_task_count": task_index,
            "attempt_count": attempts,
            "next_task": None if complete else tasks[task_index],
            "complete": complete,
            "event_count": len(files),
            "latest_event_sha256": previous_event,
            "latest_session_id": current_session.get("session_id") if current_session else None,
            "predecessor_receipt_sha256": predecessor_receipt,
            "unique_pass_receipt_count": len(set(passed_receipts)),
        }

    def _append_event(self, event_type: str, payload: Mapping[str, Any]) -> dict[str, Any]:
        state = self.inspect()
        plan = self._load_plan()
        sequence = state["event_count"]
        event = {
            "schema": EVENT_SCHEMA,
            "sequence": sequence,
            "plan_id": plan["plan_id"],
            "previous_event_sha256": state["latest_event_sha256"],
            "event_type": event_type,
            "recorded_at": _utc_now(),
            **dict(payload),
        }
        event["event_sha256"] = canonical_digest(event)
        _write_new_json(self.events_path / f"{sequence:08d}.json", event)
        self.inspect()
        return event

    def open_session(self, readiness: Any, *, session_id: str | None = None) -> dict[str, Any]:
        state = self.inspect()
        if state["complete"]:
            raise ValidationStateError("plan_already_complete")
        normalized = _validate_readiness(readiness, enforce_fresh=True)
        value = session_id or "session-" + uuid.uuid4().hex
        if not isinstance(value, str) or not value:
            raise ValidationStateError("session_id_invalid")
        existing = {
            event.get("session_id")
            for event in (_load_json(path) for path in self._event_files())
            if isinstance(event, Mapping) and event.get("event_type") == "SESSION"
        }
        if value in existing:
            raise ValidationStateError("session_id_reused")
        return self._append_event(
            "SESSION", {"session_id": value, "readiness": normalized}
        )

    def record_result(self, receipt: Any) -> dict[str, Any]:
        state = self.inspect()
        if state["complete"]:
            raise ValidationStateError("plan_already_complete")
        if state["latest_session_id"] is None:
            raise ValidationStateError("session_required_before_result")
        plan = self._load_plan()
        events = [_load_json(path) for path in self._event_files()]
        current_session = next(
            (
                event
                for event in reversed(events)
                if isinstance(event, Mapping) and event.get("event_type") == "SESSION"
            ),
            None,
        )
        identities = {
            field: {
                event["receipt"][field]
                for event in events
                if isinstance(event, Mapping)
                and event.get("event_type") == "RESULT"
                and isinstance(event.get("receipt"), Mapping)
            }
            for field in ("run_id", "challenge_nonce", "process_identity_nonce")
        }
        self._validate_receipt(
            receipt,
            plan=plan,
            expected_task=state["next_task"],
            session=current_session,
            predecessor=state["predecessor_receipt_sha256"],
            identities=identities,
        )
        return self._append_event("RESULT", {"receipt": dict(receipt)})

    def resume_decision(
        self,
        *,
        policy: Any,
        change: Any,
        bindings: Any,
    ) -> dict[str, Any]:
        state = self.inspect()
        plan = self._load_plan()
        policy_check = validation_tiering.validate_policy(policy)
        if not policy_check["valid"] or policy_check["actual_digest"] != plan["policy_digest"]:
            return {"decision": "BLOCKED", "reason": "policy_authority_mismatch", **state}
        try:
            current_bindings = _validate_bindings(bindings)
        except ValidationStateError as exc:
            return {"decision": "BLOCKED", "reason": str(exc), **state}
        if current_bindings != plan["bindings"]:
            return {"decision": "BLOCKED", "reason": "immutable_binding_mismatch", **state}
        current = validation_tiering.build_plan(change, policy)
        if TIERS.index(current["tier"]) > TIERS.index(plan["tier"]):
            return {
                "decision": "BLOCKED",
                "reason": "tier_upgrade_requires_new_plan",
                "required_tier": current["tier"],
                **state,
            }
        if canonical_digest(change) != plan["change_sha256"]:
            return {
                "decision": "BLOCKED",
                "reason": "change_definition_mismatch_no_downgrade",
                "effective_tier": plan["tier"],
                "classified_tier": current["tier"],
                **state,
            }
        if state["complete"]:
            return {"decision": "COMPLETE", "reason": "all_tasks_pass", **state}
        return {
            "decision": "RESUME",
            "reason": "exact_bindings_and_checkpoint_chain_match",
            "effective_tier": plan["tier"],
            "classified_tier": current["tier"],
            **state,
        }


__all__ = [
    "CheckpointStore",
    "ValidationStateError",
    "canonical_bytes",
    "canonical_digest",
    "readiness_digest",
    "receipt_digest",
]
