"""
Backup and rollback functionality.
"""

import contextlib
import json
import shutil
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any

from persuade.logging import get_logger

logger = get_logger()


@dataclass
class BackupInfo:
    """Information about a backup."""

    timestamp: str
    created_at: str
    file_count: int
    backup_path: Path

    @classmethod
    def from_metadata(cls, metadata: dict[str, Any]) -> "BackupInfo":
        """Create BackupInfo from metadata dictionary."""
        return cls(
            timestamp=metadata["timestamp"],
            created_at=metadata["created_at"],
            file_count=metadata["file_count"],
            backup_path=Path(metadata["backup_path"]),
        )


class BackupError(Exception):
    """Backup-related error."""

    pass


class RestoreError(Exception):
    """Restore-related error."""

    pass


class BackupManager:
    """Manages backups and rollbacks."""

    def __init__(self, target_path: Path) -> None:
        self.target_path = target_path
        self.backup_dir = target_path / ".claude" / ".backup"
        self.backup_dir.mkdir(parents=True, exist_ok=True)

    def create_backup(self) -> Path:
        """
        Create timestamped backup of entire .claude directory.

        Returns:
            Path to backup directory
        """
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        backup_path = self.backup_dir / timestamp

        claude_dir = self.target_path / ".claude"

        if not claude_dir.exists():
            raise BackupError(f".claude/ directory not found: {claude_dir}")

        # Copy entire .claude directory (excluding .backup itself)
        def ignore_backup(dir: str, files: list[str]) -> list[str]:
            """Ignore .backup directory during copy."""
            if dir == str(claude_dir):
                return [".backup", "__pycache__", "*.pyc"]
            return []

        try:
            shutil.copytree(
                claude_dir,
                backup_path,
                ignore=ignore_backup,
                symlinks=True,  # Preserve symlinks
            )
        except OSError as e:
            raise BackupError(f"Failed to create backup: {e}") from e

        # Create backup metadata
        file_count = sum(1 for _ in backup_path.rglob("*") if _.is_file())

        metadata = {
            "timestamp": timestamp,
            "source": str(claude_dir),
            "backup_path": str(backup_path),
            "created_at": datetime.now().isoformat(),
            "file_count": file_count,
        }

        metadata_path = backup_path / ".backup_metadata.json"
        with open(metadata_path, "w") as f:
            json.dump(metadata, f, indent=2)

        return backup_path

    def list_backups(self) -> list[BackupInfo]:
        """List all available backups, newest first."""
        backups: list[BackupInfo] = []

        if not self.backup_dir.exists():
            return backups

        for backup_dir in sorted(self.backup_dir.iterdir(), reverse=True):
            if not backup_dir.is_dir():
                continue

            metadata_path = backup_dir / ".backup_metadata.json"
            if metadata_path.exists():
                try:
                    with open(metadata_path) as f:
                        metadata = json.load(f)
                    backups.append(BackupInfo.from_metadata(metadata))
                except (json.JSONDecodeError, KeyError):
                    # Skip corrupted backup metadata
                    continue

        return backups

    def restore_backup(self, timestamp: str | None = None) -> Path:
        """
        Restore from backup.

        Args:
            timestamp: Specific backup timestamp, or None for latest

        Returns:
            Path to restored backup
        """
        if timestamp:
            backup_path = self.backup_dir / timestamp
        else:
            # Get latest backup
            backups = self.list_backups()
            if not backups:
                raise BackupError("No backups available")
            backup_path = backups[0].backup_path

        if not backup_path.exists():
            raise BackupError(f"Backup not found: {timestamp}")

        # Create safety backup before restore
        logger.info("Creating safety backup before restore")
        try:
            safety_backup = self.create_backup()
            logger.info("Safety backup created", backup_name=safety_backup.name)
        except BackupError:
            # If can't create safety backup, ask user to confirm
            logger.warning("Could not create safety backup")

        try:
            # Remove current .claude directory (except .backup)
            claude_dir = self.target_path / ".claude"

            for item in claude_dir.iterdir():
                if item.name == ".backup":
                    continue

                if item.is_dir():
                    shutil.rmtree(item)
                else:
                    item.unlink()

            # Restore from backup
            for item in backup_path.iterdir():
                if item.name == ".backup_metadata.json":
                    continue

                target = claude_dir / item.name

                if item.is_dir():
                    shutil.copytree(item, target, symlinks=True)
                else:
                    shutil.copy2(item, target)

            return backup_path

        except OSError as e:
            # Try to restore from safety backup
            if "safety_backup" in locals():
                logger.warning("Restore failed, attempting to recover from safety backup")
                with contextlib.suppress(OSError):
                    self.restore_backup(safety_backup.name)

            raise RestoreError(f"Restore failed: {e}") from e

    def cleanup_old_backups(self, keep: int = 10) -> None:
        """
        Keep only N most recent backups.

        Args:
            keep: Number of backups to keep
        """
        backups = self.list_backups()

        for backup in backups[keep:]:
            try:
                shutil.rmtree(backup.backup_path)
            except OSError as e:
                logger.warning("Could not delete backup", timestamp=backup.timestamp, error=str(e))

    def get_backup_info(self, timestamp: str) -> BackupInfo | None:
        """Get info for a specific backup."""
        backup_path = self.backup_dir / timestamp
        metadata_path = backup_path / ".backup_metadata.json"

        if not metadata_path.exists():
            return None

        try:
            with open(metadata_path) as f:
                metadata = json.load(f)
            return BackupInfo.from_metadata(metadata)
        except (json.JSONDecodeError, KeyError):
            return None
