"""Durable, process-serialized hash-chain recording and signed audit packets."""

from __future__ import annotations

import errno
import json
import os
import threading
import time
from collections.abc import Callable, Iterator, Mapping
from contextlib import contextmanager
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, BinaryIO

from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

from .canonical import canonical_json, sha256_bytes, sha256_value
from .crypto import TrustStore, sign_envelope, verify_envelope
from .errors import LedgerError
from .models import AuditPacket, SignedEnvelope

GENESIS_HASH = "0" * 64


def _now() -> datetime:
    return datetime.now(UTC)


@contextmanager
def file_lock(path: Path, *, write: bool, timeout_seconds: float = 10) -> Iterator[BinaryIO]:
    """Cooperative local-filesystem lock. Readers need no write permission."""
    try:
        handle = path.open("a+b" if write else "rb")
    except OSError as exc:
        raise LedgerError("flight recorder or lock file is not accessible") from exc
    acquired = False
    deadline = time.monotonic() + timeout_seconds
    try:
        while not acquired:
            try:
                handle.seek(0)
                os.lseek(handle.fileno(), 0, os.SEEK_SET)
                if os.name == "nt":
                    import msvcrt

                    mode = msvcrt.LK_NBLCK if write else msvcrt.LK_NBRLCK
                    msvcrt.locking(handle.fileno(), mode, 1)
                else:
                    import fcntl

                    mode = fcntl.LOCK_EX if write else fcntl.LOCK_SH
                    fcntl.flock(handle.fileno(), mode | fcntl.LOCK_NB)
                acquired = True
            except OSError as exc:
                if exc.errno not in {errno.EACCES, errno.EAGAIN, errno.EDEADLK}:
                    raise LedgerError("flight recorder locking failed") from exc
                if time.monotonic() >= deadline:
                    raise LedgerError("flight recorder is busy") from exc
                time.sleep(0.005)
        yield handle
    finally:
        try:
            if acquired:
                handle.seek(0)
                # BufferedReader.seek can stay inside its read buffer without
                # moving the OS descriptor used by msvcrt.locking.
                os.lseek(handle.fileno(), 0, os.SEEK_SET)
                if os.name == "nt":
                    import msvcrt

                    msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
                else:
                    import fcntl

                    fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
        finally:
            handle.close()


def _scan(handle: BinaryIO, *, allow_empty: bool = False) -> dict[str, Any]:
    handle.seek(0)
    previous = GENESIS_HASH
    count = 0
    decisions: dict[str, int] = {}
    pending: dict[str, list[str | None]] = {}
    closed = None
    try:
        for line_number, line in enumerate(handle, start=1):
            if closed and closed.get("closure_policy") == "settled-executions-v1":
                raise LedgerError("events follow a sealed run closure")
            if not line.strip():
                raise LedgerError(f"blank flight recorder entry at line {line_number}")
            record = json.loads(line)
            if not isinstance(record, dict):
                raise LedgerError(f"flight recorder entry is not an object at line {line_number}")
            claimed = record.pop("event_sha256", None)
            if type(record.get("sequence")) is not int or record["sequence"] != count + 1:
                raise LedgerError(f"non-contiguous sequence at line {line_number}")
            if record.get("previous_sha256") != previous:
                raise LedgerError(f"broken hash chain at line {line_number}")
            computed = sha256_value(record)
            if claimed != computed:
                raise LedgerError(f"event digest mismatch at line {line_number}")
            payload = record.get("payload")
            if not isinstance(payload, dict) or not isinstance(record.get("event_type"), str):
                raise LedgerError(f"invalid event contract at line {line_number}")
            event = record["event_type"]
            request_id = payload.get("request_id")
            if event == "decision" and isinstance(payload.get("decision"), str):
                decision = payload["decision"]
                decisions[decision] = decisions.get(decision, 0) + 1
            elif event == "ticket_consumed" and isinstance(request_id, str):
                pending.setdefault(request_id, []).append(payload.get("proposal_sha256"))
            elif event == "execution" and isinstance(request_id, str) and pending.get(request_id):
                digest = payload.get("proposal_sha256")
                if digest is None:
                    pending[request_id].pop(0)  # historical record contract
                elif digest in pending[request_id]:
                    pending[request_id].remove(digest)
                if not pending[request_id]:
                    del pending[request_id]
            elif event == "run_closed":
                closed = payload
            previous = computed
            count += 1
    except (UnicodeError, ValueError, TypeError) as exc:
        if isinstance(exc, LedgerError):
            raise
        raise LedgerError("flight recorder is not valid canonical JSONL") from exc
    if count == 0 and not allow_empty:
        raise LedgerError("flight recorder is empty")
    return {
        "valid": True,
        "event_count": count,
        "head_sha256": previous,
        "decision_counts": decisions,
        "_pending": pending,
        "_closed": closed,
    }


