"""
RBSP Snapshot Manager.

Create, restore, list, and clean up index snapshots for backup and
replication.  Each snapshot is a directory containing ``meta.json``
(metadata) and ``index.dat`` (JSON-serialized index state).
"""

from __future__ import annotations

import json
import os
import secrets
import shutil
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional


# ---------------------------------------------------------------------------
# Snapshot info
# ---------------------------------------------------------------------------

@dataclass
class SnapshotInfo:
    """Describes a single snapshot on disk."""

    snapshot_id: str
    created_at: float
    index_entries: int
    size_bytes: int
    path: str


# ---------------------------------------------------------------------------
# Snapshot manager
# ---------------------------------------------------------------------------

class SnapshotManager:
    """Manages index snapshots stored under a root directory.

    Each snapshot lives in its own sub-directory named by *snapshot_id*.
    The directory contains:

    * ``meta.json`` -- creation time, entry count, user metadata.
    * ``index.dat`` -- the JSON-serialized index data.
    """

    def __init__(self, snapshot_dir: str | Path) -> None:
        self._root = Path(snapshot_dir)
        self._root.mkdir(parents=True, exist_ok=True)

    # -- public API ---------------------------------------------------------

    def create_snapshot(
        self,
        index_data: Dict[str, Any],
        metadata: Optional[Dict[str, Any]] = None,
    ) -> SnapshotInfo:
        """Serialize *index_data* into a new snapshot directory.

        Returns a :class:`SnapshotInfo` describing the snapshot.
        """
        snapshot_id = self._generate_id()
        snap_dir = self._root / snapshot_id
        snap_dir.mkdir(parents=True, exist_ok=True)

        # Write index data
        index_path = snap_dir / "index.dat"
        index_bytes = json.dumps(index_data, separators=(",", ":")).encode("utf-8")
        index_path.write_bytes(index_bytes)

        # Build metadata
        created_at = time.time()
        meta = {
            "snapshot_id": snapshot_id,
            "created_at": created_at,
            "index_entries": len(index_data),
            "user_metadata": metadata or {},
        }
        meta_path = snap_dir / "meta.json"
        meta_path.write_text(json.dumps(meta, indent=2), encoding="utf-8")

        size_bytes = index_path.stat().st_size + meta_path.stat().st_size

        return SnapshotInfo(
            snapshot_id=snapshot_id,
            created_at=created_at,
            index_entries=len(index_data),
            size_bytes=size_bytes,
            path=str(snap_dir),
        )

    def restore_snapshot(self, snapshot_id: str) -> Dict[str, Any]:
        """Load and return the index data stored in *snapshot_id*.

        Raises :class:`FileNotFoundError` if the snapshot does not exist.
        """
        snap_dir = self._snap_dir(snapshot_id)
        index_path = snap_dir / "index.dat"
        if not index_path.exists():
            raise FileNotFoundError(f"index.dat missing in snapshot {snapshot_id}")
        return json.loads(index_path.read_text(encoding="utf-8"))

    def list_snapshots(self) -> List[SnapshotInfo]:
        """Return all available snapshots sorted by creation time (oldest first)."""
        snapshots: List[SnapshotInfo] = []
        for info in self._iter_snapshots():
            snapshots.append(info)
        snapshots.sort(key=lambda s: s.created_at)
        return snapshots

    def delete_snapshot(self, snapshot_id: str) -> bool:
        """Delete the snapshot directory.  Returns ``True`` on success."""
        snap_dir = self._root / snapshot_id
        if not snap_dir.is_dir():
            return False
        shutil.rmtree(snap_dir)
        return True

    def get_info(self, snapshot_id: str) -> Optional[SnapshotInfo]:
        """Return :class:`SnapshotInfo` for *snapshot_id*, or ``None``."""
        snap_dir = self._root / snapshot_id
        if not snap_dir.is_dir():
            return None
        return self._read_info(snap_dir)

    def cleanup(self, keep_last_n: int = 5) -> None:
        """Delete old snapshots, keeping the *keep_last_n* most recent."""
        snapshots = self.list_snapshots()
        if len(snapshots) <= keep_last_n:
            return
        to_delete = snapshots[: len(snapshots) - keep_last_n]
        for snap in to_delete:
            self.delete_snapshot(snap.snapshot_id)

    # -- internals ----------------------------------------------------------

    @staticmethod
    def _generate_id() -> str:
        ts = str(int(time.time() * 1000))
        suffix = secrets.token_hex(4)
        return f"{ts}-{suffix}"

    def _snap_dir(self, snapshot_id: str) -> Path:
        snap_dir = self._root / snapshot_id
        if not snap_dir.is_dir():
            raise FileNotFoundError(f"Snapshot not found: {snapshot_id}")
        return snap_dir

    def _read_info(self, snap_dir: Path) -> Optional[SnapshotInfo]:
        meta_path = snap_dir / "meta.json"
        if not meta_path.exists():
            return None
        try:
            meta = json.loads(meta_path.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, OSError):
            return None

        # Compute size from files on disk
        size_bytes = 0
        for f in snap_dir.iterdir():
            if f.is_file():
                size_bytes += f.stat().st_size

        return SnapshotInfo(
            snapshot_id=meta["snapshot_id"],
            created_at=meta["created_at"],
            index_entries=meta["index_entries"],
            size_bytes=size_bytes,
            path=str(snap_dir),
        )

    def _iter_snapshots(self):
        if not self._root.exists():
            return
        for child in self._root.iterdir():
            if child.is_dir():
                info = self._read_info(child)
                if info is not None:
                    yield info
