"""World-state backup strategy — timestamped files with rotation.

``WorldStateBackup`` writes each backup as a standalone JSON file and
maintains a ``MANIFEST.jsonl`` index for fast listing.  Old backups
beyond the retention limit are rotated out automatically.

Usage::

    backup = WorldStateBackup("var/backups/", max_backups=10)
    record = backup.create(adapter, notes="pre-phase-8 checkpoint")
    backup.restore(adapter, record.backup_id)
"""

from __future__ import annotations

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

from infrastructure.world.adapters.mock import MockWorldAdapter

logger = logging.getLogger(__name__)


@dataclass
class BackupRecord:
    """Metadata entry written to the backup manifest."""

    backup_id: str
    timestamp: str
    location: str
    entity_count: int
    event_count: int
    size_bytes: int = 0
    notes: str = ""


class WorldStateBackup:
    """Timestamped, rotating world-state backups.

    Each backup is a JSON file named ``backup_<timestamp>.json`` inside
    *backup_dir*.  A ``MANIFEST.jsonl`` index tracks all backups for fast
    listing and rotation.

    Parameters
    ----------
    backup_dir:
        Directory where backup files and the manifest are stored.
    max_backups:
        Maximum number of backup files to retain.
    """

    MANIFEST_NAME = "MANIFEST.jsonl"

    def __init__(
        self,
        backup_dir: Path | str,
        *,
        max_backups: int = 10,
    ) -> None:
        self._dir = Path(backup_dir)
        self._dir.mkdir(parents=True, exist_ok=True)
        self._max = max_backups

    # -- create ------------------------------------------------------------

    def create(
        self,
        adapter: MockWorldAdapter,
        *,
        notes: str = "",
    ) -> BackupRecord:
        """Snapshot *adapter* and write a new backup file.

        Returns the ``BackupRecord`` describing the backup.
        """
        perception = adapter.observe()
        ts = datetime.now(UTC).strftime("%Y%m%dT%H%M%S%f")
        backup_id = f"backup_{ts}"
        payload = {
            "backup_id": backup_id,
            "timestamp": datetime.now(UTC).isoformat(),
            "location": perception.location,
            "entities": list(perception.entities),
            "events": list(perception.events),
            "raw": dict(perception.raw),
            "notes": notes,
        }
        backup_path = self._dir / f"{backup_id}.json"
        backup_path.write_text(json.dumps(payload, indent=2))
        size = backup_path.stat().st_size

        record = BackupRecord(
            backup_id=backup_id,
            timestamp=payload["timestamp"],
            location=perception.location,
            entity_count=len(perception.entities),
            event_count=len(perception.events),
            size_bytes=size,
            notes=notes,
        )
        self._update_manifest(record)
        self._rotate()
        logger.info("WorldStateBackup: created %s (%d bytes)", backup_id, size)
        return record

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

    def restore(self, adapter: MockWorldAdapter, backup_id: str) -> bool:
        """Restore *adapter* state from backup *backup_id*.

        Returns ``True`` on success, ``False`` if the backup file is missing.
        """
        backup_path = self._dir / f"{backup_id}.json"
        if not backup_path.exists():
            logger.warning("WorldStateBackup: backup %s not found", backup_id)
            return False

        payload = json.loads(backup_path.read_text())
        adapter._location = payload.get("location", "")
        adapter._entities = list(payload.get("entities", []))
        adapter._events = list(payload.get("events", []))
        logger.info("WorldStateBackup: restored from %s", backup_id)
        return True

    # -- listing -----------------------------------------------------------

    def list_backups(self) -> list[BackupRecord]:
        """Return all backup records, most recent first."""
        manifest = self._dir / self.MANIFEST_NAME
        if not manifest.exists():
            return []
        records: list[BackupRecord] = []
        for line in manifest.read_text().strip().splitlines():
            try:
                data = json.loads(line)
                records.append(BackupRecord(**data))
            except (json.JSONDecodeError, TypeError):
                continue
        return list(reversed(records))

    def latest(self) -> BackupRecord | None:
        """Return the most recent backup record, or ``None``."""
        backups = self.list_backups()
        return backups[0] if backups else None

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

    def _update_manifest(self, record: BackupRecord) -> None:
        manifest = self._dir / self.MANIFEST_NAME
        with manifest.open("a") as f:
            f.write(json.dumps(asdict(record)) + "\n")

    def _rotate(self) -> None:
        """Remove oldest backups when over the retention limit."""
        backups = self.list_backups()  # most recent first
        if len(backups) <= self._max:
            return
        to_remove = backups[self._max :]
        for rec in to_remove:
            path = self._dir / f"{rec.backup_id}.json"
            try:
                path.unlink(missing_ok=True)
                logger.debug("WorldStateBackup: rotated out %s", rec.backup_id)
            except OSError as exc:
                logger.warning("WorldStateBackup: could not remove %s: %s", path, exc)
        # Rewrite manifest with only the retained backups
        keep = backups[: self._max]
        manifest = self._dir / self.MANIFEST_NAME
        manifest.write_text("\n".join(json.dumps(asdict(r)) for r in reversed(keep)) + "\n")
