"""Core backup execution logic for containers."""

from __future__ import annotations

import hashlib
import logging
import tempfile
import uuid
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING

import yaml

from gaol.backup.exceptions import (
    BackupExecutionError,
    IncrementalNotAvailableError,
    RestoreError,
    VerificationError,
)
from gaol.backup.models import (
    BackupMetadata,
    BackupResult,
    BackupStatus,
    BackupType,
    RestoreResult,
    StorageBackendType,
)
from gaol.backup.storage.base import StorageBackend

if TYPE_CHECKING:
    from gaol.cow.manager import CoWManager
    from gaol.models.container import ContainerConfig
    from gaol.runtimes.base import ContainerRuntime
    from gaol.store import StoredContainer


logger = logging.getLogger(__name__)


class BackupExecutor:
    """Execute backup operations on containers.

    Handles the low-level execution of backup/restore operations,
    integrating with runtimes and CoW manager.

    Example:
        >>> executor = BackupExecutor(runtime, storage)
        >>> result = executor.create_full_backup(
        ...     container_id, "mycontainer", "backup-2024-01-13"
        ... )
    """

    def __init__(
        self,
        runtime: ContainerRuntime,
        storage: StorageBackend,
        cow_manager: CoWManager | None = None,
    ) -> None:
        """Initialize the executor.

        Args:
            runtime: Container runtime for filesystem export
            storage: Storage backend for backup files
            cow_manager: Optional CoW manager for incremental backups
        """
        self.runtime = runtime
        self.storage = storage
        self.cow_manager = cow_manager

    def create_full_backup(
        self,
        container_id: str,
        container_name: str,
        backup_name: str,
        compression: str = "gzip",
        pre_commands: list[str] | None = None,
        post_commands: list[str] | None = None,
    ) -> tuple[BackupResult, BackupMetadata | None]:
        """Create a full filesystem backup.

        Uses the runtime's export_filesystem() method to create a
        complete backup of the container's filesystem.

        Args:
            container_id: Container ID
            container_name: Container name
            backup_name: Human-readable backup name
            compression: Compression type (gzip, xz, none)
            pre_commands: Commands to run before backup
            post_commands: Commands to run after backup

        Returns:
            Tuple of (BackupResult, BackupMetadata or None on failure)
        """
        backup_id = str(uuid.uuid4())
        result = BackupResult(
            container_name=container_name,
            backup_name=backup_name,
            backup_id=backup_id,
            backup_type=BackupType.FULL,
        )

        metadata: BackupMetadata | None = None

        try:
            # Run pre-backup commands
            if pre_commands:
                result.status = BackupStatus.PREPARING
                self._run_commands(container_id, pre_commands)

            result.status = BackupStatus.SNAPSHOTTING

            # Get container state
            container_running = False
            try:
                info = self.runtime.inspect(container_id)
                container_running = info.state.lower() == "running"
            except Exception:
                pass

            # Export filesystem to temp directory
            with tempfile.TemporaryDirectory() as tmpdir:
                comp = compression if compression != "none" else None
                export_path = self.runtime.export_filesystem(
                    container_id,
                    tmpdir,
                    compression=comp,
                )
                export_file = Path(export_path)

                result.size_bytes = export_file.stat().st_size
                result.checksum = self._calculate_checksum(export_file)

                result.status = BackupStatus.UPLOADING

                # Determine file extension
                if compression == "gzip":
                    ext = ".tar.gz"
                elif compression == "xz":
                    ext = ".tar.xz"
                else:
                    ext = ".tar"

                # Upload to storage
                remote_path = f"{container_name}/{backup_name}{ext}"
                storage_path = self.storage.upload(
                    export_file,
                    remote_path,
                    metadata={
                        "container": container_name,
                        "type": "full",
                        "backup_id": backup_id,
                    },
                )
                result.storage_path = storage_path

            # Run post-backup commands
            if post_commands:
                self._run_commands(container_id, post_commands)

            result.status = BackupStatus.COMPLETED
            result.completed_at = datetime.now()

            # Create metadata
            metadata = BackupMetadata(
                backup_id=backup_id,
                container_name=container_name,
                backup_name=backup_name,
                backup_type=BackupType.FULL,
                created_at=result.started_at,
                completed_at=result.completed_at,
                size_bytes=result.size_bytes,
                compression=compression,
                storage_backend=StorageBackendType.LOCAL
                if self.storage.name == "local"
                else StorageBackendType.S3,
                storage_path=remote_path,
                checksum=result.checksum,
                verified=False,
                container_was_running=container_running,
            )

        except Exception as e:
            result.status = BackupStatus.FAILED
            result.error_message = str(e)
            result.completed_at = datetime.now()
            logger.error(f"Backup failed for {container_name}: {e}")

        return result, metadata

    def create_config_backup(
        self,
        container_config: ContainerConfig,
        container_name: str,
        backup_name: str,
    ) -> tuple[BackupResult, BackupMetadata | None]:
        """Create a config-only backup.

        Exports just the container configuration as YAML.

        Args:
            container_config: Container configuration to backup
            container_name: Container name
            backup_name: Human-readable backup name

        Returns:
            Tuple of (BackupResult, BackupMetadata or None on failure)
        """
        backup_id = str(uuid.uuid4())
        result = BackupResult(
            container_name=container_name,
            backup_name=backup_name,
            backup_id=backup_id,
            backup_type=BackupType.CONFIG_ONLY,
        )

        metadata: BackupMetadata | None = None

        try:
            result.status = BackupStatus.PREPARING

            with tempfile.TemporaryDirectory() as tmpdir:
                # Export config as YAML
                config_path = Path(tmpdir) / f"{backup_name}.yaml"
                config_data = container_config.model_dump(mode="json")

                with open(config_path, "w") as f:
                    yaml.dump(config_data, f, default_flow_style=False)

                result.size_bytes = config_path.stat().st_size
                result.checksum = self._calculate_checksum(config_path)

                result.status = BackupStatus.UPLOADING

                # Upload to storage
                remote_path = f"{container_name}/{backup_name}.yaml"
                storage_path = self.storage.upload(
                    config_path,
                    remote_path,
                    metadata={"container": container_name, "type": "config"},
                )
                result.storage_path = storage_path

            result.status = BackupStatus.COMPLETED
            result.completed_at = datetime.now()

            metadata = BackupMetadata(
                backup_id=backup_id,
                container_name=container_name,
                backup_name=backup_name,
                backup_type=BackupType.CONFIG_ONLY,
                created_at=result.started_at,
                completed_at=result.completed_at,
                size_bytes=result.size_bytes,
                storage_backend=StorageBackendType.LOCAL
                if self.storage.name == "local"
                else StorageBackendType.S3,
                storage_path=remote_path,
                checksum=result.checksum,
                verified=False,
            )

        except Exception as e:
            result.status = BackupStatus.FAILED
            result.error_message = str(e)
            result.completed_at = datetime.now()
            logger.error(f"Config backup failed for {container_name}: {e}")

        return result, metadata

    def create_incremental_backup(
        self,
        pool: str,
        dataset: str,
        snapshot_name: str,
        base_snapshot: str | None,
        container_name: str,
        backup_name: str,
    ) -> tuple[BackupResult, BackupMetadata | None]:
        """Create an incremental backup using CoW send.

        Uses the CoW manager to create a snapshot and send the
        incremental data stream.

        Args:
            pool: CoW pool name
            dataset: Dataset name
            snapshot_name: Name for the new snapshot
            base_snapshot: Previous snapshot for incremental (None for full)
            container_name: Container name
            backup_name: Human-readable backup name

        Returns:
            Tuple of (BackupResult, BackupMetadata or None on failure)
        """
        if self.cow_manager is None:
            raise IncrementalNotAvailableError(
                container_name, "CoW manager not available"
            )

        backup_id = str(uuid.uuid4())
        result = BackupResult(
            container_name=container_name,
            backup_name=backup_name,
            backup_id=backup_id,
            backup_type=BackupType.INCREMENTAL,
        )

        metadata: BackupMetadata | None = None

        try:
            result.status = BackupStatus.SNAPSHOTTING

            # Create snapshot
            self.cow_manager.create_snapshot(pool, dataset, snapshot_name)

            result.status = BackupStatus.UPLOADING

            # Send to temp file
            with tempfile.TemporaryDirectory() as tmpdir:
                stream_path = Path(tmpdir) / f"{backup_name}.stream"
                self.cow_manager.send(
                    pool,
                    dataset,
                    snapshot_name,
                    stream_path,
                    base_snapshot=base_snapshot,
                )

                result.size_bytes = stream_path.stat().st_size
                result.checksum = self._calculate_checksum(stream_path)

                # Upload
                remote_path = f"{container_name}/{backup_name}.stream"
                storage_path = self.storage.upload(
                    stream_path,
                    remote_path,
                    metadata={
                        "container": container_name,
                        "type": "incremental",
                        "base": base_snapshot or "",
                    },
                )
                result.storage_path = storage_path

            result.status = BackupStatus.COMPLETED
            result.completed_at = datetime.now()

            metadata = BackupMetadata(
                backup_id=backup_id,
                container_name=container_name,
                backup_name=backup_name,
                backup_type=BackupType.INCREMENTAL,
                created_at=result.started_at,
                completed_at=result.completed_at,
                size_bytes=result.size_bytes,
                is_incremental=base_snapshot is not None,
                cow_snapshot=snapshot_name,
                storage_backend=StorageBackendType.LOCAL
                if self.storage.name == "local"
                else StorageBackendType.S3,
                storage_path=remote_path,
                checksum=result.checksum,
                verified=False,
            )

        except Exception as e:
            result.status = BackupStatus.FAILED
            result.error_message = str(e)
            result.completed_at = datetime.now()
            logger.error(f"Incremental backup failed for {container_name}: {e}")

        return result, metadata

    def restore_full_backup(
        self,
        stored: StoredContainer,
        backup_metadata: BackupMetadata,
        create_safety_snapshot: bool = True,
    ) -> RestoreResult:
        """Restore from a full backup.

        Args:
            stored: Stored container info
            backup_metadata: Metadata of backup to restore
            create_safety_snapshot: Create snapshot before restore

        Returns:
            RestoreResult with operation outcome
        """
        container_name = stored.config.name
        result = RestoreResult(
            container_name=container_name,
            backup_name=backup_metadata.backup_name,
            backup_id=backup_metadata.backup_id,
        )

        try:
            result.status = BackupStatus.PREPARING

            # Create safety snapshot if requested
            if create_safety_snapshot and stored.runtime_id:
                try:
                    snapshot_name = f"pre-restore-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
                    self.runtime.snapshot_create(stored.runtime_id, snapshot_name)
                    result.pre_restore_snapshot = snapshot_name
                except Exception as e:
                    logger.warning(f"Could not create safety snapshot: {e}")

            # Download backup
            with tempfile.TemporaryDirectory() as tmpdir:
                # Determine extension from storage path
                ext = ".tar.gz"
                if backup_metadata.storage_path.endswith(".tar.xz"):
                    ext = ".tar.xz"
                elif backup_metadata.storage_path.endswith(".tar"):
                    ext = ".tar"

                local_path = Path(tmpdir) / f"restore{ext}"

                self.storage.download(backup_metadata.storage_path, local_path)

                # Import filesystem
                result.status = BackupStatus.VERIFYING
                self.runtime.import_filesystem(stored.config, str(local_path))

            result.status = BackupStatus.COMPLETED
            result.completed_at = datetime.now()

        except Exception as e:
            result.status = BackupStatus.FAILED
            result.error_message = str(e)
            result.completed_at = datetime.now()
            raise RestoreError(
                container_name, backup_metadata.backup_name, str(e)
            ) from e

        return result

    def verify_backup(self, backup_metadata: BackupMetadata) -> bool:
        """Verify backup integrity.

        Downloads the backup and verifies its checksum.

        Args:
            backup_metadata: Metadata of backup to verify

        Returns:
            True if verification passed

        Raises:
            VerificationError: If verification fails
        """
        if not backup_metadata.checksum:
            raise VerificationError(
                backup_metadata.backup_name, "No checksum stored"
            )

        with tempfile.TemporaryDirectory() as tmpdir:
            local_path = Path(tmpdir) / "verify"

            try:
                self.storage.download(backup_metadata.storage_path, local_path)
            except Exception as e:
                raise VerificationError(
                    backup_metadata.backup_name, f"Download failed: {e}"
                ) from e

            actual_checksum = self._calculate_checksum(local_path)

            if actual_checksum != backup_metadata.checksum:
                raise VerificationError(
                    backup_metadata.backup_name,
                    f"Checksum mismatch: expected {backup_metadata.checksum}, "
                    f"got {actual_checksum}",
                )

        return True

    def _calculate_checksum(self, path: Path) -> str:
        """Calculate SHA256 checksum of a file."""
        sha256 = hashlib.sha256()
        with open(path, "rb") as f:
            for chunk in iter(lambda: f.read(8192), b""):
                sha256.update(chunk)
        return sha256.hexdigest()

    def _run_commands(self, container_id: str, commands: list[str]) -> None:
        """Run commands in a container."""
        for cmd in commands:
            exit_code, stdout, stderr = self.runtime.exec(
                container_id,
                ["sh", "-c", cmd],
            )
            if exit_code != 0:
                raise BackupExecutionError(
                    f"Pre/post command failed: {cmd}",
                    output=stderr or stdout,
                )
