"""Thread-safe Event Stream logger."""

from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from threading import Lock
from typing import Any

from stipul.chronicle.events.models import CanonicalEvent
from stipul.chronicle.events.store import EventStore
from stipul.writ.proxy.session_head import read_session_head, write_session_head
from stipul.chronicle.signing.keys import RuntimeKeyPair
from stipul.chronicle.signing.signer import sign_event
from stipul.utils.canonical import canonical_json_bytes, compute_prev_hash

_EVENT_HASH_EXCLUDED_FIELDS = frozenset(
    {
        "event_hash",
        "signature",
        "prev_hash",
        "sequence_id",
        "key_id",
        "key_created_at",
        "algorithm",
        "agent_identity",
    }
)
_LIFECYCLE_EVENT_TYPES = frozenset({"session_open", "session_close"})
_LIFECYCLE_NULL_FIELDS = frozenset(
    {
        "tool_name",
        "risk_class",
        "decision",
        "input_hash",
        "tool_input",
        "rule_triggered",
        "lifecycle_hash",
        "metadata",
    }
)


class EventWriteError(Exception):
    """Raised when appending an event to storage fails."""


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


def _hash_full_event_payload(event: dict[str, Any]) -> str:
    return hashlib.sha256(canonical_json_bytes(event)).hexdigest()


def compute_event_hash(event: dict[str, Any]) -> str:
    """Return the canonical body hash for a Chronicle event."""
    if not isinstance(event, dict):
        raise TypeError("event must be a dictionary")
    payload = {
        key: value
        for key, value in event.items()
        if key not in _EVENT_HASH_EXCLUDED_FIELDS
    }
    return hashlib.sha256(canonical_json_bytes(payload)).hexdigest()


