"""Rollback manifest management for agentmux sync/install/update operations.

Each sync/install/update writes a JSON manifest to ``~/.agentmux/rollback/``
describing every file that was created or updated.  The ``rollback`` command
reads the most recent manifest and restores the previous state.
"""

import json
from dataclasses import asdict, dataclass
from datetime import datetime
from pathlib import Path
from typing import Any, Literal

__all__ = [
    "RollbackEntry",
    "RollbackManifest",
    "save_manifest",
    "load_latest_manifest",
    "list_manifests",
    "list_setup_names",
]

_ROLLBACK_DIR = Path.home() / ".agentmux" / "rollback"
_MAX_MANIFESTS = 5


@dataclass
class RollbackEntry:
    """Records a single file operation that can be undone.

    Attributes:
        target_path: Absolute path of the file that was created or updated.
        backup_path: Absolute path of the backup file created before an update.
            ``None`` when the file was newly created (rollback = delete).
        action: The action that was taken: ``"created"``, ``"updated"``, or
            ``"skipped"`` (skipped entries are not written to the manifest).
    """

    target_path: str
    backup_path: str | None
    action: Literal["created", "updated"]


@dataclass
class RollbackManifest:
    """Full record of a single sync/install/update operation.

    Attributes:
        timestamp: UTC time at which the operation completed.
        setup_name: Name of the setup that was synced (from ``agentmux.toml``).
        entries: One entry per file that was created or updated.
    """

    timestamp: datetime
    setup_name: str
    entries: list[RollbackEntry]

    def to_dict(self) -> dict[str, Any]:
        data = asdict(self)
        data["timestamp"] = self.timestamp.isoformat()
        return data

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "RollbackManifest":
        try:
            entries = [RollbackEntry(**e) for e in data["entries"]]
            return cls(
                timestamp=datetime.fromisoformat(data["timestamp"]),
                setup_name=data["setup_name"],
                entries=entries,
            )
        except (KeyError, TypeError, ValueError) as exc:
            raise ValueError(f"Invalid rollback manifest: {exc}") from exc


def _manifest_path(setup_name: str, timestamp: datetime) -> Path:
    stamp = timestamp.strftime("%Y%m%d_%H%M%S_%f")
    safe_name = setup_name.replace("/", "_").replace("\\", "_")
    return _ROLLBACK_DIR / f"{stamp}-{safe_name}.json"


def save_manifest(manifest: RollbackManifest) -> Path:
    """Persist *manifest* to ``~/.agentmux/rollback/`` and prune old ones.

    Returns the path where the manifest was written.
    """
    _ROLLBACK_DIR.mkdir(parents=True, exist_ok=True)
    path = _manifest_path(manifest.setup_name, manifest.timestamp)
    path.write_text(json.dumps(manifest.to_dict(), indent=2))
    _prune_manifests(manifest.setup_name)
    return path


def load_latest_manifest(setup_name: str) -> RollbackManifest | None:
    """Return the most recent manifest for *setup_name*, or ``None`` if absent."""
    manifests = list_manifests(setup_name)
    if not manifests:
        return None
    latest = manifests[-1]
    try:
        return RollbackManifest.from_dict(json.loads(latest.read_text()))
    except (json.JSONDecodeError, ValueError) as exc:
        raise ValueError(f"Corrupt manifest at {latest}: {exc}") from exc


def list_manifests(setup_name: str) -> list[Path]:
    """Return all manifest paths for *setup_name*, sorted oldest-first."""
    if not _ROLLBACK_DIR.exists():
        return []
    safe_name = setup_name.replace("/", "_").replace("\\", "_")
    return sorted(
        p
        for p in _ROLLBACK_DIR.glob("*.json")
        if "-" in p.stem and p.stem.split("-", 1)[1] == safe_name
    )


def list_setup_names() -> set[str]:
    """Return the set of setup names that have rollback history."""
    if not _ROLLBACK_DIR.exists():
        return set()
    return {path.stem.split("-", 1)[1] for path in _ROLLBACK_DIR.glob("*.json") if "-" in path.stem}


def _prune_manifests(setup_name: str) -> None:
    """Remove manifests beyond the *_MAX_MANIFESTS* retention limit."""
    manifests = list_manifests(setup_name)
    for old in manifests[:-_MAX_MANIFESTS]:
        old.unlink(missing_ok=True)