def verify_ledger(path: str | Path) -> dict[str, Any]:
    ledger_path = Path(path)
    if not ledger_path.exists():
        raise LedgerError("flight recorder does not exist")
    with file_lock(ledger_path, write=False) as handle:
        status = _scan(handle)
    # Preserve the public v1 chain-verification contract. A closure's stronger
    # settled-execution policy is explicitly recorded in its final event.
    return {key: value for key, value in status.items() if not key.startswith("_")}


class FlightRecorder:
    def __init__(self, path: str | Path, *, clock: Callable[[], datetime] = _now):
        self.path = Path(path)
        self.path.parent.mkdir(parents=True, exist_ok=True)
        self.clock = clock
        self._lock = threading.RLock()
        with file_lock(self.path, write=True) as handle:
            _scan(handle, allow_empty=True)

    @property
    def head_sha256(self) -> str:
        with self._lock, file_lock(self.path, write=False) as handle:
            return str(_scan(handle, allow_empty=True)["head_sha256"])

    @property
    def event_count(self) -> int:
        with self._lock, file_lock(self.path, write=False) as handle:
            return int(_scan(handle, allow_empty=True)["event_count"])

    def append(self, event_type: str, payload: Mapping[str, Any]) -> str:
        if not isinstance(event_type, str) or not event_type or not isinstance(payload, Mapping):
            raise LedgerError("event type and object payload are required")
        # Snapshot the payload once, so caller mutation cannot separate the
        # hash input from the bytes eventually written to the recorder.
        try:
            frozen_payload = json.loads(canonical_json(dict(payload)))
        except (ValueError, TypeError) as exc:
            raise LedgerError("event payload must be finite canonical JSON") from exc
        with self._lock, file_lock(self.path, write=True) as handle:
            status = _scan(handle, allow_empty=True)
            if status["_closed"] is not None:
                if event_type == "run_closed" and status["_closed"] == frozen_payload:
                    return str(status["head_sha256"])
                raise LedgerError("flight recorder is closed")
            if event_type == "run_closed" and status["_pending"]:
                raise LedgerError("consumed tickets still await execution outcomes")
            if event_type == "execution":
                pending = status["_pending"].get(frozen_payload.get("request_id"), [])
                digest = frozen_payload.get("proposal_sha256")
                if not pending or (digest is not None and digest not in pending):
                    raise LedgerError("execution does not match a consumed ticket")
            record = {
                "sequence": status["event_count"] + 1,
                "recorded_at": self.clock().astimezone(UTC).isoformat(),
                "event_type": event_type,
                "payload": frozen_payload,
                "previous_sha256": status["head_sha256"],
            }
            event_sha256 = sha256_bytes(canonical_json(record))
            encoded = canonical_json({**record, "event_sha256": event_sha256}) + b"\n"
            try:
                handle.seek(0, os.SEEK_END)
                handle.write(encoded)
                handle.flush()
                os.fsync(handle.fileno())
            except OSError as exc:
                raise LedgerError("could not append to flight recorder") from exc
            return event_sha256


def build_audit_packet(
    *,
    recorder: FlightRecorder,
    run_id: str,
    mandate_id: str,
    manifest_sha256: str,
    claim_boundary: str,
    private_key: Ed25519PrivateKey,
    key_id: str,
    generated_at: datetime | None = None,
) -> SignedEnvelope:
    status = verify_ledger(recorder.path)
    packet = AuditPacket(
        run_id=run_id,
        mandate_id=mandate_id,
        manifest_sha256=manifest_sha256,
        generated_at=(generated_at or _now()).astimezone(UTC),
        ledger_head_sha256=status["head_sha256"],
        event_count=status["event_count"],
        decision_counts=status["decision_counts"],
        claim_boundary=claim_boundary,
    )
    return sign_envelope(packet.model_dump(mode="json"), private_key, key_id)


def verify_audit_packet(
    envelope: SignedEnvelope | Mapping[str, Any], trust: TrustStore, ledger_path: str | Path
) -> AuditPacket:
    payload = verify_envelope(envelope, trust)
    packet = AuditPacket.model_validate(payload)
    status = verify_ledger(ledger_path)
    if packet.ledger_head_sha256 != status["head_sha256"]:
        raise LedgerError("audit packet is bound to a different ledger head")
    if packet.event_count != status["event_count"]:
        raise LedgerError("audit packet event count does not match ledger")
    if packet.decision_counts != status["decision_counts"]:
        raise LedgerError("audit packet decision summary does not match ledger")
    return packet
