"""Crash recovery with world-state preservation.

``RecoveryManager`` takes periodic snapshots of a ``MockWorldAdapter``'s
state and persists them to a JSONL file.  On restart, the last clean
snapshot can be loaded to rebuild adapter state and minimise data loss.

Usage::

    mgr = RecoveryManager("var/recovery.jsonl")
    snap = mgr.snapshot(adapter)          # save state
    ...
    mgr.restore(adapter)                  # restore latest on restart
"""

from __future__ import annotations

import json
import logging
from dataclasses import asdict, dataclass, field
from datetime import UTC, datetime
from pathlib import Path

from infrastructure.world.adapters.mock import MockWorldAdapter

logger = logging.getLogger(__name__)


@dataclass
class WorldSnapshot:
    """Serialisable snapshot of a world adapter's state.

    Attributes:
        snapshot_id:  Unique identifier (ISO timestamp by default).
        timestamp:    ISO-8601 string of when the snapshot was taken.
        location:     World location at snapshot time.
        entities:     Entities present at snapshot time.
        events:       Recent events at snapshot time.
        metadata:     Arbitrary extra payload from the adapter's ``raw`` field.
    """

    snapshot_id: str
    timestamp: str
    location: str = ""
    entities: list[str] = field(default_factory=list)
    events: list[str] = field(default_factory=list)
    metadata: dict = field(default_factory=dict)


class RecoveryManager:
    """Snapshot-based crash recovery for world adapters.

    Snapshots are appended to a JSONL file; the most recent entry is
    used when restoring.  Old snapshots beyond *max_snapshots* are
    trimmed automatically.

    Parameters
    ----------
    state_path:
        Path to the JSONL file where snapshots are stored.
    max_snapshots:
        Maximum number of snapshots to retain.
    """

    def __init__(
        self,
        state_path: Path | str,
        *,
        max_snapshots: int = 50,
    ) -> None:
        self._path = Path(state_path)
        self._max = max_snapshots
        self._path.parent.mkdir(parents=True, exist_ok=True)

    # -- snapshot ----------------------------------------------------------

    def snapshot(
        self,
        adapter: MockWorldAdapter,
        *,
        snapshot_id: str | None = None,
    ) -> WorldSnapshot:
        """Snapshot *adapter* state and persist to disk.

        Returns the ``WorldSnapshot`` that was saved.
        """
        perception = adapter.observe()
        sid = snapshot_id or datetime.now(UTC).strftime("%Y%m%dT%H%M%S%f")
        snap = WorldSnapshot(
            snapshot_id=sid,
            timestamp=datetime.now(UTC).isoformat(),
            location=perception.location,
            entities=list(perception.entities),
            events=list(perception.events),
            metadata=dict(perception.raw),
        )
        self._append(snap)
        logger.info("RecoveryManager: snapshot %s saved to %s", sid, self._path)
        return snap

    # -- restore -----------------------------------------------------------

    def restore(
        self,
        adapter: MockWorldAdapter,
        *,
        snapshot_id: str | None = None,
    ) -> WorldSnapshot | None:
        """Restore *adapter* from a snapshot.

        Parameters
        ----------
        snapshot_id:
            If given, restore from that specific snapshot ID.
            Otherwise restore from the most recent snapshot.

        Returns the ``WorldSnapshot`` used to restore, or ``None`` if none found.
        """
        history = self.load_history()
        if not history:
            logger.warning("RecoveryManager: no snapshots found at %s", self._path)
            return None

        if snapshot_id is None:
            snap_data = history[0]  # most recent
        else:
            snap_data = next(
                (s for s in history if s["snapshot_id"] == snapshot_id),
                None,
            )

        if snap_data is None:
            logger.warning("RecoveryManager: snapshot %s not found", snapshot_id)
            return None

        snap = WorldSnapshot(**snap_data)
        adapter._location = snap.location
        adapter._entities = list(snap.entities)
        adapter._events = list(snap.events)
        logger.info("RecoveryManager: restored from snapshot %s", snap.snapshot_id)
        return snap

    # -- history -----------------------------------------------------------

    def load_history(self) -> list[dict]:
        """Return all snapshots as dicts, most recent first."""
        if not self._path.exists():
            return []
        records: list[dict] = []
        for line in self._path.read_text().strip().splitlines():
            try:
                records.append(json.loads(line))
            except json.JSONDecodeError:
                continue
        return list(reversed(records))

    def latest(self) -> WorldSnapshot | None:
        """Return the most recent snapshot, or ``None``."""
        history = self.load_history()
        if not history:
            return None
        return WorldSnapshot(**history[0])

    @property
    def snapshot_count(self) -> int:
        """Number of snapshots currently on disk."""
        return len(self.load_history())

    # -- internal ----------------------------------------------------------

    def _append(self, snap: WorldSnapshot) -> None:
        with self._path.open("a") as f:
            f.write(json.dumps(asdict(snap)) + "\n")
        self._trim()

    def _trim(self) -> None:
        """Keep only the last *max_snapshots* lines."""
        lines = [ln for ln in self._path.read_text().strip().splitlines() if ln.strip()]
        if len(lines) > self._max:
            lines = lines[-self._max :]
            self._path.write_text("\n".join(lines) + "\n")
