"""Apple Containers runtime implementation using Virtualization.framework."""

from __future__ import annotations

import json
import logging
import platform
import shutil
import subprocess
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any

from gaol.config import get_config_dir
from gaol.models.container import ContainerState, ContainerStatus, NetworkMode
from gaol.profiles.applicator import MergedProfile, get_profile_applicator
from gaol.runtimes.base import ContainerRuntime

if TYPE_CHECKING:
    from gaol.models.container import ContainerConfig, ContainerStats, Snapshot

logger = logging.getLogger(__name__)

# Label prefix for gaol-managed containers (used in metadata)
LABEL_PREFIX = "dev.gaol"


class AppleRuntime(ContainerRuntime):
    """Apple Containers runtime using Virtualization.framework.

    This runtime uses Apple's native `container` CLI tool to manage
    Linux containers as lightweight VMs on Apple Silicon Macs.

    Requirements:
        - macOS 15+ (Sequoia or later)
        - Apple Silicon (arm64)
        - Apple `container` CLI installed
        - container system service running

    Example:
        >>> runtime = AppleRuntime()
        >>> if runtime.is_available():
        ...     container_id = runtime.create(config)
        ...     runtime.start(container_id)
    """

    def __init__(self) -> None:
        """Initialize the Apple Containers runtime."""
        self._metadata_dir = get_config_dir() / "apple" / "containers"
        self._metadata_dir.mkdir(parents=True, exist_ok=True)

    @property
    def name(self) -> str:
        """Return the name of this runtime."""
        return "apple"

    def is_available(self) -> bool:
        """Check if Apple Containers runtime is available.

        Returns:
            True if on macOS with Apple Silicon and container CLI installed
        """
        # Must be macOS
        if platform.system() != "Darwin":
            return False

        # Must be Apple Silicon (arm64)
        if platform.machine() != "arm64":
            return False

        # container CLI must be installed
        if not shutil.which("container"):
            return False

        # Check if container system is running
        try:
            result = subprocess.run(
                ["container", "system", "status"],
                capture_output=True,
                text=True,
                timeout=5,
            )
            return result.returncode == 0
        except (subprocess.TimeoutExpired, FileNotFoundError):
            return False

    def _run_container_cmd(
        self,
        args: list[str],
        *,
        capture: bool = True,
        check: bool = True,
        timeout: int | None = None,
    ) -> subprocess.CompletedProcess[str]:
        """Run a container CLI command.

        Args:
            args: Arguments to pass to container CLI
            capture: Whether to capture output
            check: Whether to raise on non-zero exit
            timeout: Command timeout in seconds

        Returns:
            CompletedProcess with command result

        Raises:
            RuntimeError: If command fails and check=True
        """
        cmd = ["container"] + args
        logger.debug(f"Running: {' '.join(cmd)}")

        try:
            result = subprocess.run(
                cmd,
                capture_output=capture,
                text=True,
                timeout=timeout,
            )

            if check and result.returncode != 0:
                error_msg = result.stderr.strip() if result.stderr else "Unknown error"
                raise RuntimeError(f"container command failed: {error_msg}")

            return result

        except subprocess.TimeoutExpired as e:
            raise RuntimeError(f"container command timed out: {' '.join(args)}") from e
        except FileNotFoundError as e:
            raise RuntimeError("container CLI not found") from e

    def _get_image(self, config: ContainerConfig) -> str:
        """Get the container image to use.

        Args:
            config: Container configuration

        Returns:
            Image name string
        """
        if config.image:
            return config.image

        from gaol.distro import get_docker_image

        try:
            return get_docker_image(config.distro)
        except KeyError:
            # Fall back to debian:{distro} for unknown distros
            return f"debian:{config.distro}"

    def _metadata_path(self, container_name: str) -> Path:
        """Get the metadata file path for a container."""
        return self._metadata_dir / f"{container_name}.json"

    def _save_metadata(
        self,
        container_name: str,
        container_id: str,
        config: ContainerConfig,
        merged_profile: MergedProfile | None = None,
    ) -> None:
        """Save container metadata to local file.

        Args:
            container_name: Name of the container
            container_id: Container ID from CLI
            config: Original container configuration
            merged_profile: Merged profile if profiles were applied
        """
        metadata = {
            "name": container_name,
            "id": container_id,
            "profiles": config.profiles,
            "distro": config.distro,
            "image": self._get_image(config),
            "created_at": datetime.now().isoformat(),
            "runtime": "apple",
            "config": config.model_dump(exclude_none=True),
        }

        if merged_profile:
            metadata["merged_profile"] = {
                "packages": merged_profile.packages,
                "services": merged_profile.services,
                "setup_commands": merged_profile.setup_commands,
            }

        path = self._metadata_path(container_name)
        with open(path, "w") as f:
            json.dump(metadata, f, indent=2, default=str)

        logger.debug(f"Saved metadata for {container_name} to {path}")

    def _load_metadata(self, container_name: str) -> dict[str, Any] | None:
        """Load container metadata from local file.

        Args:
            container_name: Name of the container

        Returns:
            Metadata dict or None if not found
        """
        path = self._metadata_path(container_name)
        if not path.exists():
            return None

        with open(path) as f:
            return json.load(f)

    def _delete_metadata(self, container_name: str) -> None:
        """Delete container metadata file.

        Args:
            container_name: Name of the container
        """
        path = self._metadata_path(container_name)
        if path.exists():
            path.unlink()
            logger.debug(f"Deleted metadata for {container_name}")

    def _resolve_environment(self, env: dict[str, str]) -> dict[str, str]:
        """Resolve ${secret:NAME} references in environment variables.

        Args:
            env: Dictionary of environment variables

        Returns:
            New dictionary with secrets resolved
        """
        from gaol.secrets import has_secrets
        from gaol.secrets.resolver import get_secret_resolver

        # Check if any values have secret references
        has_refs = any(has_secrets(v) for v in env.values())
        if not has_refs:
            return env

        resolver = get_secret_resolver()
        return resolver.resolve_environment(env)

    def _apple_status_to_status(self, status_str: str) -> ContainerStatus:
        """Convert Apple container status string to ContainerStatus.

        Args:
            status_str: Status string from container CLI

        Returns:
            ContainerStatus enum value
        """
        status_map = {
            "created": ContainerStatus.CREATED,
            "running": ContainerStatus.RUNNING,
            "stopped": ContainerStatus.STOPPED,
            "exited": ContainerStatus.EXITED,
            "dead": ContainerStatus.DEAD,
        }
        return status_map.get(status_str.lower(), ContainerStatus.UNKNOWN)

    def _parse_datetime(self, dt_str: str | None) -> datetime | None:
        """Parse datetime string from container CLI.

        Args:
            dt_str: ISO format datetime string

        Returns:
            datetime object or None
        """
        if not dt_str:
            return None
        try:
            return datetime.fromisoformat(dt_str.replace("Z", "+00:00"))
        except (ValueError, TypeError):
            return None

    def create(self, config: ContainerConfig) -> str:
        """Create a new container.

        Args:
            config: Container configuration

        Returns:
            Container ID

        Raises:
            RuntimeError: If container creation fails
        """
        # Apply profiles if specified
        merged_profile: MergedProfile | None = None
        if config.profiles:
            applicator = get_profile_applicator()
            merged_profile = applicator.resolve_and_merge(config.profiles)
            config = applicator.apply_to_config(config, merged_profile)

        image = self._get_image(config)

        # Build container create command
        args = ["create", "--name", config.name]

        # Add environment variables
        resolved_env = self._resolve_environment(config.environment)
        for key, value in resolved_env.items():
            args.extend(["-e", f"{key}={value}"])

        # Add port mappings
        for host_port, container_port in config.network.ports.items():
            args.extend(["-p", f"{host_port}:{container_port}"])

        # Add volume mounts
        for host_path, container_path in config.volumes.items():
            args.extend(["-v", f"{host_path}:{container_path}"])

        # Add resource limits
        if config.resources.cpu_count:
            args.extend(["-c", str(int(config.resources.cpu_count))])
        if config.resources.memory_mb:
            args.extend(["-m", f"{config.resources.memory_mb}m"])

        # Add working directory
        if config.working_dir:
            args.extend(["-w", config.working_dir])

        # Add hostname
        if config.network.hostname:
            # Note: Apple container may not support --hostname, check compatibility
            pass  # Skip for now

        # Add image
        args.append(image)

        # Add command if specified
        if config.command:
            args.extend(config.command)

        # Run container create
        result = self._run_container_cmd(args)

        # Parse container ID from output
        container_id = result.stdout.strip()
        if not container_id:
            # Try to get ID by listing
            container_id = self._get_container_id(config.name) or config.name

        # Save metadata
        self._save_metadata(config.name, container_id, config, merged_profile)

        logger.info(f"Created container {config.name} ({container_id})")
        return container_id

    def _get_container_id(self, name_or_id: str) -> str | None:
        """Get container ID by name.

        Args:
            name_or_id: Container name or ID

        Returns:
            Container ID or None if not found
        """
        try:
            result = self._run_container_cmd(
                ["inspect", "--format", "json", name_or_id],
                check=False,
            )
            if result.returncode == 0 and result.stdout:
                data = json.loads(result.stdout)
                if isinstance(data, list) and len(data) > 0:
                    return data[0].get("Id", data[0].get("ID"))
                elif isinstance(data, dict):
                    return data.get("Id", data.get("ID"))
        except (json.JSONDecodeError, RuntimeError):
            pass
        return None

    def setup(self, container_id: str) -> tuple[int, str, str]:
        """Run profile setup script in a container.

        This should be called after start() to install packages and run setup commands.

        Args:
            container_id: ID of the container

        Returns:
            Tuple of (exit_code, stdout, stderr)
        """
        # Get container name from ID or use ID as name
        container_name = self._get_container_name(container_id)
        metadata = self._load_metadata(container_name) if container_name else None

        if not metadata:
            return 0, "No metadata found, skipping setup\n", ""

        profiles = metadata.get("profiles", [])
        if not profiles:
            return 0, "No profiles to set up\n", ""

        distro = metadata.get("distro", "trixie")

        # Generate setup script
        applicator = get_profile_applicator()
        merged = applicator.resolve_and_merge(profiles)
        setup_script = merged.generate_setup_script(distro)

        # Run setup script in container
        return self.exec(
            container_id,
            ["bash", "-c", setup_script],
            user="root",
        )

    def _get_container_name(self, container_id: str) -> str | None:
        """Get container name from ID.

        Args:
            container_id: Container ID

        Returns:
            Container name or None if not found
        """
        try:
            result = self._run_container_cmd(
                ["inspect", "--format", "json", container_id],
                check=False,
            )
            if result.returncode == 0 and result.stdout:
                data = json.loads(result.stdout)
                if isinstance(data, list) and len(data) > 0:
                    return data[0].get("Name", "").lstrip("/")
                elif isinstance(data, dict):
                    return data.get("Name", "").lstrip("/")
        except (json.JSONDecodeError, RuntimeError):
            pass
        return container_id  # Fall back to using ID as name

    def start(self, container_id: str) -> None:
        """Start a stopped container.

        Args:
            container_id: Container ID or name

        Raises:
            RuntimeError: If start fails
        """
        self._run_container_cmd(["start", container_id])
        logger.info(f"Started container {container_id}")

    def stop(self, container_id: str, timeout: int = 10) -> None:
        """Stop a running container.

        Args:
            container_id: Container ID or name
            timeout: Seconds to wait before force killing

        Raises:
            RuntimeError: If stop fails
        """
        self._run_container_cmd(["stop", "--time", str(timeout), container_id])
        logger.info(f"Stopped container {container_id}")

    def destroy(self, container_id: str, force: bool = False) -> None:
        """Remove a container.

        Args:
            container_id: Container ID or name
            force: Force removal of running container

        Raises:
            RuntimeError: If removal fails
        """
        args = ["delete"]
        if force:
            args.append("--force")
        args.append(container_id)

        # Get container name before deletion for metadata cleanup
        container_name = self._get_container_name(container_id)

        self._run_container_cmd(args)

        # Clean up metadata
        if container_name:
            self._delete_metadata(container_name)

        logger.info(f"Destroyed container {container_id}")

    def exec(
        self,
        container_id: str,
        command: list[str],
        *,
        interactive: bool = False,
        tty: bool = False,
        user: str | None = None,
        workdir: str | None = None,
        environment: dict[str, str] | None = None,
    ) -> tuple[int, str, str]:
        """Execute a command in a running container.

        Args:
            container_id: Container ID or name
            command: Command and arguments to execute
            interactive: Enable interactive mode
            tty: Allocate pseudo-TTY
            user: User to run command as
            workdir: Working directory for command
            environment: Additional environment variables

        Returns:
            Tuple of (exit_code, stdout, stderr)
        """
        args = ["exec"]

        if interactive:
            args.append("-i")
        if tty:
            args.append("-t")
        if user:
            args.extend(["-u", user])
        if workdir:
            args.extend(["-w", workdir])
        if environment:
            for key, value in environment.items():
                args.extend(["-e", f"{key}={value}"])

        args.append(container_id)
        args.append("--")
        args.extend(command)

        result = self._run_container_cmd(args, check=False)

        return result.returncode, result.stdout, result.stderr

    def list(self, all: bool = False) -> list[ContainerState]:
        """List containers.

        Args:
            all: Include stopped containers

        Returns:
            List of ContainerState objects
        """
        args = ["list", "--format", "json"]
        if all:
            args.append("--all")

        result = self._run_container_cmd(args, check=False)

        if result.returncode != 0 or not result.stdout:
            return []

        try:
            containers_data = json.loads(result.stdout)
        except json.JSONDecodeError:
            return []

        if not isinstance(containers_data, list):
            containers_data = [containers_data]

        states = []
        for container in containers_data:
            # Load our metadata if available
            name = container.get("Name", container.get("Names", [""])[0]).lstrip("/")
            metadata = self._load_metadata(name)

            # Only include gaol-managed containers
            if metadata is None:
                continue

            states.append(
                ContainerState(
                    id=container.get("Id", container.get("ID", ""))[:12],
                    name=name,
                    runtime="apple",
                    status=self._apple_status_to_status(
                        container.get("State", container.get("Status", "unknown"))
                    ),
                    image=container.get("Image", ""),
                    created_at=self._parse_datetime(container.get("Created")),
                    started_at=self._parse_datetime(container.get("StartedAt")),
                    finished_at=self._parse_datetime(container.get("FinishedAt")),
                    exit_code=container.get("ExitCode"),
                    error=container.get("Error"),
                    ip_address=container.get("IPAddress"),
                    ports=metadata.get("config", {}).get("network", {}).get("ports", {}),
                    profiles=metadata.get("profiles", []),
                )
            )

        return states

    def status(self, container_id: str) -> ContainerState:
        """Get the status of a container.

        Args:
            container_id: Container ID or name

        Returns:
            ContainerState object

        Raises:
            RuntimeError: If container not found
        """
        result = self._run_container_cmd(
            ["inspect", "--format", "json", container_id],
            check=False,
        )

        if result.returncode != 0:
            raise RuntimeError(f"Container not found: {container_id}")

        try:
            data = json.loads(result.stdout)
            if isinstance(data, list):
                data = data[0] if data else {}
        except json.JSONDecodeError as e:
            raise RuntimeError(f"Failed to parse container info: {e}") from e

        name = data.get("Name", container_id).lstrip("/")
        metadata = self._load_metadata(name)

        state_data = data.get("State", {})

        return ContainerState(
            id=data.get("Id", data.get("ID", container_id))[:12],
            name=name,
            runtime="apple",
            status=self._apple_status_to_status(
                state_data.get("Status", data.get("State", "unknown"))
            ),
            image=data.get("Image", data.get("Config", {}).get("Image", "")),
            created_at=self._parse_datetime(data.get("Created")),
            started_at=self._parse_datetime(state_data.get("StartedAt")),
            finished_at=self._parse_datetime(state_data.get("FinishedAt")),
            exit_code=state_data.get("ExitCode"),
            error=state_data.get("Error"),
            ip_address=data.get("NetworkSettings", {}).get("IPAddress"),
            ports=metadata.get("config", {}).get("network", {}).get("ports", {})
            if metadata
            else {},
            profiles=metadata.get("profiles", []) if metadata else [],
        )

    def logs(
        self,
        container_id: str,
        *,
        follow: bool = False,
        tail: int | None = None,
        since: str | None = None,
    ) -> str:
        """Get container logs.

        Args:
            container_id: Container ID or name
            follow: Follow log output (not supported in non-interactive)
            tail: Number of lines to show from end
            since: Show logs since timestamp

        Returns:
            Log output as string
        """
        args = ["logs"]

        if follow:
            args.append("--follow")
        if tail:
            args.extend(["-n", str(tail)])
        # Note: --since may not be supported, check compatibility

        args.append(container_id)

        result = self._run_container_cmd(args, check=False, timeout=30 if not follow else None)

        return result.stdout + result.stderr

    def stats(self, container_id: str) -> ContainerStats:
        """Get resource usage statistics for a container.

        Args:
            container_id: Container ID or name

        Returns:
            ContainerStats object

        Raises:
            RuntimeError: If stats retrieval fails
        """
        from gaol.models.container import ContainerStats

        result = self._run_container_cmd(
            ["stats", "--format", "json", "--no-stream", container_id],
            check=False,
        )

        if result.returncode != 0:
            raise RuntimeError(f"Failed to get stats for {container_id}")

        try:
            data = json.loads(result.stdout)
            if isinstance(data, list):
                data = data[0] if data else {}
        except json.JSONDecodeError as e:
            raise RuntimeError(f"Failed to parse stats: {e}") from e

        container_name = self._get_container_name(container_id) or container_id

        # Parse CPU percentage
        cpu_str = data.get("CPUPerc", "0%").rstrip("%")
        try:
            cpu_percent = float(cpu_str)
        except ValueError:
            cpu_percent = 0.0

        # Parse memory
        mem_usage = data.get("MemUsage", "0B / 0B")
        mem_parts = mem_usage.split(" / ")
        memory_used = self._parse_size(mem_parts[0]) if mem_parts else 0
        memory_limit = self._parse_size(mem_parts[1]) if len(mem_parts) > 1 else 0

        mem_str = data.get("MemPerc", "0%").rstrip("%")
        try:
            memory_percent = float(mem_str)
        except ValueError:
            memory_percent = 0.0

        # Parse network I/O
        net_io = data.get("NetIO", "0B / 0B")
        net_parts = net_io.split(" / ")
        network_rx = self._parse_size(net_parts[0]) if net_parts else 0
        network_tx = self._parse_size(net_parts[1]) if len(net_parts) > 1 else 0

        # Parse block I/O
        block_io = data.get("BlockIO", "0B / 0B")
        block_parts = block_io.split(" / ")
        block_read = self._parse_size(block_parts[0]) if block_parts else 0
        block_write = self._parse_size(block_parts[1]) if len(block_parts) > 1 else 0

        # PIDs
        pids = data.get("PIDs", 0)
        if isinstance(pids, str):
            try:
                pids = int(pids)
            except ValueError:
                pids = 0

        return ContainerStats(
            container_id=container_id,
            container_name=container_name,
            cpu_percent=round(cpu_percent, 2),
            cpu_count=1,  # Apple VMs typically have single vCPU by default
            memory_used_bytes=memory_used,
            memory_limit_bytes=memory_limit if memory_limit > 0 else None,
            memory_percent=round(memory_percent, 2),
            network_rx_bytes=network_rx,
            network_tx_bytes=network_tx,
            block_read_bytes=block_read,
            block_write_bytes=block_write,
            pids=pids,
        )

    def _parse_size(self, size_str: str) -> int:
        """Parse a size string like '1.5GB' to bytes.

        Args:
            size_str: Size string with unit

        Returns:
            Size in bytes
        """
        size_str = size_str.strip().upper()
        # Check longer suffixes first to avoid matching "B" before "KB"
        units = [
            ("TIB", 1024**4),
            ("GIB", 1024**3),
            ("MIB", 1024**2),
            ("KIB", 1024),
            ("TB", 1024**4),
            ("GB", 1024**3),
            ("MB", 1024**2),
            ("KB", 1024),
            ("B", 1),
        ]

        for unit, multiplier in units:
            if size_str.endswith(unit):
                try:
                    value = float(size_str[: -len(unit)])
                    return int(value * multiplier)
                except ValueError:
                    return 0

        # Try parsing as plain number
        try:
            return int(float(size_str))
        except ValueError:
            return 0

    def copy_to(self, container_id: str, src: str, dest: str) -> None:
        """Copy a file or directory into a container.

        Args:
            container_id: Container ID or name
            src: Local source path
            dest: Container destination path

        Raises:
            RuntimeError: If copy fails
        """
        # Try using container cp if available
        result = self._run_container_cmd(
            ["cp", src, f"{container_id}:{dest}"],
            check=False,
        )

        if result.returncode != 0:
            # Fall back to exec tar method
            import tarfile
            import io
            import base64

            src_path = Path(src)
            if not src_path.exists():
                raise RuntimeError(f"Source path not found: {src}")

            # Create tar archive
            tar_buffer = io.BytesIO()
            with tarfile.open(fileobj=tar_buffer, mode="w") as tar:
                tar.add(src_path, arcname=src_path.name)
            tar_data = base64.b64encode(tar_buffer.getvalue()).decode()

            # Extract in container
            exit_code, stdout, stderr = self.exec(
                container_id,
                [
                    "bash",
                    "-c",
                    f"echo '{tar_data}' | base64 -d | tar xf - -C {dest}",
                ],
            )
            if exit_code != 0:
                raise RuntimeError(f"Failed to copy to container: {stderr}")

        logger.debug(f"Copied {src} to {container_id}:{dest}")

    def copy_from(self, container_id: str, src: str, dest: str) -> None:
        """Copy a file or directory from a container.

        Args:
            container_id: Container ID or name
            src: Container source path
            dest: Local destination path

        Raises:
            RuntimeError: If copy fails
        """
        # Try using container cp if available
        result = self._run_container_cmd(
            ["cp", f"{container_id}:{src}", dest],
            check=False,
        )

        if result.returncode != 0:
            # Fall back to exec tar method
            import tarfile
            import io
            import base64

            # Create tar in container and get base64
            exit_code, stdout, stderr = self.exec(
                container_id,
                ["bash", "-c", f"tar cf - -C $(dirname {src}) $(basename {src}) | base64"],
            )
            if exit_code != 0:
                raise RuntimeError(f"Failed to copy from container: {stderr}")

            # Decode and extract locally
            tar_data = base64.b64decode(stdout.strip())
            tar_buffer = io.BytesIO(tar_data)

            dest_path = Path(dest)
            dest_path.parent.mkdir(parents=True, exist_ok=True)

            with tarfile.open(fileobj=tar_buffer, mode="r") as tar:
                tar.extractall(dest_path.parent)

        logger.debug(f"Copied {container_id}:{src} to {dest}")

    def pause(self, container_id: str) -> None:
        """Pause a running container.

        Note: Apple Containers (VMs) do not support pause like Docker containers.

        Raises:
            NotImplementedError: Always raised as pause is not supported
        """
        raise NotImplementedError(
            "Apple Containers runtime does not support pause. Use stop instead."
        )

    def unpause(self, container_id: str) -> None:
        """Unpause a paused container.

        Note: Apple Containers (VMs) do not support pause/unpause.

        Raises:
            NotImplementedError: Always raised as unpause is not supported
        """
        raise NotImplementedError(
            "Apple Containers runtime does not support unpause. Use start instead."
        )

    def snapshot_create(
        self, container_id: str, name: str, description: str | None = None
    ) -> Snapshot:
        """Create a snapshot of a container.

        For Apple containers, we export the container's image to a tar file.

        Args:
            container_id: Container ID or name
            name: Snapshot name
            description: Optional description

        Returns:
            Snapshot object

        Raises:
            RuntimeError: If snapshot creation fails
        """
        from gaol.models.container import Snapshot

        container_name = self._get_container_name(container_id) or container_id

        # Create snapshot directory
        snapshot_dir = self._metadata_dir / "snapshots" / container_name
        snapshot_dir.mkdir(parents=True, exist_ok=True)

        snapshot_path = snapshot_dir / f"{name}.tar"

        # Stop container if running (needed for consistent snapshot)
        try:
            status = self.status(container_id)
            was_running = status.status == ContainerStatus.RUNNING
            if was_running:
                self.stop(container_id, timeout=30)
        except RuntimeError:
            was_running = False

        # Export container filesystem
        # Note: Apple container may use different export mechanism
        try:
            # Try container export if available
            result = self._run_container_cmd(
                ["export", container_id, "-o", str(snapshot_path)],
                check=False,
            )

            if result.returncode != 0:
                # Fall back to tar-based export
                exit_code, stdout, stderr = self.exec(
                    container_id,
                    ["bash", "-c", "tar cf - /"],
                    user="root",
                )
                if exit_code == 0:
                    with open(snapshot_path, "wb") as f:
                        f.write(stdout.encode() if isinstance(stdout, str) else stdout)
                else:
                    raise RuntimeError(f"Failed to export container: {stderr}")

            # Get snapshot size
            size = snapshot_path.stat().st_size if snapshot_path.exists() else 0

            # Save snapshot metadata
            snapshot_meta = {
                "name": name,
                "container_id": container_id,
                "container_name": container_name,
                "created_at": datetime.now().isoformat(),
                "description": description,
                "size_bytes": size,
                "path": str(snapshot_path),
            }

            meta_path = snapshot_dir / f"{name}.json"
            with open(meta_path, "w") as f:
                json.dump(snapshot_meta, f, indent=2)

            # Restart if was running
            if was_running:
                self.start(container_id)

            return Snapshot(
                id=name,
                name=name,
                container_id=container_id,
                container_name=container_name,
                created_at=datetime.now(),
                size_bytes=size,
                description=description,
            )

        except Exception as e:
            # Try to restart container if we stopped it
            if was_running:
                try:
                    self.start(container_id)
                except RuntimeError:
                    pass
            raise RuntimeError(f"Failed to create snapshot: {e}") from e

    def snapshot_list(self, container_id: str) -> list[Snapshot]:
        """List snapshots for a container.

        Args:
            container_id: Container ID or name

        Returns:
            List of Snapshot objects
        """
        from gaol.models.container import Snapshot

        container_name = self._get_container_name(container_id) or container_id
        snapshot_dir = self._metadata_dir / "snapshots" / container_name

        if not snapshot_dir.exists():
            return []

        snapshots = []
        for meta_path in snapshot_dir.glob("*.json"):
            try:
                with open(meta_path) as f:
                    meta = json.load(f)

                snapshots.append(
                    Snapshot(
                        id=meta.get("name", meta_path.stem),
                        name=meta.get("name", meta_path.stem),
                        container_id=meta.get("container_id", container_id),
                        container_name=meta.get("container_name", container_name),
                        created_at=self._parse_datetime(meta.get("created_at"))
                        or datetime.now(),
                        size_bytes=meta.get("size_bytes"),
                        description=meta.get("description"),
                    )
                )
            except (json.JSONDecodeError, OSError):
                continue

        return sorted(snapshots, key=lambda s: s.created_at, reverse=True)

    def snapshot_restore(self, container_id: str, snapshot_name: str) -> None:
        """Restore a container from a snapshot.

        Args:
            container_id: Container ID or name
            snapshot_name: Name of the snapshot

        Raises:
            RuntimeError: If restore fails
        """
        container_name = self._get_container_name(container_id) or container_id
        snapshot_dir = self._metadata_dir / "snapshots" / container_name

        meta_path = snapshot_dir / f"{snapshot_name}.json"
        snapshot_path = snapshot_dir / f"{snapshot_name}.tar"

        if not meta_path.exists() or not snapshot_path.exists():
            raise RuntimeError(f"Snapshot not found: {snapshot_name}")

        # Load original container config
        container_meta = self._load_metadata(container_name)
        if not container_meta:
            raise RuntimeError(f"Container metadata not found: {container_name}")

        # Stop and destroy current container
        try:
            self.stop(container_id, timeout=10)
        except RuntimeError:
            pass

        self.destroy(container_id, force=True)

        # Import snapshot as new image and recreate container
        # This is a simplified implementation - full restore would need more work
        from gaol.models.container import ContainerConfig

        config_data = container_meta.get("config", {})
        config = ContainerConfig(**config_data)

        # Import the snapshot tar as a new image
        image_name = f"gaol-snapshot/{container_name}:{snapshot_name}"
        self._run_container_cmd(
            ["image", "load", "-i", str(snapshot_path)],
            check=True,
        )

        # Create new container with the restored image
        config.image = image_name
        new_id = self.create(config)
        self.start(new_id)

        logger.info(f"Restored container {container_name} from snapshot {snapshot_name}")

    def snapshot_delete(self, container_id: str, snapshot_name: str) -> None:
        """Delete a snapshot.

        Args:
            container_id: Container ID or name
            snapshot_name: Name of the snapshot

        Raises:
            RuntimeError: If deletion fails
        """
        container_name = self._get_container_name(container_id) or container_id
        snapshot_dir = self._metadata_dir / "snapshots" / container_name

        meta_path = snapshot_dir / f"{snapshot_name}.json"
        snapshot_path = snapshot_dir / f"{snapshot_name}.tar"

        if not meta_path.exists():
            raise RuntimeError(f"Snapshot not found: {snapshot_name}")

        # Delete files
        if snapshot_path.exists():
            snapshot_path.unlink()
        meta_path.unlink()

        logger.info(f"Deleted snapshot {snapshot_name} for {container_name}")

    def export_filesystem(
        self,
        container_id: str,
        dest_path: str,
        *,
        compression: str | None = "gzip",
    ) -> str:
        """Export container filesystem to a tarball.

        Args:
            container_id: Container ID or name
            dest_path: Destination directory
            compression: Compression type (gzip, xz, or None)

        Returns:
            Path to the exported tarball

        Raises:
            RuntimeError: If export fails
        """
        container_name = self._get_container_name(container_id) or container_id

        # Determine output filename
        if compression == "gzip":
            ext = ".tar.gz"
        elif compression == "xz":
            ext = ".tar.xz"
        else:
            ext = ".tar"

        output_path = Path(dest_path) / f"{container_name}-export{ext}"

        # Create tar in container
        exit_code, stdout, stderr = self.exec(
            container_id,
            ["bash", "-c", "tar cf - --exclude=/proc --exclude=/sys --exclude=/dev /"],
            user="root",
        )

        if exit_code != 0:
            raise RuntimeError(f"Failed to export filesystem: {stderr}")

        # Write with compression
        if compression == "gzip":
            import gzip

            with gzip.open(output_path, "wb") as f:
                f.write(stdout.encode() if isinstance(stdout, str) else stdout)
        elif compression == "xz":
            import lzma

            with lzma.open(output_path, "wb") as f:
                f.write(stdout.encode() if isinstance(stdout, str) else stdout)
        else:
            with open(output_path, "wb") as f:
                f.write(stdout.encode() if isinstance(stdout, str) else stdout)

        return str(output_path)

    def import_filesystem(
        self,
        config: ContainerConfig,
        source_path: str,
    ) -> str:
        """Create container by importing filesystem from tarball.

        Args:
            config: Container configuration
            source_path: Path to tarball

        Returns:
            Container ID

        Raises:
            RuntimeError: If import fails
        """
        source = Path(source_path)
        if not source.exists():
            raise RuntimeError(f"Source file not found: {source_path}")

        # Import tarball as image
        image_name = f"gaol-migrated/{config.name}:latest"

        self._run_container_cmd(
            ["image", "load", "-i", str(source)],
            check=True,
        )

        # Create container with imported image
        migrated_config = config.model_copy(update={"image": image_name})
        return self.create(migrated_config)

    def get_filesystem_size(self, container_id: str) -> int:
        """Get approximate filesystem size in bytes.

        Args:
            container_id: Container ID or name

        Returns:
            Size in bytes
        """
        # Get size via du in container
        exit_code, stdout, stderr = self.exec(
            container_id,
            ["bash", "-c", "du -sb / 2>/dev/null | cut -f1"],
            user="root",
        )

        if exit_code == 0 and stdout.strip():
            try:
                return int(stdout.strip())
            except ValueError:
                pass

        return 0

    # Checkpoint/restore not supported on macOS
    def checkpoint_create(
        self,
        container_id: str,
        checkpoint_name: str,
        image_dir: str | None = None,
        *,
        leave_running: bool = False,
        tcp_established: bool = False,
    ) -> Any:
        """Create a checkpoint (not supported on Apple Containers).

        Raises:
            NotImplementedError: Always raised as CRIU is not available on macOS
        """
        raise NotImplementedError(
            "Checkpoint/restore is not supported on Apple Containers. "
            "Use snapshots instead."
        )

    def checkpoint_restore(
        self,
        container_id: str,
        checkpoint_name: str,
        image_dir: str | None = None,
        *,
        tcp_established: bool = False,
    ) -> Any:
        """Restore from checkpoint (not supported on Apple Containers).

        Raises:
            NotImplementedError: Always raised as CRIU is not available on macOS
        """
        raise NotImplementedError(
            "Checkpoint/restore is not supported on Apple Containers. "
            "Use snapshots instead."
        )

    def checkpoint_list(self, container_id: str) -> list[str]:
        """List checkpoints (not supported on Apple Containers).

        Returns:
            Empty list as checkpoints are not supported
        """
        return []

    def checkpoint_delete(
        self,
        container_id: str,
        checkpoint_name: str,
        image_dir: str | None = None,
    ) -> bool:
        """Delete checkpoint (not supported on Apple Containers).

        Returns:
            False as checkpoints are not supported
        """
        return False

    def checkpoint_capabilities(self) -> dict[str, bool]:
        """Get checkpoint capabilities.

        Returns:
            Dictionary indicating checkpoint is not supported
        """
        return {
            "available": False,
            "reason": "CRIU not available on macOS",
            "checkpoint_supported": False,
            "tcp_established": False,
            "leave_running": False,
        }