@dataclass
class EventLogger:
    """Generate, sign, and append canonical events with monotonic sequence ids."""

    store: EventStore
    session_id: str
    contract_id: str
    contract_hash: str
    signing_key: RuntimeKeyPair
    state_dir: Path
    prev_chain_terminal_hash: str | None = None
    prev_session_id: str | None = None
    _lock: Lock = field(default_factory=Lock, init=False, repr=False)
    _sequence_id: int = field(default=0, init=False)
    _last_event_timestamp: str | None = field(default=None, init=False)
    _last_event_hash: str | None = field(default=None, init=False)
    _last_attestation: dict[str, Any] | None = field(default=None, init=False)
    _pending_prev_unsigned_terminal_hash: str | None = field(default=None, init=False)

    def __post_init__(self) -> None:
        self.state_dir = Path(self.state_dir)
        self._bootstrap_state()

    @property
    def last_event_timestamp(self) -> str | None:
        return self._last_event_timestamp

    @property
    def has_events(self) -> bool:
        return self._sequence_id > 0

    @property
    def last_attestation(self) -> dict[str, Any] | None:
        """Return the most recent authoritative event attestation snapshot."""
        if self._last_attestation is None:
            return None
        return dict(self._last_attestation)

    def _bootstrap_state(self) -> None:
        head = read_session_head(self.state_dir)
        if head is not None and head.get("session_id") == self.session_id:
            self._sequence_id = int(head["sequence_id"])
            self._last_event_hash = str(head["event_hash"]).lower()
            return

        # No usable head: start a fresh signed chain and only retain sequence continuity.
        if not self.store.path.exists():
            return

        last_sequence = 0
        last_timestamp: str | None = None
        saw_signed = False
        last_unsigned_hash: str | None = None

        with self.store.path.open("r", encoding="utf-8") as handle:
            for raw_line in handle:
                line = raw_line.strip()
                if not line:
                    continue
                try:
                    event = json.loads(line)
                except json.JSONDecodeError:
                    continue
                if not isinstance(event, dict):
                    continue
                seq = event.get("sequence_id")
                if isinstance(seq, int) and seq > last_sequence:
                    last_sequence = seq
                    timestamp = event.get("timestamp")
                    if isinstance(timestamp, str):
                        last_timestamp = timestamp
                if event.get("signature") is None:
                    last_unsigned_hash = _hash_full_event_payload(event)
                else:
                    saw_signed = True

        self._sequence_id = last_sequence
        self._last_event_timestamp = last_timestamp
        if not saw_signed and last_unsigned_hash is not None:
            self._pending_prev_unsigned_terminal_hash = last_unsigned_hash

    def _read_file_session_id(self) -> str | None:
        if not self.store.path.exists():
            return None
        with self.store.path.open("r", encoding="utf-8") as handle:
            for raw_line in handle:
                line = raw_line.strip()
                if not line:
                    continue
                try:
                    payload = json.loads(line)
                except Exception as exc:
                    raise EventWriteError("events.jsonl first line is malformed JSON") from exc
                if not isinstance(payload, dict):
                    raise EventWriteError("events.jsonl first line must be a JSON object")
                value = payload.get("session_id")
                if not isinstance(value, str) or not value:
                    raise EventWriteError("events.jsonl first line missing session_id")
                return value
        return None

    def _assert_session_boundary(self) -> None:
        file_session_id = self._read_file_session_id()
        if file_session_id is None:
            return
        if file_session_id != self.session_id:
            raise EventWriteError(
                "fatal write error: events.jsonl session_id mismatch. "
                f"file has '{file_session_id}', logger has '{self.session_id}'."
            )

    def _build_payload(self, event_kwargs: dict[str, Any], sequence_id: int) -> dict[str, Any]:
        payload = dict(event_kwargs)
        payload["sequence_id"] = sequence_id
        payload["timestamp"] = _now_iso_utc()
        payload["session_id"] = self.session_id
        payload["contract_id"] = self.contract_id
        payload["contract_hash"] = self.contract_hash
        payload["key_id"] = self.signing_key.key_id
        payload["algorithm"] = self.signing_key.algorithm
        payload["key_created_at"] = self.signing_key.key_created_at
        payload["prev_hash"] = self._last_event_hash or self.contract_hash

        if self._last_event_hash is None:
            if self._pending_prev_unsigned_terminal_hash is not None:
                payload["prev_unsigned_terminal_hash"] = self._pending_prev_unsigned_terminal_hash
            else:
                payload.pop("prev_unsigned_terminal_hash", None)

            if self.prev_chain_terminal_hash is not None and self.prev_session_id is not None:
                payload["prev_chain_terminal_hash"] = self.prev_chain_terminal_hash
                payload["prev_session_id"] = self.prev_session_id
            else:
                payload.pop("prev_chain_terminal_hash", None)
                payload.pop("prev_session_id", None)
        else:
            payload.pop("prev_unsigned_terminal_hash", None)
            payload.pop("prev_chain_terminal_hash", None)
            payload.pop("prev_session_id", None)
        return payload

    def log_decision_event(
        self,
        *,
        event_type: str,
        tool_name: str,
        risk_class: str,
        decision: str,
        reason: str,
        agent_identity: str,
        input_hash: str | None,
        tool_input: dict[str, Any] | None = None,
        rule_triggered: str | None = None,
        lifecycle_hash: str | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> CanonicalEvent:
        """Append a decision-bearing proxy event to the authoritative stream."""
        payload: dict[str, Any] = {
            "event_type": event_type,
            "tool_name": tool_name,
            "risk_class": risk_class,
            "decision": decision,
            "reason": reason,
            "agent_identity": agent_identity,
            "tool_input": dict(tool_input) if isinstance(tool_input, dict) else None,
        }
        if input_hash is not None:
            payload["input_hash"] = input_hash
        if rule_triggered is not None:
            payload["rule_triggered"] = rule_triggered
        if lifecycle_hash is not None:
            payload["lifecycle_hash"] = lifecycle_hash
        if metadata is not None:
            payload["metadata"] = dict(metadata)
        return self.log_event(payload)

    def log_event(self, event_kwargs: dict[str, Any]) -> CanonicalEvent:
        """Validate, sequence, sign, persist, and return a canonical event."""
        with self._lock:
            self._assert_session_boundary()

            expected_sequence = self._sequence_id + 1
            if "sequence_id" in event_kwargs and event_kwargs["sequence_id"] != expected_sequence:
                raise ValueError(
                    f"sequence_id gap detected: expected {expected_sequence}, "
                    f"got {event_kwargs['sequence_id']}"
                )

            if "session_id" in event_kwargs and event_kwargs["session_id"] != self.session_id:
                raise EventWriteError(
                    f"fatal write error: event session_id {event_kwargs['session_id']} "
                    f"does not match logger session_id {self.session_id}"
                )

            normalized_kwargs = dict(event_kwargs)
            metadata = normalized_kwargs.get("metadata")
            if isinstance(metadata, dict):
                normalized_kwargs["metadata"] = dict(metadata)
            if "tool_input" not in normalized_kwargs:
                normalized_kwargs["tool_input"] = None
            if "rule_triggered" not in normalized_kwargs:
                normalized_kwargs["rule_triggered"] = None
            if "lifecycle_hash" not in normalized_kwargs:
                normalized_kwargs["lifecycle_hash"] = None
            tool_input = normalized_kwargs.get("tool_input")
            if isinstance(tool_input, dict):
                normalized_kwargs["tool_input"] = dict(tool_input)

            payload = self._build_payload(normalized_kwargs, expected_sequence)
            preserve_null_fields = set()
            if "tool_input" in payload:
                preserve_null_fields.add("tool_input")
            if "rule_triggered" in payload:
                preserve_null_fields.add("rule_triggered")
            if "lifecycle_hash" in payload:
                preserve_null_fields.add("lifecycle_hash")
            if payload.get("event_type") in _LIFECYCLE_EVENT_TYPES:
                preserve_null_fields.update(_LIFECYCLE_NULL_FIELDS)
            canonical_payload = {
                key: value
                for key, value in payload.items()
                if value is not None or key in preserve_null_fields
            }
            # event_hash excludes event_hash, signature, prev_hash, sequence_id, key_id,
            # key_created_at, algorithm, and agent_identity.
            # Ordering requirement: compute the hash from the canonical event body before
            # adding event_hash to the persisted payload, then sign and serialize the
            # event with event_hash included.
            event_hash = compute_event_hash(canonical_payload)
            canonical_payload["event_hash"] = event_hash
            canonical_payload["signature"] = sign_event(
                canonical_payload,
                self.signing_key.private_key,
            )
            event = CanonicalEvent(**canonical_payload)
            event_dict = event.to_dict()
            line = canonical_json_bytes(event_dict).decode("utf-8")

            try:
                self.store.append(line)
                write_session_head(self.state_dir, event_dict)
            except Exception as exc:
                raise EventWriteError("failed to append signed event") from exc

            chain_hash = compute_prev_hash(event_dict)
            self._sequence_id = expected_sequence
            self._last_event_timestamp = event.timestamp
            self._last_event_hash = chain_hash
            self._last_attestation = {
                "kind": "chronicle_attestation",
                "event_hash": chain_hash,
                "sequence_id": event.sequence_id,
                "timestamp": event.timestamp,
                "session_id": event.session_id,
                "event_type": event.event_type,
                "tool_name": event.tool_name,
                "decision": event.decision,
                "reason": event.reason,
                "contract_id": event.contract_id,
                "contract_hash": event.contract_hash,
                "prev_hash": event.prev_hash,
                "signature": event.signature,
                "key_id": event.key_id,
                "algorithm": event.algorithm,
                "key_created_at": event.key_created_at,
            }
            if "prev_unsigned_terminal_hash" in event_dict:
                self._pending_prev_unsigned_terminal_hash = None
            return event
