"""Typer CLI for gaol."""

from __future__ import annotations

from contextvars import ContextVar
from pathlib import Path
from typing import Annotated

import typer

# Context variable for remote host execution
_current_host: ContextVar[str | None] = ContextVar("current_host", default=None)
from rich.console import Console
from rich.table import Table

from gaol import __version__
from gaol.config import get_config
from gaol.models.container import ContainerConfig, ContainerStatus, NetworkConfig, NetworkMode
from gaol.runtimes import get_runtime as _get_runtime
from gaol.runtimes import list_available_runtimes
from gaol.runtimes.base import ContainerRuntime
from gaol.store import get_container_store

app = typer.Typer(
    name="gaol",
    help="Cross-platform container and VM management tool",
    no_args_is_help=True,
)
console = Console()
err_console = Console(stderr=True)

# Help panel group names for organizing commands
PANEL_LIFECYCLE = "Container Lifecycle"
PANEL_EXECUTION = "Execution"
PANEL_FILES = "Files & Data"
PANEL_CONFIGURATION = "Configuration"
PANEL_STORAGE = "Storage"
PANEL_NETWORKING = "Networking"
PANEL_DEVELOPMENT = "Development"
PANEL_SHELL = "Desktop & Shell"
PANEL_INFRASTRUCTURE = "Infrastructure"
PANEL_MONITORING = "Monitoring & Operations"
PANEL_ORCHESTRATION = "Orchestration"

# Configuration subcommands
profiles_app = typer.Typer(help="Manage container profiles")
app.add_typer(profiles_app, name="profiles", rich_help_panel=PANEL_CONFIGURATION)

spec_app = typer.Typer(help="Container specification management")
app.add_typer(spec_app, name="spec", rich_help_panel=PANEL_CONFIGURATION)

secret_app = typer.Typer(
    help="Manage secrets for containers. "
    "Under sudo, the invoking user's config directory is used automatically. "
    "Set GAOL_MASTER_KEY (base64 or hex) to provide the master key via environment "
    "instead of the system keyring."
)
app.add_typer(secret_app, name="secret", rich_help_panel=PANEL_CONFIGURATION)

template_app = typer.Typer(help="Template gallery management")
app.add_typer(template_app, name="template", rich_help_panel=PANEL_CONFIGURATION)

# Storage subcommands
snapshot_app = typer.Typer(help="Manage container snapshots")
app.add_typer(snapshot_app, name="snapshot", rich_help_panel=PANEL_STORAGE)

checkpoint_app = typer.Typer(help="CRIU checkpoint/restore management")
app.add_typer(checkpoint_app, name="checkpoint", rich_help_panel=PANEL_STORAGE)

diff_app = typer.Typer(help="Compare container filesystems")
app.add_typer(diff_app, name="diff", rich_help_panel=PANEL_STORAGE)

volume_app = typer.Typer(help="Encrypted volume management")
app.add_typer(volume_app, name="volume", rich_help_panel=PANEL_STORAGE)

cow_app = typer.Typer(help="Copy-on-Write filesystem management")
app.add_typer(cow_app, name="cow", rich_help_panel=PANEL_STORAGE)

backup_app = typer.Typer(help="Container backup management")
app.add_typer(backup_app, name="backup", rich_help_panel=PANEL_STORAGE)

# Networking subcommands
network_app = typer.Typer(help="Network management and diagnostics")
app.add_typer(network_app, name="network", rich_help_panel=PANEL_NETWORKING)

tunnel_app = typer.Typer(help="Expose containers to the internet via tunnels")
app.add_typer(tunnel_app, name="tunnel", rich_help_panel=PANEL_NETWORKING)

# Development subcommands
build_app = typer.Typer(help="Build images from containers")
app.add_typer(build_app, name="build", rich_help_panel=PANEL_DEVELOPMENT)

brew_app = typer.Typer(help="Homebrew packages in containers")
app.add_typer(brew_app, name="brew", rich_help_panel=PANEL_DEVELOPMENT)

# Desktop & Shell subcommands
desktop_app = typer.Typer(help="Desktop integration for containerized GUI apps")
app.add_typer(desktop_app, name="desktop", rich_help_panel=PANEL_SHELL)

shell_int_app = typer.Typer(help="Shell integration (completions, prompt)")
app.add_typer(shell_int_app, name="shell-integration", rich_help_panel=PANEL_SHELL)

# Infrastructure subcommands
remote_app = typer.Typer(help="Remote host management")
app.add_typer(remote_app, name="remote", rich_help_panel=PANEL_INFRASTRUCTURE)

gpu_app = typer.Typer(help="GPU management and passthrough")
app.add_typer(gpu_app, name="gpu", rich_help_panel=PANEL_INFRASTRUCTURE)

# Monitoring & Operations subcommands
logs_app = typer.Typer(help="Advanced log aggregation and search")
app.add_typer(logs_app, name="log", rich_help_panel=PANEL_MONITORING)

update_app = typer.Typer(help="Container update management")
app.add_typer(update_app, name="update", rich_help_panel=PANEL_MONITORING)

metrics_app = typer.Typer(help="Prometheus metrics export")
app.add_typer(metrics_app, name="metrics", rich_help_panel=PANEL_MONITORING)

monitor_app = typer.Typer(help="Network leak monitoring for privacy containers")
app.add_typer(monitor_app, name="monitor", rich_help_panel=PANEL_MONITORING)

service_app = typer.Typer(help="Manage systemd services inside containers")
app.add_typer(service_app, name="service", rich_help_panel=PANEL_CONFIGURATION)

# security domain-inspired security subcommands
PANEL_SECURITY = "Security (domain-based)"

template_container_app = typer.Typer(help="Template containers (domain-based base images)")
app.add_typer(template_container_app, name="template-container", rich_help_panel=PANEL_SECURITY)

disposable_app = typer.Typer(help="Disposable containers (ephemeral, destroyed after use)")
app.add_typer(disposable_app, name="disposable", rich_help_panel=PANEL_SECURITY)

domain_app = typer.Typer(help="Security domains (color-coded trust levels)")
app.add_typer(domain_app, name="domain", rich_help_panel=PANEL_SECURITY)

service_container_app = typer.Typer(help="Service containers (Tor, VPN, firewall gateways)")
app.add_typer(service_container_app, name="service-container", rich_help_panel=PANEL_SECURITY)

vault_app = typer.Typer(help="Split secrets vaults (isolated key storage)")
app.add_typer(vault_app, name="vault", rich_help_panel=PANEL_SECURITY)

chain_app = typer.Typer(help="Network chains (multi-hop traffic routing)")
app.add_typer(chain_app, name="chain", rich_help_panel=PANEL_SECURITY)


def get_runtime(runtime_name: str | None = None) -> ContainerRuntime:
    """Get the appropriate runtime instance."""
    config = get_config()
    runtime = runtime_name or config.default_runtime

    try:
        return _get_runtime(runtime)
    except ValueError as e:
        raise typer.BadParameter(str(e)) from e


def version_callback(value: bool) -> None:
    """Print version and exit."""
    if value:
        console.print(f"gaol version {__version__}")
        raise typer.Exit()


def get_current_host() -> str | None:
    """Get the current remote host from context, if set."""
    return _current_host.get()


@app.callback()
def main(
    version: Annotated[
        bool | None,
        typer.Option("--version", "-V", callback=version_callback, is_eager=True),
    ] = None,
    host: Annotated[
        str | None,
        typer.Option("--host", "-H", help="Execute commands on a remote host"),
    ] = None,
) -> None:
    """Gaol - Containers and VMs all the way down."""
    if host:
        _current_host.set(host)


@app.command(name="import", rich_help_panel=PANEL_CONFIGURATION)
def import_content(
    url: Annotated[str, typer.Argument(help="URL or reference to import from")],
    content_type: Annotated[
        str | None, typer.Option("--type", "-t", help="Content type (profile, spec). Auto-detect if omitted")
    ] = None,
    name: Annotated[
        str | None, typer.Option("--name", "-n", help="Override name for imported content")
    ] = None,
    force: Annotated[
        bool, typer.Option("--force", "-f", help="Overwrite if already exists")
    ] = False,
    skip_scan: Annotated[
        bool, typer.Option("--skip-scan", help="Skip security scanning")
    ] = False,
    no_save: Annotated[
        bool, typer.Option("--no-save", help="Don't save locally (validate only)")
    ] = False,
    json_output: Annotated[
        bool, typer.Option("--json", "-j", help="Output result as JSON")
    ] = False,
) -> None:
    """Import a profile or spec from a URL.

    Auto-detects content type from the file content, or specify with --type.

    Supports:
      - https://... or http://... - Direct URLs
      - github:user/repo/path - GitHub raw URL
      - github:user/repo/path@branch - GitHub raw URL with branch
      - gist:id - GitHub gist
      - gist:id/filename - GitHub gist file

    Examples:
        gaol import https://example.com/profile.yaml
        gaol import github:user/repo/specs/webserver.yaml --type spec
        gaol import github:user/repo/profiles/dev.yaml@develop
        gaol import gist:abc123/my-profile.yaml --name my-dev
    """
    import json

    from gaol.sharing import ContentType, get_content_importer
    from gaol.sharing.exceptions import (
        ContentTypeDetectionError,
        ContentValidationError,
        FetchError,
        ImportError,
        ProfileExistsError,
        SpecExistsError,
    )

    # Parse content type
    ct: ContentType | None = None
    if content_type:
        content_type = content_type.lower()
        if content_type == "profile":
            ct = ContentType.PROFILE
        elif content_type == "spec":
            ct = ContentType.SPEC
        else:
            err_console.print(f"[red]Invalid content type:[/red] {content_type}")
            err_console.print("Valid types: profile, spec")
            raise typer.Exit(1)

    importer = get_content_importer()

    try:
        result = importer.import_url(
            url,
            content_type=ct,
            name=name,
            save_locally=not no_save,
            skip_security_scan=skip_scan,
        )
    except ProfileExistsError as e:
        err_console.print(f"[red]Profile already exists:[/red] {e.name}")
        err_console.print("Use --force to overwrite")
        raise typer.Exit(1) from e
    except SpecExistsError as e:
        err_console.print(f"[red]Spec already exists:[/red] {e.name}")
        err_console.print("Use --force to overwrite")
        raise typer.Exit(1) from e
    except ContentTypeDetectionError as e:
        err_console.print(f"[red]Could not detect content type:[/red] {e}")
        err_console.print("Use --type to specify: profile or spec")
        raise typer.Exit(1) from e
    except FetchError as e:
        err_console.print(f"[red]Failed to fetch:[/red] {e}")
        raise typer.Exit(1) from e
    except ContentValidationError as e:
        err_console.print(f"[red]Invalid content:[/red] {e}")
        raise typer.Exit(1) from e
    except ImportError as e:
        err_console.print(f"[red]Import failed:[/red] {e}")
        raise typer.Exit(1) from e

    if json_output:
        output = {
            "success": result.success,
            "content_type": result.content_type.value,
            "name": result.name,
            "source_url": result.source_url,
            "local_path": str(result.local_path) if result.local_path else None,
            "trust_level": result.trust_level.value,
            "warnings": result.warnings,
            "errors": result.errors,
        }
        if result.security_scan:
            output["security_scan"] = {
                "passed": result.security_scan.passed,
                "risk_level": result.security_scan.risk_level,
                "findings": [
                    {
                        "severity": f.severity.value,
                        "category": f.category,
                        "message": f.message,
                        "location": f.location,
                    }
                    for f in result.security_scan.findings
                ],
            }
        console.print(json.dumps(output, indent=2))
        return

    # Display result
    console.print(f"[green]Imported {result.content_type.value}:[/green] {result.name}")
    console.print(f"  Source: {result.source_url}")
    if result.local_path:
        console.print(f"  Saved to: {result.local_path}")
    console.print(f"  Trust level: {result.trust_level.value}")

    # Show security scan results
    if result.security_scan:
        if result.security_scan.passed:
            console.print(f"  Security: [green]passed[/green] (risk: {result.security_scan.risk_level})")
        else:
            console.print(f"  Security: [red]issues found[/red] (risk: {result.security_scan.risk_level})")

        if result.security_scan.findings:
            console.print()
            console.print("[bold]Security findings:[/bold]")
            for finding in result.security_scan.findings:
                color = {
                    "critical": "red",
                    "error": "red",
                    "warning": "yellow",
                    "info": "dim",
                }.get(finding.severity.value, "white")
                console.print(f"  [{color}][{finding.severity.value.upper()}][/{color}] {finding.message}")
                if finding.location:
                    console.print(f"    at {finding.location}")

    # Show warnings
    if result.warnings:
        console.print()
        console.print("[yellow]Warnings:[/yellow]")
        for warning in result.warnings:
            console.print(f"  - {warning}")


@app.command(rich_help_panel=PANEL_LIFECYCLE)
def create(
    name: Annotated[str | None, typer.Argument(help="Container name")] = None,
    file: Annotated[
        str | None,
        typer.Option("--file", "-f", help="Path to spec file (YAML or TOML)"),
    ] = None,
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime to use")
    ] = None,
    distro: Annotated[str | None, typer.Option("--distro", "-d", help="Base distribution")] = None,
    image: Annotated[str | None, typer.Option("--image", "-i", help="Custom base image")] = None,
    profile: Annotated[
        list[str] | None, typer.Option("--profile", "-p", help="Profiles to apply")
    ] = None,
    port: Annotated[
        list[str] | None,
        typer.Option("--port", "-P", help="Port mapping (host:container or host:container/proto)"),
    ] = None,
    volume: Annotated[
        list[str] | None,
        typer.Option("--volume", "-v", help="Volume mount (host:container or host:container:ro)"),
    ] = None,
    env: Annotated[
        list[str] | None,
        typer.Option("--env", "-e", help="Environment variable (KEY=VALUE)"),
    ] = None,
    network: Annotated[
        NetworkMode, typer.Option("--network", "-n", help="Network mode")
    ] = NetworkMode.SHARED,
    privileged: Annotated[
        bool, typer.Option("--privileged", help="Run in privileged mode")
    ] = False,
    start: Annotated[
        bool, typer.Option("--start", "-s", help="Start container after creation")
    ] = False,
    gpu: Annotated[
        bool, typer.Option("--gpu", help="Enable GPU passthrough")
    ] = False,
    gpu_device: Annotated[
        list[int] | None, typer.Option("--gpu-device", help="Specific GPU device ID(s)")
    ] = None,
    gpu_vendor: Annotated[
        str | None, typer.Option("--gpu-vendor", help="GPU vendor (nvidia, amd, intel, auto)")
    ] = None,
    device: Annotated[
        list[str] | None, typer.Option("--device", help="Device passthrough (e.g., /dev/dri/card0)")
    ] = None,
    project_dir: Annotated[
        list[str] | None,
        typer.Option(
            "--project-dir",
            "--bind-project",
            help="Bind mount directory at same path inside container (for Claude Code resume support). Can specify multiple.",
        ),
    ] = None,
) -> None:
    """Create a new container.

    You can specify options on the command line, or use a spec file with -f.
    Command line options override spec file values.

    Examples:
        gaol create mycontainer --runtime docker --distro trixie
        gaol create -f container.yaml
        gaol create -f container.yaml --start
        gaol create mycontainer -f base.yaml --port 8080:80

        # Dev container with project mounted at same path (for Claude Code):
        gaol create dev --runtime nspawn --project-dir ~/Projects/myapp --start
    """
    app_config = get_config()

    # Load from spec file if provided
    container_config: ContainerConfig | None = None
    if file:
        from gaol.specs import SpecError, load_spec_config

        try:
            container_config = load_spec_config(file)
        except SpecError as e:
            err_console.print(f"[red]Error loading spec:[/red] {e}")
            raise typer.Exit(1) from None

    # If no spec file, name is required
    if container_config is None and not name:
        err_console.print("[red]Error:[/red] Container name is required (or use --file)")
        raise typer.Exit(1)

    # Parse port mappings from CLI
    ports: dict[int, int] = {}
    if port:
        for p in port:
            if ":" not in p:
                err_console.print(f"[red]Invalid port format: {p} (expected host:container)[/red]")
                raise typer.Exit(1)
            # Strip protocol suffix for legacy port dict
            port_str = p.split("/")[0] if "/" in p else p
            parts = port_str.split(":")
            if len(parts) == 2:
                host_p, container_p = parts
            elif len(parts) == 3:
                # host_ip:host_port:container_port
                _, host_p, container_p = parts
            else:
                err_console.print(f"[red]Invalid port format: {p}[/red]")
                raise typer.Exit(1)
            ports[int(host_p)] = int(container_p)

    # Parse volume mounts from CLI
    volumes: dict[str, str] = {}
    if volume:
        for v in volume:
            if ":" not in v:
                err_console.print(
                    f"[red]Invalid volume format: {v} (expected host:container)[/red]"
                )
                raise typer.Exit(1)
            parts = v.split(":")
            # Handle Windows paths and :ro suffix
            if len(parts) == 2:
                host_path, container_path = parts
            elif len(parts) == 3:
                if parts[2].lower() == "ro":
                    host_path, container_path = parts[0], parts[1]
                else:
                    # Could be Windows path C:\path:container
                    host_path = f"{parts[0]}:{parts[1]}"
                    container_path = parts[2]
            else:
                host_path, container_path = parts[0], parts[1]
            volumes[host_path] = container_path

    # Add project directory bind mounts (same path inside container)
    # This enables Claude Code to resume conversations since paths match
    if project_dir:
        for proj in project_dir:
            # Resolve to absolute path
            proj_path = Path(proj).resolve()
            if not proj_path.exists():
                err_console.print(f"[red]Project directory does not exist: {proj}[/red]")
                raise typer.Exit(1)
            if not proj_path.is_dir():
                err_console.print(f"[red]Project path is not a directory: {proj}[/red]")
                raise typer.Exit(1)
            # Bind at the same absolute path
            abs_path = str(proj_path)
            volumes[abs_path] = abs_path

    # Parse environment variables from CLI
    environment: dict[str, str] = {}
    if env:
        for e in env:
            if "=" not in e:
                err_console.print(f"[red]Invalid env format: {e} (expected KEY=VALUE)[/red]")
                raise typer.Exit(1)
            key, value = e.split("=", 1)
            environment[key] = value

    # Build or merge config
    if container_config:
        # Override spec values with CLI options
        if name:
            container_config.name = name
        if runtime:
            container_config.runtime = runtime
        if distro:
            container_config.distro = distro
        if image:
            container_config.image = image
        if profile:
            container_config.profiles = list(profile)
        if ports:
            container_config.network.ports.update(ports)
        if volumes:
            container_config.volumes.update(volumes)
        if environment:
            container_config.environment.update(environment)
        if network != NetworkMode.SHARED:
            container_config.network.mode = network
        if privileged:
            container_config.privileged = True
        # Handle GPU config from CLI
        if gpu or gpu_device or gpu_vendor:
            from gaol.gpu.models import GPUConfig, GPUVendor

            gpu_config = container_config.gpu or GPUConfig()
            if gpu:
                gpu_config.enabled = True
            if gpu_device:
                gpu_config.device_ids = list(gpu_device)
                gpu_config.enabled = True
            if gpu_vendor:
                gpu_config.vendor = GPUVendor(gpu_vendor)
                gpu_config.enabled = True
            container_config.gpu = gpu_config
        # Handle device passthrough from CLI
        if device:
            from gaol.gpu.models import DeviceMapping

            for dev in device:
                container_config.devices.append(DeviceMapping(host_path=dev))
    else:
        # Create config from CLI options
        assert name is not None  # Validated above

        # Build GPU config if specified
        gpu_config = None
        if gpu or gpu_device or gpu_vendor:
            from gaol.gpu.models import GPUConfig, GPUVendor

            gpu_config = GPUConfig(
                enabled=True,
                device_ids=list(gpu_device) if gpu_device else [],
                vendor=GPUVendor(gpu_vendor) if gpu_vendor else GPUVendor.AUTO,
            )

        # Build device mappings
        device_mappings = []
        if device:
            from gaol.gpu.models import DeviceMapping

            for dev in device:
                device_mappings.append(DeviceMapping(host_path=dev))

        container_config = ContainerConfig(
            name=name,
            runtime=runtime or app_config.default_runtime,
            distro=distro or app_config.default_distro,
            image=image,
            profiles=profile or [],
            network=NetworkConfig(mode=network, ports=ports),
            volumes=volumes,
            environment=environment,
            privileged=privileged,
            gpu=gpu_config,
            devices=device_mappings,
        )

    try:
        rt = get_runtime(container_config.runtime if file else runtime)
        if not rt.is_available():
            err_console.print(f"[red]Runtime '{rt.name}' is not available[/red]")
            raise typer.Exit(1)

        container_id = rt.create(container_config)

        # Save container config to store
        store = get_container_store()
        store.save(container_config, container_id=container_id, runtime=rt.name)

        console.print(f"[green]Created container:[/green] {container_id}")

        if start:
            rt.start(container_id)
            store.update_started(container_config.name)
            console.print(f"[green]Started container:[/green] {container_id}")

            # Apply profiles (install packages, run setup commands) if any were specified
            if container_config.profiles:
                import time

                # Wait for container to be fully ready
                time.sleep(3)
                console.print("[cyan]Applying profiles...[/cyan]")
                exit_code, stdout, stderr = rt.setup(container_id)
                if exit_code != 0:
                    err_console.print(f"[red]Profile setup failed (exit code {exit_code})[/red]")
                    if stderr:
                        err_console.print(f"[red]{stderr}[/red]")
                    raise typer.Exit(1)
                store.update_setup_done(container_config.name)
                console.print("[green]Profiles applied successfully[/green]")

    except RuntimeError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e


@app.command(rich_help_panel=PANEL_LIFECYCLE)
def start(
    name: Annotated[str, typer.Argument(help="Container name or ID")],
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
) -> None:
    """Start a stopped container."""
    try:
        # Try to get runtime from store if not specified
        store = get_container_store()
        stored = store.get(name)
        if stored and not runtime:
            runtime = stored.runtime

        rt = get_runtime(runtime)
        rt.start(name)
        store.update_started(name)
        console.print(f"[green]Started container:[/green] {name}")

        # Apply profiles on first start if not already done
        if stored and stored.config.profiles and not stored.setup_done:
            import time

            time.sleep(3)  # Wait for container to be fully ready
            console.print("[cyan]Applying profiles (first start)...[/cyan]")
            exit_code, stdout, stderr = rt.setup(name)
            if exit_code != 0:
                err_console.print(f"[red]Profile setup failed (exit code {exit_code})[/red]")
                if stderr:
                    err_console.print(f"[red]{stderr}[/red]")
            else:
                store.update_setup_done(name)
                console.print("[green]Profiles applied successfully[/green]")
    except RuntimeError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e


@app.command(rich_help_panel=PANEL_LIFECYCLE)
def setup(
    name: Annotated[str, typer.Argument(help="Container name or ID")],
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
    force: Annotated[
        bool, typer.Option("--force", "-f", help="Re-run setup even if already done")
    ] = False,
) -> None:
    """Apply profiles to a container (install packages, run setup commands).

    This is automatically called on first start, but can be run manually
    to re-apply profiles or after adding new profiles.
    """
    try:
        store = get_container_store()
        stored = store.get(name)
        if stored and not runtime:
            runtime = stored.runtime

        if stored and stored.setup_done and not force:
            console.print(
                f"[yellow]Setup already done for {name}. Use --force to re-run.[/yellow]"
            )
            return

        if stored and not stored.config.profiles:
            console.print(f"[yellow]No profiles configured for {name}[/yellow]")
            return

        rt = get_runtime(runtime)

        # Ensure container is running
        status = rt.status(name)
        if status.status != ContainerStatus.RUNNING:
            console.print(f"[yellow]Starting container {name}...[/yellow]")
            rt.start(name)
            store.update_started(name)

        console.print("[cyan]Applying profiles...[/cyan]")
        rt.setup(name)
        store.update_setup_done(name)
        console.print("[green]Profiles applied successfully[/green]")
    except RuntimeError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e


@app.command(rich_help_panel=PANEL_LIFECYCLE)
def stop(
    name: Annotated[str, typer.Argument(help="Container name or ID")],
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
    timeout: Annotated[int, typer.Option("--timeout", "-t", help="Timeout in seconds")] = 10,
) -> None:
    """Stop a running container."""
    try:
        # Try to get runtime from store if not specified
        store = get_container_store()
        stored = store.get(name)
        if stored and not runtime:
            runtime = stored.runtime

        rt = get_runtime(runtime)
        rt.stop(name, timeout=timeout)
        console.print(f"[green]Stopped container:[/green] {name}")
    except RuntimeError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e


@app.command(rich_help_panel=PANEL_LIFECYCLE)
def destroy(
    name: Annotated[str, typer.Argument(help="Container name or ID")],
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
    force: Annotated[bool, typer.Option("--force", "-f", help="Force removal")] = False,
    keep_config: Annotated[
        bool, typer.Option("--keep-config", help="Keep stored configuration")
    ] = False,
) -> None:
    """Remove a container."""
    try:
        # Try to get runtime from store if not specified
        store = get_container_store()
        stored = store.get(name)
        if stored and not runtime:
            runtime = stored.runtime

        rt = get_runtime(runtime)
        rt.destroy(name, force=force)

        # Delete stored config unless --keep-config is set
        if not keep_config:
            store.delete(name)

        console.print(f"[green]Removed container:[/green] {name}")
    except RuntimeError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e


@app.command(name="exec", rich_help_panel=PANEL_EXECUTION)
def exec_cmd(
    name: Annotated[str, typer.Argument(help="Container name or ID")],
    command: Annotated[list[str], typer.Argument(help="Command to execute")],
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
    user: Annotated[str | None, typer.Option("--user", "-u", help="User to run as")] = None,
    workdir: Annotated[
        str | None, typer.Option("--workdir", "-w", help="Working directory")
    ] = None,
) -> None:
    """Execute a command in a running container."""
    try:
        rt = get_runtime(runtime)
        exit_code, stdout, stderr = rt.exec(
            name,
            command,
            user=user,
            workdir=workdir,
        )
        if stdout:
            console.print(stdout, end="")
        if stderr:
            err_console.print(stderr, end="")
        raise typer.Exit(exit_code)
    except RuntimeError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e


@app.command(rich_help_panel=PANEL_EXECUTION)
def shell(
    name: Annotated[str, typer.Argument(help="Container name or ID")],
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
    user: Annotated[str | None, typer.Option("--user", "-u", help="User to run as")] = None,
) -> None:
    """Open an interactive shell in a container."""
    import subprocess

    try:
        rt = get_runtime(runtime)
        if not rt.is_available():
            err_console.print(f"[red]Runtime '{rt.name}' is not available[/red]")
            raise typer.Exit(1)

        # Build command based on runtime
        if rt.name == "docker":
            cmd = ["docker", "exec", "-it"]
            if user:
                cmd.extend(["-u", user])
            cmd.extend([name, "/bin/bash", "-l"])

            # Try bash first, fall back to sh
            result = subprocess.run(cmd)
            if result.returncode != 0:
                cmd[-2:] = ["/bin/sh"]
                subprocess.run(cmd)

        elif rt.name == "nspawn":
            cmd = ["sudo", "machinectl", "shell"]
            if user:
                cmd.append(f"{user}@{name}")
            else:
                cmd.append(f"root@{name}")

            subprocess.run(cmd)

        else:
            err_console.print(f"[red]Shell not supported for runtime: {rt.name}[/red]")
            raise typer.Exit(1)

    except Exception as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e


@app.command(name="list", rich_help_panel=PANEL_LIFECYCLE)
def list_containers(
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
    all: Annotated[bool, typer.Option("--all", "-a", help="Show all containers")] = False,
    all_runtimes: Annotated[
        bool, typer.Option("--all-runtimes", "-A", help="Show containers from all runtimes")
    ] = False,
    stored: Annotated[
        bool, typer.Option("--stored", "-s", help="Show stored container configs")
    ] = False,
) -> None:
    """List containers."""
    from gaol.models.container import ContainerState

    containers: list[ContainerState] = []

    if stored:
        # Show stored container configurations
        store = get_container_store()
        stored_containers = store.list()

        if not stored_containers:
            console.print("[dim]No stored containers found[/dim]")
            return

        table = Table(title="Stored Containers")
        table.add_column("Name", style="green")
        table.add_column("Runtime", style="dim")
        table.add_column("Container ID", style="cyan")
        table.add_column("Distro")
        table.add_column("Profiles", style="dim")
        table.add_column("Created")

        for sc in stored_containers:
            table.add_row(
                sc.config.name,
                sc.runtime,
                sc.container_id[:12] if sc.container_id else "-",
                sc.config.distro,
                ", ".join(sc.config.profiles) if sc.config.profiles else "-",
                sc.created_at.strftime("%Y-%m-%d %H:%M") if sc.created_at else "-",
            )

        console.print(table)
        return

    try:
        if all_runtimes:
            # List from all available runtimes
            for name, available in list_available_runtimes():
                if available:
                    try:
                        rt = _get_runtime(name)
                        containers.extend(rt.list(all=all))
                    except Exception:
                        continue
        else:
            rt = get_runtime(runtime)
            containers = rt.list(all=all)

        if not containers:
            console.print("[dim]No containers found[/dim]")
            return

        table = Table(title="Containers")
        table.add_column("ID", style="cyan")
        table.add_column("Name", style="green")
        table.add_column("Runtime", style="dim")
        table.add_column("Status")
        table.add_column("Image")
        table.add_column("IP Address")
        table.add_column("Profiles", style="dim")

        for c in containers:
            status_style = {
                ContainerStatus.RUNNING: "green",
                ContainerStatus.STOPPED: "yellow",
                ContainerStatus.EXITED: "red",
                ContainerStatus.PAUSED: "blue",
            }.get(c.status, "dim")

            table.add_row(
                c.id,
                c.name,
                c.runtime,
                f"[{status_style}]{c.status.value}[/{status_style}]",
                c.image,
                c.ip_address or "-",
                ", ".join(c.profiles) if c.profiles else "-",
            )

        console.print(table)
    except RuntimeError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e


@app.command(rich_help_panel=PANEL_LIFECYCLE)
def status(
    name: Annotated[str, typer.Argument(help="Container name or ID")],
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
) -> None:
    """Get detailed status of a container."""
    try:
        rt = get_runtime(runtime)
        c = rt.status(name)

        status_style = {
            ContainerStatus.RUNNING: "green",
            ContainerStatus.STOPPED: "yellow",
            ContainerStatus.EXITED: "red",
            ContainerStatus.PAUSED: "blue",
        }.get(c.status, "dim")

        console.print(f"[bold]Container:[/bold] {c.name}")
        console.print(f"[bold]ID:[/bold] {c.id}")
        console.print(f"[bold]Status:[/bold] [{status_style}]{c.status.value}[/{status_style}]")
        console.print(f"[bold]Runtime:[/bold] {c.runtime}")
        console.print(f"[bold]Image:[/bold] {c.image}")
        if c.ip_address:
            console.print(f"[bold]IP Address:[/bold] {c.ip_address}")
        if c.ports:
            ports_str = ", ".join(f"{h}:{c}" for h, c in c.ports.items())
            console.print(f"[bold]Ports:[/bold] {ports_str}")
        if c.profiles:
            console.print(f"[bold]Profiles:[/bold] {', '.join(c.profiles)}")
        if c.created_at:
            console.print(f"[bold]Created:[/bold] {c.created_at}")
        if c.started_at:
            console.print(f"[bold]Started:[/bold] {c.started_at}")
        if c.exit_code is not None and c.status == ContainerStatus.EXITED:
            console.print(f"[bold]Exit Code:[/bold] {c.exit_code}")
        if c.error:
            console.print(f"[bold red]Error:[/bold red] {c.error}")

    except RuntimeError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e


@app.command(rich_help_panel=PANEL_MONITORING)
def stats(
    name: Annotated[str, typer.Argument(help="Container name or ID")],
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
    json_output: Annotated[
        bool, typer.Option("--json", "-j", help="Output as JSON")
    ] = False,
    watch: Annotated[
        bool, typer.Option("--watch", "-w", help="Continuously update stats")
    ] = False,
    interval: Annotated[
        float, typer.Option("--interval", "-i", help="Update interval in seconds")
    ] = 2.0,
) -> None:
    """Display container resource usage statistics.

    Shows CPU, memory, network I/O, and block I/O statistics
    for a running container.
    """
    import json
    import time

    # Try to get runtime from store if not specified
    store = get_container_store()
    stored = store.get(name)
    if stored and not runtime:
        runtime = stored.runtime

    try:
        rt = get_runtime(runtime)

        def print_stats() -> None:
            s = rt.stats(name)

            if json_output:
                console.print(json.dumps(s.model_dump(mode="json"), indent=2))
                return

            # Format memory
            mem_used = _format_size(s.memory_used_bytes)
            mem_limit = _format_size(s.memory_limit_bytes) if s.memory_limit_bytes else "unlimited"

            # Format network
            net_rx = _format_size(s.network_rx_bytes)
            net_tx = _format_size(s.network_tx_bytes)

            # Format block I/O
            blk_read = _format_size(s.block_read_bytes)
            blk_write = _format_size(s.block_write_bytes)

            console.print(f"[bold cyan]Container:[/bold cyan] {s.container_name}")
            console.print()
            console.print(f"[bold]CPU:[/bold]        {s.cpu_percent:.2f}% ({s.cpu_count} cores)")
            console.print(f"[bold]Memory:[/bold]     {mem_used} / {mem_limit} ({s.memory_percent:.1f}%)")
            console.print(f"[bold]Network:[/bold]    RX: {net_rx}  TX: {net_tx}")
            console.print(f"[bold]Block I/O:[/bold]  Read: {blk_read}  Write: {blk_write}")
            console.print(f"[bold]PIDs:[/bold]       {s.pids}")

        if watch:
            try:
                while True:
                    console.clear()
                    print_stats()
                    console.print()
                    console.print(f"[dim]Updating every {interval}s. Press Ctrl+C to stop.[/dim]")
                    time.sleep(interval)
            except KeyboardInterrupt:
                console.print("\n[dim]Stopped[/dim]")
        else:
            print_stats()

    except NotImplementedError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e
    except RuntimeError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e


@app.command(rich_help_panel=PANEL_LIFECYCLE)
def inspect(
    name: Annotated[str, typer.Argument(help="Container name")],
    json_output: Annotated[
        bool, typer.Option("--json", "-j", help="Output as JSON")
    ] = False,
) -> None:
    """Show stored container configuration."""
    import json

    store = get_container_store()
    stored = store.get(name)

    if not stored:
        err_console.print(f"[red]Container not found in store:[/red] {name}")
        raise typer.Exit(1)

    if json_output:
        console.print(json.dumps(stored.model_dump(mode="json"), indent=2))
        return

    config = stored.config
    console.print(f"[bold cyan]Container:[/bold cyan] {config.name}")
    console.print()

    # Metadata
    console.print("[bold]Metadata:[/bold]")
    console.print(f"  Container ID: {stored.container_id or '-'}")
    console.print(f"  Runtime: {stored.runtime}")
    console.print(f"  Created: {stored.created_at}")
    console.print(f"  Updated: {stored.updated_at}")
    if stored.last_started_at:
        console.print(f"  Last Started: {stored.last_started_at}")
    console.print()

    # Configuration
    console.print("[bold]Configuration:[/bold]")
    console.print(f"  Distro: {config.distro}")
    if config.image:
        console.print(f"  Image: {config.image}")
    if config.working_dir:
        console.print(f"  Working Dir: {config.working_dir}")
    if config.command:
        console.print(f"  Command: {' '.join(config.command)}")
    console.print(f"  Privileged: {config.privileged}")
    console.print(f"  Read Only: {config.read_only}")
    console.print()

    # Profiles
    if config.profiles:
        console.print(f"[bold]Profiles:[/bold] {', '.join(config.profiles)}")
        console.print()

    # Network
    console.print("[bold]Network:[/bold]")
    console.print(f"  Mode: {config.network.mode.value}")
    if config.network.hostname:
        console.print(f"  Hostname: {config.network.hostname}")
    if config.network.ports:
        console.print("  Ports:")
        for host_port, container_port in config.network.ports.items():
            console.print(f"    {host_port} -> {container_port}")
    if config.network.dns:
        console.print(f"  DNS: {', '.join(config.network.dns)}")
    console.print()

    # Resources
    if config.resources.memory_mb or config.resources.cpu_count or config.resources.cpu_shares:
        console.print("[bold]Resources:[/bold]")
        if config.resources.memory_mb:
            console.print(f"  Memory: {config.resources.memory_mb}MB")
        if config.resources.cpu_count:
            console.print(f"  CPUs: {config.resources.cpu_count}")
        if config.resources.cpu_shares:
            console.print(f"  CPU Shares: {config.resources.cpu_shares}")
        console.print()

    # Volumes
    if config.volumes:
        console.print("[bold]Volumes:[/bold]")
        for host_path, container_path in config.volumes.items():
            console.print(f"  {host_path} -> {container_path}")
        console.print()

    # Environment
    if config.environment:
        console.print("[bold]Environment:[/bold]")
        for key, value in config.environment.items():
            console.print(f"  {key}={value}")


@app.command(rich_help_panel=PANEL_FILES)
def logs(
    name: Annotated[str, typer.Argument(help="Container name or ID")],
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
    follow: Annotated[bool, typer.Option("--follow", "-f", help="Follow log output")] = False,
    tail: Annotated[
        int | None, typer.Option("--tail", "-n", help="Number of lines from end")
    ] = None,
) -> None:
    """View container logs."""
    try:
        rt = get_runtime(runtime)
        output = rt.logs(name, follow=follow, tail=tail)
        console.print(output, end="")
    except RuntimeError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e


@app.command(rich_help_panel=PANEL_LIFECYCLE)
def pause(
    name: Annotated[str, typer.Argument(help="Container name or ID")],
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
) -> None:
    """Pause a running container."""
    try:
        rt = get_runtime(runtime)
        rt.pause(name)
        console.print(f"[green]Paused container:[/green] {name}")
    except RuntimeError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e


@app.command(rich_help_panel=PANEL_LIFECYCLE)
def unpause(
    name: Annotated[str, typer.Argument(help="Container name or ID")],
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
) -> None:
    """Unpause a paused container."""
    try:
        rt = get_runtime(runtime)
        rt.unpause(name)
        console.print(f"[green]Unpaused container:[/green] {name}")
    except RuntimeError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e


@app.command(rich_help_panel=PANEL_FILES)
def cp(
    src: Annotated[str, typer.Argument(help="Source path (container:path or local path)")],
    dest: Annotated[str, typer.Argument(help="Destination path (container:path or local path)")],
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
) -> None:
    """Copy files between host and container.

    Use container:path syntax to reference container paths.

    Examples:
        gaol cp myfile.txt mycontainer:/tmp/
        gaol cp mycontainer:/etc/passwd ./passwd.bak
    """
    # Parse source and destination
    src_container, src_path = _parse_copy_path(src)
    dest_container, dest_path = _parse_copy_path(dest)

    # Validate - exactly one must be a container path
    if src_container and dest_container:
        err_console.print("[red]Error:[/red] Cannot copy between two containers directly")
        raise typer.Exit(1)

    if not src_container and not dest_container:
        err_console.print("[red]Error:[/red] At least one path must be a container path (container:path)")
        raise typer.Exit(1)

    try:
        container_name = src_container or dest_container
        assert container_name is not None  # We validated above

        # Try to get runtime from store if not specified
        store = get_container_store()
        stored = store.get(container_name)
        if stored and not runtime:
            runtime = stored.runtime

        rt = get_runtime(runtime)

        if src_container:
            # Copy from container to host
            rt.copy_from(src_container, src_path, dest_path)
            console.print(f"[green]Copied[/green] {src_container}:{src_path} -> {dest_path}")
        else:
            # Copy from host to container
            assert dest_container is not None
            rt.copy_to(dest_container, src_path, dest_path)
            console.print(f"[green]Copied[/green] {src_path} -> {dest_container}:{dest_path}")

    except RuntimeError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e


def _parse_copy_path(path: str) -> tuple[str | None, str]:
    """Parse a copy path into (container_name, path).

    Returns (None, path) for local paths, (container, path) for container paths.
    """
    if ":" in path:
        # Check if it's a Windows absolute path (C:\...) - unlikely in container context
        parts = path.split(":", 1)
        if len(parts[0]) == 1 and parts[0].isalpha():
            # Likely a Windows drive letter, treat as local path
            return None, path
        return parts[0], parts[1]
    return None, path


# --- Runtimes command ---


@app.command(rich_help_panel=PANEL_INFRASTRUCTURE)
def runtimes() -> None:
    """List available container runtimes."""
    table = Table(title="Available Runtimes")
    table.add_column("Runtime", style="cyan")
    table.add_column("Available")
    table.add_column("Description")

    runtime_descriptions = {
        "docker": "Docker containers (Linux/Mac)",
        "nspawn": "systemd-nspawn containers (Linux)",
    }

    for name, available in list_available_runtimes():
        status = "[green]Yes[/green]" if available else "[red]No[/red]"
        desc = runtime_descriptions.get(name, "")
        table.add_row(name, status, desc)

    console.print(table)


# --- Profiles subcommands ---


@profiles_app.command(name="list")
def profiles_list() -> None:
    """List available profiles."""
    from gaol.profiles.loader import get_profile_loader

    loader = get_profile_loader()
    available = loader.list_available()

    if not available:
        console.print("[dim]No profiles found[/dim]")
        return

    table = Table(title="Available Profiles")
    table.add_column("Name", style="cyan")
    table.add_column("Description")
    table.add_column("Dependencies", style="dim")

    for name in available:
        try:
            profile = loader.get(name)
            deps = ", ".join(d.name for d in profile.depends_on) if profile.depends_on else "-"
            table.add_row(name, profile.description or "-", deps)
        except Exception:
            table.add_row(name, "[red]Error loading[/red]", "-")

    console.print(table)


@profiles_app.command(name="show")
def profiles_show(
    name: Annotated[str, typer.Argument(help="Profile name")],
) -> None:
    """Show details of a profile."""
    from gaol.profiles.loader import get_profile_loader

    loader = get_profile_loader()

    try:
        profile = loader.get(name)
    except KeyError as e:
        err_console.print(f"[red]Profile not found:[/red] {name}")
        raise typer.Exit(1) from e

    console.print(f"[bold cyan]Profile:[/bold cyan] {profile.name}")
    console.print(f"[bold]Description:[/bold] {profile.description or '-'}")
    console.print(f"[bold]Version:[/bold] {profile.version}")
    console.print()

    # Requirements
    if profile.requires_root or profile.requires_privileged:
        console.print("[bold]Requirements:[/bold]")
        if profile.requires_root:
            console.print("  - Requires root")
        if profile.requires_privileged:
            console.print("  - Requires privileged mode")
        console.print()

    # Dependencies
    if profile.depends_on:
        console.print("[bold]Dependencies:[/bold]")
        for dep in profile.depends_on:
            optional = " (optional)" if dep.optional else ""
            console.print(f"  - {dep.name}{optional}")
        console.print()

    # Conflicts
    if profile.conflicts_with:
        console.print("[bold]Conflicts with:[/bold]")
        for conflict in profile.conflicts_with:
            console.print(f"  - {conflict}")
        console.print()

    # Packages
    if profile.packages:
        console.print("[bold]Packages:[/bold]")
        for pkg in profile.packages:
            console.print(f"  - {pkg}")
        console.print()

    # Services
    if profile.services:
        console.print("[bold]Services:[/bold]")
        for svc in profile.services:
            console.print(f"  - {svc}")
        console.print()

    # Setup commands
    if profile.setup_commands:
        console.print("[bold]Setup commands:[/bold]")
        for cmd in profile.setup_commands:
            console.print(f"  $ {cmd}")
        console.print()

    # Environment
    if profile.environment:
        console.print("[bold]Environment:[/bold]")
        for key, value in profile.environment.items():
            display_value = value if value else "[dim]<required>[/dim]"
            console.print(f"  {key}={display_value}")
        console.print()

    # Exposed ports
    if profile.exposed_ports:
        console.print(f"[bold]Exposed ports:[/bold] {', '.join(map(str, profile.exposed_ports))}")
        console.print()

    # Capabilities
    if profile.capabilities_add:
        console.print(f"[bold]Capabilities (add):[/bold] {', '.join(profile.capabilities_add)}")
    if profile.capabilities_drop:
        console.print(f"[bold]Capabilities (drop):[/bold] {', '.join(profile.capabilities_drop)}")


@profiles_app.command(name="resolve")
def profiles_resolve(
    names: Annotated[list[str], typer.Argument(help="Profile names to resolve")],
) -> None:
    """Resolve profile dependencies and show application order."""
    from gaol.profiles.loader import get_profile_loader

    loader = get_profile_loader()

    try:
        resolved = loader.resolve_dependencies(names)
    except KeyError as e:
        err_console.print(f"[red]Profile not found:[/red] {e}")
        raise typer.Exit(1) from e
    except ValueError as e:
        err_console.print(f"[red]Conflict:[/red] {e}")
        raise typer.Exit(1) from e

    console.print("[bold]Resolved profile order:[/bold]")
    for i, profile in enumerate(resolved, 1):
        console.print(f"  {i}. {profile.name}")


@profiles_app.command(name="validate")
def profiles_validate(
    path: Annotated[str, typer.Argument(help="Path to profile YAML file")],
) -> None:
    """Validate a profile YAML file.

    Checks that the profile is well-formed and all dependencies exist.
    """
    from pathlib import Path

    import yaml
    from pydantic import ValidationError

    from gaol.models.profile import Profile
    from gaol.profiles.loader import get_profile_loader

    file_path = Path(path)
    if not file_path.exists():
        err_console.print(f"[red]File not found:[/red] {path}")
        raise typer.Exit(1)

    if file_path.suffix not in [".yaml", ".yml"]:
        err_console.print("[yellow]Warning:[/yellow] File does not have .yaml or .yml extension")

    try:
        with open(file_path) as f:
            data = yaml.safe_load(f)
    except yaml.YAMLError as e:
        err_console.print(f"[red]Invalid YAML:[/red] {e}")
        raise typer.Exit(1) from e

    if not isinstance(data, dict):
        err_console.print("[red]Invalid profile:[/red] Expected a YAML mapping")
        raise typer.Exit(1)

    # Validate against Profile model
    errors: list[str] = []
    try:
        profile = Profile.from_yaml(data)
    except ValidationError as e:
        for error in e.errors():
            field = ".".join(str(x) for x in error["loc"])
            errors.append(f"  - {field}: {error['msg']}")
        err_console.print("[red]Validation errors:[/red]")
        for error in errors:
            err_console.print(error)
        raise typer.Exit(1) from e
    except Exception as e:
        err_console.print(f"[red]Error parsing profile:[/red] {e}")
        raise typer.Exit(1) from e

    # Check dependencies exist
    loader = get_profile_loader()
    missing_deps: list[str] = []
    for dep in profile.depends_on:
        if not dep.optional:
            try:
                loader.get(dep.name)
            except KeyError:
                missing_deps.append(dep.name)

    if missing_deps:
        err_console.print("[yellow]Warning:[/yellow] Missing required dependencies:")
        for dep in missing_deps:
            err_console.print(f"  - {dep}")

    # Check conflicts
    loader = get_profile_loader()
    for conflict in profile.conflicts_with:
        try:
            loader.get(conflict)
            err_console.print(f"[yellow]Note:[/yellow] Conflicts with existing profile: {conflict}")
        except KeyError:
            pass  # Conflict doesn't exist, that's fine

    console.print(f"[green]Valid profile:[/green] {profile.name}")
    console.print(f"  Description: {profile.description or '-'}")
    console.print(f"  Version: {profile.version}")
    if profile.packages:
        console.print(f"  Packages: {len(profile.packages)}")
    if profile.services:
        console.print(f"  Services: {len(profile.services)}")
    if profile.setup_commands:
        console.print(f"  Setup commands: {len(profile.setup_commands)}")


@profiles_app.command(name="import")
def profiles_import(
    url: Annotated[str, typer.Argument(help="URL or reference to import from (https://, github:, gist:)")],
    name: Annotated[
        str | None, typer.Option("--name", "-n", help="Override name for the profile")
    ] = None,
    force: Annotated[
        bool, typer.Option("--force", "-f", help="Overwrite if profile already exists")
    ] = False,
    skip_scan: Annotated[
        bool, typer.Option("--skip-scan", help="Skip security scanning")
    ] = False,
    json_output: Annotated[
        bool, typer.Option("--json", "-j", help="Output result as JSON")
    ] = False,
) -> None:
    """Import a profile from a URL.

    Supports:
      - https://... or http://... - Direct URLs
      - github:user/repo/path - GitHub raw URL
      - github:user/repo/path@branch - GitHub raw URL with branch
      - gist:id - GitHub gist
      - gist:id/filename - GitHub gist file

    Examples:
        gaol profiles import https://example.com/profile.yaml
        gaol profiles import github:user/repo/profiles/dev.yaml
        gaol profiles import github:user/repo/profiles/dev.yaml@develop
        gaol profiles import gist:abc123/my-profile.yaml
    """
    import json

    from gaol.profiles.loader import get_profile_loader
    from gaol.sharing.exceptions import (
        FetchError,
        ImportError,
        ContentValidationError,
        ProfileExistsError,
    )

    loader = get_profile_loader()

    try:
        result = loader.import_from_url(
            url,
            name=name,
            skip_security_scan=skip_scan,
            overwrite=force,
        )
    except ProfileExistsError as e:
        err_console.print(f"[red]Profile already exists:[/red] {e.name}")
        err_console.print("Use --force to overwrite")
        raise typer.Exit(1) from e
    except FetchError as e:
        err_console.print(f"[red]Failed to fetch:[/red] {e}")
        raise typer.Exit(1) from e
    except ContentValidationError as e:
        err_console.print(f"[red]Invalid profile:[/red] {e}")
        raise typer.Exit(1) from e
    except ImportError as e:
        err_console.print(f"[red]Import failed:[/red] {e}")
        raise typer.Exit(1) from e

    if json_output:
        output = {
            "success": result.success,
            "name": result.name,
            "source_url": result.source_url,
            "local_path": str(result.local_path) if result.local_path else None,
            "trust_level": result.trust_level.value,
            "warnings": result.warnings,
            "errors": result.errors,
        }
        if result.security_scan:
            output["security_scan"] = {
                "passed": result.security_scan.passed,
                "risk_level": result.security_scan.risk_level,
                "findings": [
                    {
                        "severity": f.severity.value,
                        "category": f.category,
                        "message": f.message,
                        "location": f.location,
                    }
                    for f in result.security_scan.findings
                ],
            }
        console.print(json.dumps(output, indent=2))
        return

    # Display result
    console.print(f"[green]Imported profile:[/green] {result.name}")
    console.print(f"  Source: {result.source_url}")
    if result.local_path:
        console.print(f"  Saved to: {result.local_path}")
    console.print(f"  Trust level: {result.trust_level.value}")

    # Show security scan results
    if result.security_scan:
        if result.security_scan.passed:
            console.print(f"  Security: [green]passed[/green] (risk: {result.security_scan.risk_level})")
        else:
            console.print(f"  Security: [red]issues found[/red] (risk: {result.security_scan.risk_level})")

        if result.security_scan.findings:
            console.print()
            console.print("[bold]Security findings:[/bold]")
            for finding in result.security_scan.findings:
                color = {
                    "critical": "red",
                    "error": "red",
                    "warning": "yellow",
                    "info": "dim",
                }.get(finding.severity.value, "white")
                console.print(f"  [{color}][{finding.severity.value.upper()}][/{color}] {finding.message}")
                if finding.location:
                    console.print(f"    at {finding.location}")

    # Show warnings
    if result.warnings:
        console.print()
        console.print("[yellow]Warnings:[/yellow]")
        for warning in result.warnings:
            console.print(f"  - {warning}")


# --- Snapshot subcommands ---


def _format_size(size_bytes: int | None) -> str:
    """Format bytes as human-readable size."""
    if size_bytes is None:
        return "-"
    for unit in ["B", "KB", "MB", "GB", "TB"]:
        if abs(size_bytes) < 1024:
            return f"{size_bytes:.1f} {unit}"
        size_bytes /= 1024
    return f"{size_bytes:.1f} PB"


@snapshot_app.command(name="create")
def snapshot_create(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    name: Annotated[str, typer.Argument(help="Snapshot name")],
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
    description: Annotated[
        str | None, typer.Option("--description", "-d", help="Snapshot description")
    ] = None,
) -> None:
    """Create a snapshot of a container."""
    try:
        # Try to get runtime from store if not specified
        store = get_container_store()
        stored = store.get(container)
        if stored and not runtime:
            runtime = stored.runtime

        rt = get_runtime(runtime)
        snapshot = rt.snapshot_create(container, name, description)

        console.print(f"[green]Created snapshot:[/green] {snapshot.name}")
        console.print(f"  Container: {snapshot.container_name}")
        if snapshot.size_bytes:
            console.print(f"  Size: {_format_size(snapshot.size_bytes)}")
        if snapshot.description:
            console.print(f"  Description: {snapshot.description}")

    except NotImplementedError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e
    except RuntimeError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e


@snapshot_app.command(name="list")
def snapshot_list(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
) -> None:
    """List snapshots for a container."""
    try:
        # Try to get runtime from store if not specified
        store = get_container_store()
        stored = store.get(container)
        if stored and not runtime:
            runtime = stored.runtime

        rt = get_runtime(runtime)
        snapshots = rt.snapshot_list(container)

        if not snapshots:
            console.print(f"[dim]No snapshots for container: {container}[/dim]")
            return

        table = Table(title=f"Snapshots for {container}")
        table.add_column("Name", style="cyan")
        table.add_column("Created")
        table.add_column("Size")
        table.add_column("Description", style="dim")

        for s in snapshots:
            table.add_row(
                s.name,
                s.created_at.strftime("%Y-%m-%d %H:%M:%S") if s.created_at else "-",
                _format_size(s.size_bytes),
                s.description or "-",
            )

        console.print(table)

    except NotImplementedError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e
    except RuntimeError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e


@snapshot_app.command(name="restore")
def snapshot_restore(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    name: Annotated[str, typer.Argument(help="Snapshot name to restore")],
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
) -> None:
    """Restore a container from a snapshot.

    Warning: This will stop the container and replace its current state.
    """
    try:
        # Try to get runtime from store if not specified
        store = get_container_store()
        stored = store.get(container)
        if stored and not runtime:
            runtime = stored.runtime

        rt = get_runtime(runtime)
        rt.snapshot_restore(container, name)

        console.print(f"[green]Restored container[/green] {container} [green]from snapshot:[/green] {name}")

    except NotImplementedError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e
    except RuntimeError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e


@snapshot_app.command(name="delete")
def snapshot_delete(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    name: Annotated[str, typer.Argument(help="Snapshot name to delete")],
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
    force: Annotated[
        bool, typer.Option("--force", "-f", help="Skip confirmation")
    ] = False,
) -> None:
    """Delete a snapshot."""
    if not force:
        confirm = typer.confirm(f"Delete snapshot '{name}' for container '{container}'?")
        if not confirm:
            console.print("[dim]Cancelled[/dim]")
            raise typer.Exit(0)

    try:
        # Try to get runtime from store if not specified
        store = get_container_store()
        stored = store.get(container)
        if stored and not runtime:
            runtime = stored.runtime

        rt = get_runtime(runtime)
        rt.snapshot_delete(container, name)

        console.print(f"[green]Deleted snapshot:[/green] {name}")

    except NotImplementedError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e
    except RuntimeError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e


# --- Export/Import commands ---


@app.command(name="export", rich_help_panel=PANEL_FILES)
def export_container(
    name: Annotated[str, typer.Argument(help="Container name to export")],
    output: Annotated[
        str | None,
        typer.Option("--output", "-o", help="Output file path (default: <name>.yaml)"),
    ] = None,
    json_output: Annotated[
        bool, typer.Option("--json", "-j", help="Output as JSON instead of YAML")
    ] = False,
) -> None:
    """Export container configuration to a file.

    Exports the stored container configuration including all settings,
    profiles, volumes, environment variables, and other options.
    The exported file can be imported on another system or used as
    a template for creating similar containers.
    """
    import json
    from pathlib import Path

    import yaml

    store = get_container_store()
    stored = store.get(name)

    if not stored:
        err_console.print(f"[red]Container not found in store:[/red] {name}")
        raise typer.Exit(1)

    # Prepare export data (just the config, not metadata)
    export_data = {
        "name": stored.config.name,
        "runtime": stored.runtime,
        "distro": stored.config.distro,
    }

    if stored.config.image:
        export_data["image"] = stored.config.image

    if stored.config.profiles:
        export_data["profiles"] = stored.config.profiles

    # Network config
    network_data: dict = {}
    if stored.config.network.mode != NetworkMode.SHARED:
        network_data["mode"] = stored.config.network.mode.value
    if stored.config.network.ports:
        network_data["ports"] = stored.config.network.ports
    if stored.config.network.dns:
        network_data["dns"] = stored.config.network.dns
    if stored.config.network.hostname:
        network_data["hostname"] = stored.config.network.hostname
    if network_data:
        export_data["network"] = network_data

    # Resource limits
    resources_data: dict = {}
    if stored.config.resources.memory_mb is not None:
        resources_data["memory_mb"] = stored.config.resources.memory_mb
    if stored.config.resources.cpu_shares is not None:
        resources_data["cpu_shares"] = stored.config.resources.cpu_shares
    if stored.config.resources.cpu_count is not None:
        resources_data["cpu_count"] = stored.config.resources.cpu_count
    if stored.config.resources.disk_mb is not None:
        resources_data["disk_mb"] = stored.config.resources.disk_mb
    if resources_data:
        export_data["resources"] = resources_data

    if stored.config.volumes:
        export_data["volumes"] = stored.config.volumes

    if stored.config.environment:
        export_data["environment"] = stored.config.environment

    if stored.config.working_dir:
        export_data["working_dir"] = stored.config.working_dir

    if stored.config.command:
        export_data["command"] = stored.config.command

    if stored.config.privileged:
        export_data["privileged"] = True

    if stored.config.read_only:
        export_data["read_only"] = True

    # Determine output format and path
    if json_output:
        content = json.dumps(export_data, indent=2)
        ext = ".json"
    else:
        content = yaml.dump(export_data, default_flow_style=False, sort_keys=False)
        ext = ".yaml"

    output_path = Path(output) if output else Path(f"{name}{ext}")

    output_path.write_text(content)
    console.print(f"[green]Exported to:[/green] {output_path}")


@app.command(name="import-config", rich_help_panel=PANEL_FILES)
def import_container(
    path: Annotated[str, typer.Argument(help="Path to container config file")],
    name: Annotated[
        str | None,
        typer.Option("--name", "-n", help="Override container name"),
    ] = None,
    create_now: Annotated[
        bool,
        typer.Option("--create", "-c", help="Create the container immediately"),
    ] = False,
    start_now: Annotated[
        bool,
        typer.Option("--start", "-s", help="Create and start the container"),
    ] = False,
) -> None:
    """Import container configuration from a file.

    Imports a container configuration from a YAML or JSON file
    previously created with 'export' or manually written.

    Use --create to create the container immediately, or --start
    to create and start it.
    """
    import json as json_module
    from pathlib import Path

    import yaml

    file_path = Path(path)
    if not file_path.exists():
        err_console.print(f"[red]File not found:[/red] {path}")
        raise typer.Exit(1)

    # Load file
    content = file_path.read_text()
    try:
        if file_path.suffix == ".json":
            data = json_module.loads(content)
        else:
            data = yaml.safe_load(content)
    except Exception as e:
        err_console.print(f"[red]Error parsing file:[/red] {e}")
        raise typer.Exit(1) from e

    if not isinstance(data, dict):
        err_console.print("[red]Invalid config:[/red] Expected a mapping")
        raise typer.Exit(1)

    # Override name if specified
    if name:
        data["name"] = name

    if "name" not in data:
        err_console.print("[red]Missing required field:[/red] name")
        raise typer.Exit(1)

    # Build config
    try:
        network_data = data.get("network", {})
        network_config = NetworkConfig(
            mode=NetworkMode(network_data.get("mode", "shared")),
            ports=network_data.get("ports", {}),
            dns=network_data.get("dns", []),
            hostname=network_data.get("hostname"),
        )

        from gaol.models.container import ResourceLimits

        resources_data = data.get("resources", {})
        resources = ResourceLimits(**resources_data)

        config = ContainerConfig(
            name=data["name"],
            runtime=data.get("runtime", "docker"),
            distro=data.get("distro", "trixie"),
            image=data.get("image"),
            profiles=data.get("profiles", []),
            network=network_config,
            resources=resources,
            volumes=data.get("volumes", {}),
            environment=data.get("environment", {}),
            working_dir=data.get("working_dir"),
            command=data.get("command"),
            privileged=data.get("privileged", False),
            read_only=data.get("read_only", False),
        )
    except Exception as e:
        err_console.print(f"[red]Invalid configuration:[/red] {e}")
        raise typer.Exit(1) from e

    # Save to store
    store = get_container_store()

    if store.exists(config.name) and not typer.confirm(
        f"Container '{config.name}' already exists in store. Overwrite?"
    ):
        console.print("[dim]Cancelled[/dim]")
        raise typer.Exit(0)

    runtime_name = data.get("runtime", "docker")
    store.save(config, runtime=runtime_name)
    console.print(f"[green]Imported configuration:[/green] {config.name}")

    # Create and/or start if requested
    if create_now or start_now:
        try:
            rt = get_runtime(runtime_name)

            # Apply profiles to config
            if config.profiles:
                from gaol.profiles import get_profile_applicator

                applicator = get_profile_applicator()
                config = applicator.apply_to_config(config, config.profiles)

            container_id = rt.create(config)
            store.update_container_id(config.name, container_id)
            console.print(f"[green]Created container:[/green] {config.name} ({container_id[:12]})")

            if start_now:
                rt.start(container_id)
                store.update_started(config.name)
                console.print(f"[green]Started container:[/green] {config.name}")

        except RuntimeError as e:
            err_console.print(f"[red]Error:[/red] {e}")
            raise typer.Exit(1) from e


@app.command(name="ssh-config", rich_help_panel=PANEL_NETWORKING)
def ssh_config(
    name: Annotated[
        str | None,
        typer.Argument(help="Container name (omit for all running containers)"),
    ] = None,
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
    user: Annotated[
        str, typer.Option("--user", "-u", help="SSH user")
    ] = "root",
    port: Annotated[
        int, typer.Option("--port", "-p", help="SSH port in container")
    ] = 22,
    identity: Annotated[
        str | None, typer.Option("--identity", "-i", help="Path to SSH identity file")
    ] = None,
    append: Annotated[
        bool, typer.Option("--append", "-a", help="Append to ~/.ssh/config")
    ] = False,
) -> None:
    """Generate SSH config entries for containers.

    Generates SSH configuration for running containers that can be
    added to ~/.ssh/config. This enables easy SSH access using just
    the container name.

    Example:
        gaol ssh-config mycontainer >> ~/.ssh/config
        ssh mycontainer
    """
    from pathlib import Path

    store = get_container_store()
    configs: list[str] = []

    if name:
        # Single container
        stored = store.get(name)
        if stored and not runtime:
            runtime = stored.runtime

        try:
            rt = get_runtime(runtime)
            state = rt.status(name)

            if state.ip_address:
                config_entry = _generate_ssh_config_entry(
                    state.name, state.ip_address, user, port, identity
                )
                configs.append(config_entry)
            else:
                err_console.print(f"[yellow]Warning:[/yellow] Container {name} has no IP address")

        except RuntimeError as e:
            err_console.print(f"[red]Error:[/red] {e}")
            raise typer.Exit(1) from e
    else:
        # All running containers
        for rt_name, available in list_available_runtimes():
            if not available:
                continue
            try:
                rt = _get_runtime(rt_name)
                containers = rt.list(all=False)  # Only running

                for state in containers:
                    if state.ip_address:
                        config_entry = _generate_ssh_config_entry(
                            state.name, state.ip_address, user, port, identity
                        )
                        configs.append(config_entry)
            except Exception:
                continue

    if not configs:
        err_console.print("[yellow]No containers with IP addresses found[/yellow]")
        raise typer.Exit(0)

    output = "\n".join(configs)

    if append:
        ssh_config_path = Path.home() / ".ssh" / "config"
        ssh_config_path.parent.mkdir(mode=0o700, exist_ok=True)

        # Add marker comment
        marker = "# Gaol container entries"
        existing = ssh_config_path.read_text() if ssh_config_path.exists() else ""

        if marker in existing:
            console.print("[yellow]Warning:[/yellow] Gaol entries already exist in SSH config")
            console.print("Remove existing entries before appending new ones")
            raise typer.Exit(1)

        with open(ssh_config_path, "a") as f:
            f.write(f"\n{marker}\n")
            f.write(output)
            f.write("\n# End Gaol entries\n")

        console.print(f"[green]Appended to:[/green] {ssh_config_path}")
    else:
        console.print(output)


def _generate_ssh_config_entry(
    name: str,
    ip_address: str,
    user: str,
    port: int,
    identity: str | None,
) -> str:
    """Generate a single SSH config entry."""
    lines = [
        f"Host {name}",
        f"    HostName {ip_address}",
        f"    User {user}",
    ]

    if port != 22:
        lines.append(f"    Port {port}")

    if identity:
        lines.append(f"    IdentityFile {identity}")

    lines.append("    StrictHostKeyChecking no")
    lines.append("    UserKnownHostsFile /dev/null")
    lines.append("")

    return "\n".join(lines)


# =============================================================================
# Network commands
# =============================================================================


@network_app.command(name="check-port")
def network_check_port(
    port: Annotated[int, typer.Argument(help="Port number to check")],
    protocol: Annotated[
        str, typer.Option("--protocol", "-p", help="Protocol (tcp/udp)")
    ] = "tcp",
    host: Annotated[
        str, typer.Option("--host", "-H", help="Host address to check")
    ] = "0.0.0.0",
) -> None:
    """Check if a port is available for binding."""
    from gaol.networking import Protocol, get_port_process, is_port_available

    try:
        proto = Protocol(protocol.lower())
    except ValueError:
        err_console.print(f"[red]Invalid protocol: {protocol}. Use tcp or udp.[/red]")
        raise typer.Exit(1) from None

    available = is_port_available(port, proto, host)

    if available:
        console.print(f"[green]Port {port}/{protocol} is available[/green]")
    else:
        process = get_port_process(port, proto)
        msg = f"[red]Port {port}/{protocol} is in use"
        if process:
            msg += f" by {process}"
        msg += "[/red]"
        console.print(msg)
        raise typer.Exit(1)


@network_app.command(name="find-port")
def network_find_port(
    start: Annotated[
        int, typer.Option("--start", "-s", help="Start of port range")
    ] = 8000,
    end: Annotated[int, typer.Option("--end", "-e", help="End of port range")] = 9000,
    protocol: Annotated[str, typer.Option("--protocol", "-p")] = "tcp",
    count: Annotated[
        int, typer.Option("--count", "-n", help="Number of ports to find")
    ] = 1,
) -> None:
    """Find available ports in a range."""
    from gaol.networking import Protocol, find_available_port

    try:
        proto = Protocol(protocol.lower())
    except ValueError:
        err_console.print(f"[red]Invalid protocol: {protocol}. Use tcp or udp.[/red]")
        raise typer.Exit(1) from None

    found: list[int] = []
    current_start = start

    while len(found) < count and current_start <= end:
        port = find_available_port(current_start, end, proto)
        if port:
            found.append(port)
            current_start = port + 1
        else:
            break

    if found:
        for port in found:
            console.print(port)
    else:
        err_console.print("[red]No available ports found in range[/red]")
        raise typer.Exit(1)


@network_app.command(name="bridges")
def network_bridges() -> None:
    """List network bridges on the system."""
    from gaol.networking import list_bridges

    bridges = list_bridges()

    if not bridges:
        console.print("[dim]No bridges found[/dim]")
        return

    table = Table(title="Network Bridges")
    table.add_column("Name", style="cyan")
    table.add_column("State")
    table.add_column("IP Address")
    table.add_column("Interfaces", style="dim")

    for br in bridges:
        state_style = "green" if br.state == "up" else "red"
        table.add_row(
            br.name,
            f"[{state_style}]{br.state}[/{state_style}]",
            br.ip_address or "-",
            ", ".join(br.interfaces) if br.interfaces else "-",
        )

    console.print(table)


@network_app.command(name="dns")
def network_dns(
    provider: Annotated[
        str | None,
        typer.Option(
            "--provider", "-p", help="Show provider DNS (google, cloudflare, quad9)"
        ),
    ] = None,
) -> None:
    """Show DNS configuration.

    By default shows the system DNS. Use --provider to show a public DNS provider's servers.
    """
    from gaol.networking import get_default_dns_config, get_system_dns

    if provider:
        config = get_default_dns_config(provider=provider)
        console.print(f"[bold]{provider.title()} DNS:[/bold]")
    else:
        config = get_system_dns()
        console.print("[bold]System DNS:[/bold]")

    if config.nameservers:
        console.print("  Nameservers:")
        for ns in config.nameservers:
            console.print(f"    - {ns}")
    else:
        console.print("  [dim]No nameservers configured[/dim]")

    if config.search_domains:
        console.print(f"  Search: {' '.join(config.search_domains)}")

    if config.options:
        console.print(f"  Options: {' '.join(config.options)}")


# =============================================================================
# Privacy networking commands
# =============================================================================


@network_app.command(name="tor-proxy")
def network_tor_proxy(
    action: Annotated[str, typer.Argument(help="Action: create, delete, list, status")],
    name: Annotated[str | None, typer.Argument(help="Proxy name")] = None,
    container: Annotated[
        str | None, typer.Option("--container", "-c", help="Container name")
    ] = None,
    transparent: Annotated[
        bool, typer.Option("--transparent", help="Enable transparent proxy mode")
    ] = False,
    exit_nodes: Annotated[
        list[str] | None, typer.Option("--exit", help="Exit node country codes")
    ] = None,
    socks_port: Annotated[
        int, typer.Option("--socks-port", help="SOCKS port")
    ] = 9050,
) -> None:
    """Manage Tor proxy for containers.

    Examples:
        gaol network tor-proxy create mytor --container myapp
        gaol network tor-proxy create mytor -c myapp --transparent
        gaol network tor-proxy list
        gaol network tor-proxy status mytor
        gaol network tor-proxy delete mytor
    """
    from gaol.privacy import TorProxyConfig, TorProxyManager

    manager = TorProxyManager()

    if action == "create":
        if not name or not container:
            err_console.print("[red]Error: name and --container required for create[/red]")
            raise typer.Exit(1)

        config = TorProxyConfig(
            socks_port=socks_port,
            transparent=transparent,
            exit_nodes=exit_nodes or [],
        )

        try:
            proxy = manager.create(name, container, config)
            console.print(f"[green]Created Tor proxy:[/green] {name}")
            console.print(f"  SOCKS port: {proxy.socks_port}")
            console.print(f"  Control port: {proxy.control_port}")
            if transparent:
                console.print(f"  TransPort: {config.trans_port}")
        except Exception as e:
            err_console.print(f"[red]Error:[/red] {e}")
            raise typer.Exit(1) from e

    elif action == "delete":
        if not name:
            err_console.print("[red]Error: name required for delete[/red]")
            raise typer.Exit(1)

        try:
            manager.delete(name)
            console.print(f"[green]Deleted Tor proxy:[/green] {name}")
        except Exception as e:
            err_console.print(f"[red]Error:[/red] {e}")
            raise typer.Exit(1) from e

    elif action == "list":
        proxies = manager.list(container)
        if not proxies:
            console.print("[dim]No Tor proxies found[/dim]")
            return

        table = Table(title="Tor Proxies")
        table.add_column("Name")
        table.add_column("Container")
        table.add_column("Status")
        table.add_column("SOCKS Port")

        for proxy in proxies:
            table.add_row(
                proxy.name,
                proxy.container_name,
                proxy.status.value,
                str(proxy.socks_port),
            )
        console.print(table)

    elif action == "status":
        if not name:
            err_console.print("[red]Error: name required for status[/red]")
            raise typer.Exit(1)

        proxy = manager.status(name)
        if not proxy:
            console.print(f"[dim]Tor proxy '{name}' not found[/dim]")
            raise typer.Exit(1)

        console.print(f"[bold]Tor Proxy:[/bold] {name}")
        console.print(f"  Status: {proxy.status.value}")
        console.print(f"  Container: {proxy.container_name}")
        console.print(f"  SOCKS port: {proxy.socks_port}")
        console.print(f"  Control port: {proxy.control_port}")
        console.print(f"  Bootstrap: {proxy.bootstrap_progress}%")

    else:
        err_console.print(f"[red]Unknown action:[/red] {action}")
        err_console.print("Valid actions: create, delete, list, status")
        raise typer.Exit(1)


@network_app.command(name="onion")
def network_onion(
    action: Annotated[str, typer.Argument(help="Action: create, delete, list, address")],
    name: Annotated[str | None, typer.Argument(help="Service name")] = None,
    container: Annotated[
        str | None, typer.Option("--container", "-c", help="Container name")
    ] = None,
    port: Annotated[
        int | None, typer.Option("--port", "-p", help="Container port to expose")
    ] = None,
    onion_port: Annotated[
        int, typer.Option("--onion-port", help="Port on .onion address")
    ] = 80,
) -> None:
    """Manage Tor hidden services for containers.

    Examples:
        gaol network onion create myservice --container nginx --port 80
        gaol network onion address myservice
        gaol network onion list
        gaol network onion delete myservice
    """
    from gaol.privacy import HiddenServiceConfig, HiddenServiceManager

    manager = HiddenServiceManager()

    if action == "create":
        if not name or not container or port is None:
            err_console.print("[red]Error: name, --container, and --port required[/red]")
            raise typer.Exit(1)

        config = HiddenServiceConfig(
            name=name,
            container_name=container,
            container_port=port,
            hidden_service_port=onion_port,
        )

        try:
            service = manager.create(config)
            console.print(f"[green]Created hidden service:[/green] {name}")
            if service.onion_address:
                console.print(f"  Onion address: {service.onion_address}")
            else:
                console.print("  [yellow]Onion address pending...[/yellow]")
        except Exception as e:
            err_console.print(f"[red]Error:[/red] {e}")
            raise typer.Exit(1) from e

    elif action == "delete":
        if not name:
            err_console.print("[red]Error: name required for delete[/red]")
            raise typer.Exit(1)

        try:
            manager.delete(name)
            console.print(f"[green]Deleted hidden service:[/green] {name}")
        except Exception as e:
            err_console.print(f"[red]Error:[/red] {e}")
            raise typer.Exit(1) from e

    elif action == "list":
        services = manager.list(container)
        if not services:
            console.print("[dim]No hidden services found[/dim]")
            return

        table = Table(title="Tor Hidden Services")
        table.add_column("Name")
        table.add_column("Container")
        table.add_column("Onion Address")
        table.add_column("Status")

        for svc in services:
            table.add_row(
                svc.name,
                svc.container_name,
                svc.onion_address or "[pending]",
                svc.status.value,
            )
        console.print(table)

    elif action == "address":
        if not name:
            err_console.print("[red]Error: name required for address[/red]")
            raise typer.Exit(1)

        address = manager.get_onion_address(name)
        if address:
            console.print(address)
        else:
            console.print(f"[dim]No address for '{name}'[/dim]")
            raise typer.Exit(1)

    else:
        err_console.print(f"[red]Unknown action:[/red] {action}")
        err_console.print("Valid actions: create, delete, list, address")
        raise typer.Exit(1)


@network_app.command(name="vpn-gateway")
def network_vpn_gateway(
    action: Annotated[str, typer.Argument(help="Action: create, delete, list, status")],
    name: Annotated[str | None, typer.Argument(help="Gateway name")] = None,
    config_file: Annotated[
        Path | None, typer.Option("--config", "-c", help="VPN config file")
    ] = None,
    provider: Annotated[
        str | None,
        typer.Option("--provider", "-p", help="VPN provider (openvpn, wireguard)"),
    ] = None,
    kill_switch: Annotated[
        bool, typer.Option("--kill-switch/--no-kill-switch", help="Enable kill switch")
    ] = True,
) -> None:
    """Manage VPN gateway for containers.

    Examples:
        gaol network vpn-gateway create myvpn --config ~/vpn.ovpn --provider openvpn
        gaol network vpn-gateway status myvpn
        gaol network vpn-gateway list
        gaol network vpn-gateway delete myvpn
    """
    from gaol.privacy import VPNGatewayConfig, VPNGatewayManager, VPNProvider

    manager = VPNGatewayManager()

    if action == "create":
        if not name or not config_file or not provider:
            err_console.print("[red]Error: name, --config, and --provider required[/red]")
            raise typer.Exit(1)

        try:
            vpn_provider = VPNProvider(provider.lower())
        except ValueError:
            err_console.print(f"[red]Invalid provider:[/red] {provider}")
            err_console.print("Valid providers: openvpn, wireguard")
            raise typer.Exit(1) from None

        config = VPNGatewayConfig(
            name=name,
            provider=vpn_provider,
            config_file=config_file,
            kill_switch=kill_switch,
        )

        try:
            gateway = manager.create(config)
            console.print(f"[green]Created VPN gateway:[/green] {name}")
            console.print(f"  Provider: {gateway.provider.value}")
            console.print(f"  Status: {gateway.status.value}")
            if gateway.interface:
                console.print(f"  Interface: {gateway.interface}")
        except Exception as e:
            err_console.print(f"[red]Error:[/red] {e}")
            raise typer.Exit(1) from e

    elif action == "delete":
        if not name:
            err_console.print("[red]Error: name required for delete[/red]")
            raise typer.Exit(1)

        try:
            manager.delete(name)
            console.print(f"[green]Deleted VPN gateway:[/green] {name}")
        except Exception as e:
            err_console.print(f"[red]Error:[/red] {e}")
            raise typer.Exit(1) from e

    elif action == "list":
        gateways = manager.list()
        if not gateways:
            console.print("[dim]No VPN gateways found[/dim]")
            return

        table = Table(title="VPN Gateways")
        table.add_column("Name")
        table.add_column("Provider")
        table.add_column("Status")
        table.add_column("Interface")

        for gw in gateways:
            table.add_row(
                gw.name,
                gw.provider.value,
                gw.status.value,
                gw.interface or "-",
            )
        console.print(table)

    elif action == "status":
        if not name:
            err_console.print("[red]Error: name required for status[/red]")
            raise typer.Exit(1)

        gateway = manager.status(name)
        if not gateway:
            console.print(f"[dim]VPN gateway '{name}' not found[/dim]")
            raise typer.Exit(1)

        console.print(f"[bold]VPN Gateway:[/bold] {name}")
        console.print(f"  Provider: {gateway.provider.value}")
        console.print(f"  Status: {gateway.status.value}")
        if gateway.interface:
            console.print(f"  Interface: {gateway.interface}")
        if gateway.public_ip:
            console.print(f"  Public IP: {gateway.public_ip}")

    else:
        err_console.print(f"[red]Unknown action:[/red] {action}")
        err_console.print("Valid actions: create, delete, list, status")
        raise typer.Exit(1)


@network_app.command(name="wireguard")
def network_wireguard(
    action: Annotated[
        str, typer.Argument(help="Action: create, delete, list, start, stop, add-peer, status")
    ],
    name: Annotated[str | None, typer.Argument(help="Mesh name")] = None,
    subnet: Annotated[
        str | None, typer.Option("--subnet", "-s", help="Mesh subnet (CIDR)")
    ] = None,
    peer_name: Annotated[
        str | None, typer.Option("--peer-name", help="Peer name")
    ] = None,
    public_key: Annotated[
        str | None, typer.Option("--public-key", "-k", help="Peer public key")
    ] = None,
    peer_ip: Annotated[
        str | None, typer.Option("--peer-ip", help="Peer IP address")
    ] = None,
    endpoint: Annotated[
        str | None, typer.Option("--endpoint", "-e", help="Peer endpoint (host:port)")
    ] = None,
) -> None:
    """Manage WireGuard mesh networks.

    Examples:
        gaol network wireguard create mymesh --subnet 10.200.0.0/24
        gaol network wireguard start mymesh
        gaol network wireguard add-peer mymesh --peer-name peer1 --public-key KEY --peer-ip 10.200.0.2
        gaol network wireguard status mymesh
        gaol network wireguard stop mymesh
        gaol network wireguard delete mymesh
    """
    from gaol.privacy import WireGuardMeshManager

    manager = WireGuardMeshManager()

    if action == "create":
        if not name:
            err_console.print("[red]Error: name required for create[/red]")
            raise typer.Exit(1)

        try:
            mesh = manager.create(name, subnet=subnet or "10.200.0.0/24")
            console.print(f"[green]Created WireGuard mesh:[/green] {name}")
            console.print(f"  Interface: {mesh.interface_name}")
            if mesh.interface:
                console.print(f"  Address: {mesh.interface.address}")
            pubkey = manager.get_public_key(name)
            if pubkey:
                console.print(f"  Public key: {pubkey}")
        except Exception as e:
            err_console.print(f"[red]Error:[/red] {e}")
            raise typer.Exit(1) from e

    elif action == "delete":
        if not name:
            err_console.print("[red]Error: name required for delete[/red]")
            raise typer.Exit(1)

        try:
            manager.delete(name)
            console.print(f"[green]Deleted WireGuard mesh:[/green] {name}")
        except Exception as e:
            err_console.print(f"[red]Error:[/red] {e}")
            raise typer.Exit(1) from e

    elif action == "start":
        if not name:
            err_console.print("[red]Error: name required for start[/red]")
            raise typer.Exit(1)

        try:
            mesh = manager.start(name)
            console.print(f"[green]Started WireGuard mesh:[/green] {name}")
        except Exception as e:
            err_console.print(f"[red]Error:[/red] {e}")
            raise typer.Exit(1) from e

    elif action == "stop":
        if not name:
            err_console.print("[red]Error: name required for stop[/red]")
            raise typer.Exit(1)

        try:
            mesh = manager.stop(name)
            console.print(f"[green]Stopped WireGuard mesh:[/green] {name}")
        except Exception as e:
            err_console.print(f"[red]Error:[/red] {e}")
            raise typer.Exit(1) from e

    elif action == "add-peer":
        if not name or not peer_name or not public_key or not peer_ip:
            err_console.print(
                "[red]Error: name, --peer-name, --public-key, and --peer-ip required[/red]"
            )
            raise typer.Exit(1)

        try:
            manager.add_peer(
                name,
                peer_name,
                public_key,
                peer_ip,
                endpoint=endpoint,
            )
            console.print(f"[green]Added peer:[/green] {peer_name}")
        except Exception as e:
            err_console.print(f"[red]Error:[/red] {e}")
            raise typer.Exit(1) from e

    elif action == "list":
        meshes = manager.list()
        if not meshes:
            console.print("[dim]No WireGuard meshes found[/dim]")
            return

        table = Table(title="WireGuard Meshes")
        table.add_column("Name")
        table.add_column("Interface")
        table.add_column("Status")
        table.add_column("Peers")

        for mesh in meshes:
            table.add_row(
                mesh.name,
                mesh.interface_name,
                mesh.status.value,
                str(len(mesh.peers)),
            )
        console.print(table)

    elif action == "status":
        if not name:
            err_console.print("[red]Error: name required for status[/red]")
            raise typer.Exit(1)

        status = manager.status(name)
        if not status:
            console.print(f"[dim]WireGuard mesh '{name}' not found[/dim]")
            raise typer.Exit(1)

        console.print(f"[bold]WireGuard Mesh:[/bold] {name}")
        console.print(f"  Status: {status['status']}")
        console.print(f"  Interface: {status['interface']}")
        console.print(f"  Address: {status['address']}")
        console.print(f"  Public key: {status['public_key']}")
        if status["peers"]:
            console.print(f"  Peers ({len(status['peers'])}):")
            for peer in status["peers"]:
                console.print(f"    - {peer['name']}: {peer['allowed_ips']}")

    else:
        err_console.print(f"[red]Unknown action:[/red] {action}")
        err_console.print("Valid actions: create, delete, list, start, stop, add-peer, status")
        raise typer.Exit(1)


# =============================================================================
# Apply command
# =============================================================================


@app.command(rich_help_panel=PANEL_CONFIGURATION)
def apply(
    path: Annotated[str, typer.Argument(help="Path to spec file (YAML or TOML)")],
    start_now: Annotated[
        bool, typer.Option("--start", "-s", help="Start the container after creation")
    ] = False,
    force: Annotated[
        bool, typer.Option("--force", "-f", help="Recreate if container exists")
    ] = False,
) -> None:
    """Create or update a container from a spec file.

    Similar to 'kubectl apply', this command creates a container from
    a declarative specification file. If the container already exists,
    use --force to recreate it.

    Privacy features (tor, vpn, onion, tunnel) are configured automatically
    if specified in the spec file.

    Examples:
        gaol apply container.yaml
        gaol apply container.toml --start
        gaol apply dev-env.yaml --force
        gaol apply tor-browser.yaml --start  # With Tor proxy
    """
    from gaol.specs import SpecError, load_spec

    try:
        spec = load_spec(path)
        container_config = spec.to_container_config()
    except SpecError as e:
        err_console.print(f"[red]Error loading spec:[/red] {e}")
        raise typer.Exit(1) from None

    # Resolve relative volume paths
    from pathlib import Path as PathLib
    spec_path = PathLib(path).resolve()
    spec_dir = spec_path.parent
    resolved_volumes: dict[str, str] = {}
    for host_path, container_path in container_config.volumes.items():
        if not host_path.startswith("/") and not host_path.startswith("~"):
            host_path = str((spec_dir / host_path).resolve())
        resolved_volumes[host_path] = container_path
    container_config.volumes = resolved_volumes

    store = get_container_store()
    existing = store.get(container_config.name)

    if existing and not force:
        err_console.print(
            f"[yellow]Container '{container_config.name}' already exists.[/yellow]\n"
            "Use --force to recreate it."
        )
        raise typer.Exit(1)

    # If force and exists, destroy the old container
    if existing and force:
        try:
            rt = get_runtime(existing.runtime)
            rt.destroy(container_config.name, force=True)
            store.delete(container_config.name)
            console.print(f"[yellow]Removed existing container:[/yellow] {container_config.name}")
        except Exception:
            pass  # Ignore errors during cleanup

    try:
        rt = get_runtime(container_config.runtime)
        if not rt.is_available():
            err_console.print(f"[red]Runtime '{rt.name}' is not available[/red]")
            raise typer.Exit(1)

        container_id = rt.create(container_config)

        # Save container config to store
        store.save(container_config, container_id=container_id, runtime=rt.name)

        console.print(f"[green]Created container:[/green] {container_config.name}")

        if start_now:
            rt.start(container_id)
            store.update_started(container_config.name)
            console.print(f"[green]Started container:[/green] {container_config.name}")

        # Apply privacy configuration if specified
        if spec.privacy:
            _apply_privacy_config(spec.privacy, container_config.name, start_now)

    except RuntimeError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e


def _apply_privacy_config(privacy: "SpecPrivacy", container_name: str, is_started: bool) -> None:
    """Apply privacy configuration from a spec.

    Args:
        privacy: Privacy configuration from the spec
        container_name: Name of the container
        is_started: Whether the container is already started
    """
    from gaol.specs.models import SpecPrivacy

    # Apply Tor proxy if configured
    if privacy.tor and privacy.tor.enabled:
        try:
            from gaol.privacy import TorProxyConfig, TorProxyManager

            manager = TorProxyManager()
            config = TorProxyConfig(
                socks_port=privacy.tor.socks_port,
                transparent=privacy.tor.transparent,
                exit_nodes=privacy.tor.exit_nodes,
                exclude_nodes=privacy.tor.exclude_nodes,
                bridge_enabled=privacy.tor.bridge_enabled,
                bridges=privacy.tor.bridges,
            )

            proxy_name = f"{container_name}-tor"
            try:
                proxy = manager.create(proxy_name, container_name, config)
                console.print(f"[green]Created Tor proxy:[/green] {proxy_name}")
                console.print(f"  SOCKS port: {proxy.socks_port}")
                if privacy.tor.transparent:
                    console.print("  Mode: transparent (all traffic routed through Tor)")
                if privacy.tor.exit_nodes:
                    console.print(f"  Exit nodes: {', '.join(privacy.tor.exit_nodes)}")
            except Exception as e:
                err_console.print(f"[yellow]Warning: Could not create Tor proxy:[/yellow] {e}")
        except ImportError:
            err_console.print("[yellow]Warning: Tor support not available[/yellow]")

    # Apply VPN gateway if configured
    if privacy.vpn and privacy.vpn.enabled:
        try:
            from pathlib import Path as PathLib

            from gaol.privacy import VPNGatewayConfig, VPNGatewayManager, VPNProvider

            manager = VPNGatewayManager()
            config_path = PathLib(privacy.vpn.config_file).expanduser().resolve()

            if not config_path.exists():
                err_console.print(f"[yellow]Warning: VPN config file not found:[/yellow] {config_path}")
            else:
                config = VPNGatewayConfig(
                    name=f"{container_name}-vpn",
                    provider=VPNProvider(privacy.vpn.provider),
                    config_file=config_path,
                    kill_switch=privacy.vpn.kill_switch,
                    dns_servers=privacy.vpn.dns_servers,
                )

                try:
                    gateway = manager.create(config)
                    console.print(f"[green]Created VPN gateway:[/green] {config.name}")
                    console.print(f"  Provider: {gateway.provider.value}")
                    if privacy.vpn.kill_switch:
                        console.print("  Kill switch: enabled")
                except Exception as e:
                    err_console.print(f"[yellow]Warning: Could not create VPN gateway:[/yellow] {e}")
        except ImportError:
            err_console.print("[yellow]Warning: VPN support not available[/yellow]")

    # Apply onion hidden service if configured
    if privacy.onion and privacy.onion.enabled:
        try:
            from gaol.privacy import HiddenServiceConfig, HiddenServiceManager

            manager = HiddenServiceManager()
            config = HiddenServiceConfig(
                name=f"{container_name}-onion",
                container_name=container_name,
                container_port=privacy.onion.port,
                hidden_service_port=privacy.onion.onion_port,
                version=privacy.onion.version,
            )

            try:
                service = manager.create(config)
                console.print(f"[green]Created hidden service:[/green] {config.name}")
                if service.onion_address:
                    console.print(f"  Onion address: [cyan]{service.onion_address}[/cyan]")
                else:
                    console.print("  Onion address: [yellow]pending...[/yellow]")
            except Exception as e:
                err_console.print(f"[yellow]Warning: Could not create hidden service:[/yellow] {e}")
        except ImportError:
            err_console.print("[yellow]Warning: Tor hidden service support not available[/yellow]")

    # Apply tunnel if configured
    if privacy.tunnel and privacy.tunnel.enabled:
        if not is_started:
            console.print("[yellow]Note: Tunnel will be created when container is started[/yellow]")
            console.print(f"  Run: gaol tunnel start {container_name} {privacy.tunnel.port}")
        else:
            try:
                from gaol.tunnel import TunnelManager

                manager = TunnelManager()
                try:
                    tunnel = manager.start(container_name, privacy.tunnel.port)
                    console.print(f"[green]Created tunnel:[/green] {tunnel.id}")
                    console.print(f"  Public URL: [cyan]{tunnel.public_url}[/cyan]")
                    console.print(f"  Container port: {privacy.tunnel.port}")
                except Exception as e:
                    err_console.print(f"[yellow]Warning: Could not create tunnel:[/yellow] {e}")
            except ImportError:
                err_console.print("[yellow]Warning: Tunnel support not available[/yellow]")


# =============================================================================
# Spec subcommands
# =============================================================================


@spec_app.command(name="validate")
def spec_validate(
    path: Annotated[str, typer.Argument(help="Path to spec file to validate")],
    quiet: Annotated[
        bool, typer.Option("--quiet", "-q", help="Only show errors")
    ] = False,
) -> None:
    """Validate a container spec file.

    Checks that the spec file is well-formed and all values are valid.

    Examples:
        gaol spec validate container.yaml
        gaol spec validate *.yaml
    """
    from pathlib import Path

    from gaol.specs import SpecLoader

    file_path = Path(path).resolve()
    if not file_path.exists():
        err_console.print(f"[red]File not found:[/red] {path}")
        raise typer.Exit(1)

    loader = SpecLoader(base_dir=file_path.parent)
    errors = loader.validate(file_path.name)

    if errors:
        err_console.print(f"[red]Validation errors in {path}:[/red]")
        for error in errors:
            err_console.print(f"  - {error}")
        raise typer.Exit(1)

    if not quiet:
        console.print(f"[green]Valid:[/green] {path}")


@spec_app.command(name="show")
def spec_show(
    path: Annotated[str, typer.Argument(help="Path to spec file")],
) -> None:
    """Show parsed contents of a spec file.

    Displays the spec after parsing, variable expansion, and validation.
    """
    from gaol.specs import SpecError, load_spec

    try:
        spec = load_spec(path)
    except SpecError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None

    console.print(f"[bold cyan]Container Spec:[/bold cyan] {spec.name}")
    console.print()

    # Basic info
    console.print("[bold]Basic:[/bold]")
    console.print(f"  Runtime: {spec.runtime}")
    console.print(f"  Distro: {spec.distro}")
    if spec.image:
        console.print(f"  Image: {spec.image}")
    console.print()

    # Profiles
    if spec.profiles:
        console.print(f"[bold]Profiles:[/bold] {', '.join(spec.profiles)}")
        console.print()

    # Resources
    if spec.resources.memory or spec.resources.cpus or spec.resources.disk:
        console.print("[bold]Resources:[/bold]")
        if spec.resources.memory:
            console.print(f"  Memory: {spec.resources.memory}")
        if spec.resources.cpus:
            console.print(f"  CPUs: {spec.resources.cpus}")
        if spec.resources.disk:
            console.print(f"  Disk: {spec.resources.disk}")
        console.print()

    # Network
    if spec.network.ports or spec.network.dns or spec.network.hostname:
        console.print("[bold]Network:[/bold]")
        console.print(f"  Mode: {spec.network.mode}")
        if spec.network.ports:
            console.print("  Ports:")
            for p in spec.network.ports:
                console.print(f"    - {p}")
        if spec.network.dns:
            console.print(f"  DNS: {', '.join(spec.network.dns)}")
        if spec.network.hostname:
            console.print(f"  Hostname: {spec.network.hostname}")
        console.print()

    # Volumes
    if spec.volumes:
        console.print("[bold]Volumes:[/bold]")
        for v in spec.volumes:
            console.print(f"  - {v}")
        console.print()

    # Environment
    if spec.environment:
        console.print("[bold]Environment:[/bold]")
        for key, value in spec.environment.items():
            console.print(f"  {key}={value}")
        console.print()

    # Command
    if spec.command:
        console.print(f"[bold]Command:[/bold] {' '.join(spec.command)}")
        console.print()

    # Security
    if spec.privileged or spec.read_only:
        console.print("[bold]Security:[/bold]")
        if spec.privileged:
            console.print("  Privileged: yes")
        if spec.read_only:
            console.print("  Read-only: yes")


@spec_app.command(name="export")
def spec_export(
    name: Annotated[str, typer.Argument(help="Container name to export")],
    output: Annotated[
        str | None,
        typer.Option("--output", "-o", help="Output file path"),
    ] = None,
    format: Annotated[
        str, typer.Option("--format", "-F", help="Output format (yaml or toml)")
    ] = "yaml",
) -> None:
    """Export a container configuration to a spec file.

    Creates a declarative spec file from an existing container's configuration.

    Examples:
        gaol spec export mycontainer
        gaol spec export mycontainer -o mycontainer.yaml
        gaol spec export mycontainer --format toml
    """
    from pathlib import Path

    from gaol.specs import ContainerSpec, spec_to_yaml

    store = get_container_store()
    stored = store.get(name)

    if not stored:
        err_console.print(f"[red]Container not found in store:[/red] {name}")
        raise typer.Exit(1)

    # Convert to spec
    spec = ContainerSpec.from_container_config(stored.config)

    # Generate output
    if format.lower() == "toml":
        try:
            from gaol.specs import spec_to_toml

            content = spec_to_toml(spec)
        except ImportError as e:
            err_console.print(f"[red]Error:[/red] {e}")
            raise typer.Exit(1) from None
    else:
        content = spec_to_yaml(spec)

    if output:
        output_path = Path(output)
        output_path.write_text(content)
        console.print(f"[green]Exported to:[/green] {output_path}")
    else:
        console.print(content)


@spec_app.command(name="init")
def spec_init(
    path: Annotated[
        str, typer.Argument(help="Path for the new spec file")
    ] = "container.yaml",
    name: Annotated[
        str | None, typer.Option("--name", "-n", help="Container name")
    ] = None,
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
    distro: Annotated[
        str | None, typer.Option("--distro", "-d", help="Base distribution")
    ] = None,
    force: Annotated[
        bool, typer.Option("--force", "-f", help="Overwrite existing file")
    ] = False,
) -> None:
    """Create a new container spec file template.

    Generates a starter spec file with common options commented out.

    Examples:
        gaol spec init
        gaol spec init myapp.yaml --name myapp
        gaol spec init dev.toml --runtime nspawn
    """
    from pathlib import Path

    file_path = Path(path)

    if file_path.exists() and not force:
        err_console.print(f"[red]File exists:[/red] {path}")
        err_console.print("Use --force to overwrite")
        raise typer.Exit(1)

    app_config = get_config()
    container_name = name or file_path.stem

    # Determine format from extension
    is_toml = file_path.suffix.lower() == ".toml"

    if is_toml:
        content = f'''# Container specification for {container_name}
# See: gaol spec --help

name = "{container_name}"
runtime = "{runtime or app_config.default_runtime}"
distro = "{distro or app_config.default_distro}"

# Profiles to apply
# profiles = ["base", "dev-python"]

# Resource limits
[resources]
# memory = "4G"
# cpus = 2
# disk = "10G"

# Network configuration
[network]
mode = "shared"
# ports = ["8080:80", "443:443"]
# dns = ["1.1.1.1", "8.8.8.8"]
# hostname = "{container_name}"

# Volume mounts
# volumes = [
#     "~/projects:/workspace",
#     "~/.ssh:/home/user/.ssh:ro",
# ]

# Environment variables
[environment]
# EDITOR = "vim"
# DEBUG = "1"

# Command and working directory
# command = ["bash", "-l"]
# working_dir = "/workspace"

# Security options
privileged = false
read_only = false
'''
    else:
        content = f'''# Container specification for {container_name}
# See: gaol spec --help

name: {container_name}
runtime: {runtime or app_config.default_runtime}
distro: {distro or app_config.default_distro}

# Profiles to apply
# profiles:
#   - base
#   - dev-python

# Resource limits
# resources:
#   memory: 4G
#   cpus: 2
#   disk: 10G

# Network configuration
network:
  mode: shared
  # ports:
  #   - 8080:80
  #   - 443:443
  # dns:
  #   - 1.1.1.1
  #   - 8.8.8.8
  # hostname: {container_name}

# Volume mounts
# volumes:
#   - ~/projects:/workspace
#   - ~/.ssh:/home/user/.ssh:ro

# Environment variables
# environment:
#   EDITOR: vim
#   DEBUG: "1"

# Command and working directory
# command: ["bash", "-l"]
# working_dir: /workspace

# Security options
privileged: false
read_only: false
'''

    file_path.write_text(content)
    console.print(f"[green]Created spec file:[/green] {file_path}")


@spec_app.command(name="import")
def spec_import(
    url: Annotated[str, typer.Argument(help="URL or reference to import from (https://, github:, gist:)")],
    name: Annotated[
        str | None, typer.Option("--name", "-n", help="Override name for the spec")
    ] = None,
    force: Annotated[
        bool, typer.Option("--force", "-f", help="Overwrite if spec already exists")
    ] = False,
    skip_scan: Annotated[
        bool, typer.Option("--skip-scan", help="Skip security scanning")
    ] = False,
    json_output: Annotated[
        bool, typer.Option("--json", "-j", help="Output result as JSON")
    ] = False,
) -> None:
    """Import a spec from a URL.

    Supports:
      - https://... or http://... - Direct URLs
      - github:user/repo/path - GitHub raw URL
      - github:user/repo/path@branch - GitHub raw URL with branch
      - gist:id - GitHub gist
      - gist:id/filename - GitHub gist file

    Examples:
        gaol spec import https://example.com/container.yaml
        gaol spec import github:user/repo/specs/webserver.yaml
        gaol spec import github:user/repo/specs/dev.yaml@develop
        gaol spec import gist:abc123/my-spec.yaml
    """
    import json

    from gaol.specs.loader import import_spec_from_url
    from gaol.sharing.exceptions import (
        FetchError,
        ImportError,
        ContentValidationError,
        SpecExistsError,
    )

    try:
        result = import_spec_from_url(
            url,
            name=name,
            skip_security_scan=skip_scan,
            overwrite=force,
        )
    except SpecExistsError as e:
        err_console.print(f"[red]Spec already exists:[/red] {e.name}")
        err_console.print("Use --force to overwrite")
        raise typer.Exit(1) from e
    except FetchError as e:
        err_console.print(f"[red]Failed to fetch:[/red] {e}")
        raise typer.Exit(1) from e
    except ContentValidationError as e:
        err_console.print(f"[red]Invalid spec:[/red] {e}")
        raise typer.Exit(1) from e
    except ImportError as e:
        err_console.print(f"[red]Import failed:[/red] {e}")
        raise typer.Exit(1) from e

    if json_output:
        output = {
            "success": result.success,
            "name": result.name,
            "source_url": result.source_url,
            "local_path": str(result.local_path) if result.local_path else None,
            "trust_level": result.trust_level.value,
            "warnings": result.warnings,
            "errors": result.errors,
        }
        if result.security_scan:
            output["security_scan"] = {
                "passed": result.security_scan.passed,
                "risk_level": result.security_scan.risk_level,
                "findings": [
                    {
                        "severity": f.severity.value,
                        "category": f.category,
                        "message": f.message,
                        "location": f.location,
                    }
                    for f in result.security_scan.findings
                ],
            }
        console.print(json.dumps(output, indent=2))
        return

    # Display result
    console.print(f"[green]Imported spec:[/green] {result.name}")
    console.print(f"  Source: {result.source_url}")
    if result.local_path:
        console.print(f"  Saved to: {result.local_path}")
    console.print(f"  Trust level: {result.trust_level.value}")

    # Show security scan results
    if result.security_scan:
        if result.security_scan.passed:
            console.print(f"  Security: [green]passed[/green] (risk: {result.security_scan.risk_level})")
        else:
            console.print(f"  Security: [red]issues found[/red] (risk: {result.security_scan.risk_level})")

        if result.security_scan.findings:
            console.print()
            console.print("[bold]Security findings:[/bold]")
            for finding in result.security_scan.findings:
                color = {
                    "critical": "red",
                    "error": "red",
                    "warning": "yellow",
                    "info": "dim",
                }.get(finding.severity.value, "white")
                console.print(f"  [{color}][{finding.severity.value.upper()}][/{color}] {finding.message}")
                if finding.location:
                    console.print(f"    at {finding.location}")

    # Show warnings
    if result.warnings:
        console.print()
        console.print("[yellow]Warnings:[/yellow]")
        for warning in result.warnings:
            console.print(f"  - {warning}")


# =============================================================================
# Secret subcommands
# =============================================================================


@secret_app.command(name="init")
def secret_init(
    force: Annotated[
        bool, typer.Option("--force", "-f", help="Reinitialize (destroys existing secrets)")
    ] = False,
) -> None:
    """Initialize the secrets store.

    Creates a new master key and stores it in your system keyring.
    This must be run before creating any secrets.

    Under sudo, the invoking user's config directory is resolved automatically
    (via SUDO_USER). You can also set XDG_CONFIG_HOME explicitly to override.

    If no system keyring is available (e.g. headless servers, CI), set the
    GAOL_MASTER_KEY environment variable to a base64- or hex-encoded 32-byte key.

    Examples:
        gaol secret init
        gaol secret init --force  # Reinitialize
    """
    from gaol.secrets import SecretExistsError, get_secret_store

    store = get_secret_store()

    if store.is_initialized() and not force:
        err_console.print(
            "[yellow]Secrets store is already initialized.[/yellow]\n"
            "Use --force to reinitialize (this will NOT delete existing secrets)."
        )
        raise typer.Exit(1)

    try:
        message = store.initialize(force=force)
        console.print(f"[green]Secrets store initialized.[/green] {message}")
    except SecretExistsError:
        err_console.print("[red]Secrets store already initialized[/red]")
        raise typer.Exit(1) from None
    except Exception as e:
        err_console.print(f"[red]Failed to initialize secrets store:[/red] {e}")
        raise typer.Exit(1) from None


@secret_app.command(name="create")
def secret_create(
    name: Annotated[str, typer.Argument(help="Secret name")],
    value: Annotated[
        str | None, typer.Option("--value", "-v", help="Secret value (prompts if not provided)")
    ] = None,
    description: Annotated[
        str | None, typer.Option("--description", "-d", help="Secret description")
    ] = None,
    from_file: Annotated[
        str | None, typer.Option("--from-file", "-f", help="Read value from file")
    ] = None,
    force: Annotated[bool, typer.Option("--force", help="Overwrite if exists")] = False,
) -> None:
    """Create a new secret.

    The secret value can be provided via --value, --from-file, or interactively.

    Examples:
        gaol secret create DB_PASSWORD
        gaol secret create API_KEY --value "abc123"
        gaol secret create SSH_KEY --from-file ~/.ssh/id_rsa
    """
    import getpass
    from pathlib import Path

    from gaol.secrets import (
        InvalidSecretNameError,
        SecretExistsError,
        SecretStoreNotInitializedError,
        get_secret_store,
    )

    store = get_secret_store()

    # Determine the value
    if from_file:
        file_path = Path(from_file).expanduser()
        if not file_path.exists():
            err_console.print(f"[red]File not found:[/red] {from_file}")
            raise typer.Exit(1)
        secret_value = file_path.read_text()
    elif value is not None:
        secret_value = value
    else:
        # Prompt for value
        try:
            secret_value = getpass.getpass("Secret value: ")
            if not secret_value:
                err_console.print("[red]Secret value cannot be empty[/red]")
                raise typer.Exit(1)
        except KeyboardInterrupt:
            console.print("\n[yellow]Cancelled[/yellow]")
            raise typer.Exit(1) from None

    try:
        metadata = store.create(name, secret_value, description=description, force=force)
        console.print(f"[green]Created secret:[/green] {metadata.name}")
    except SecretStoreNotInitializedError:
        err_console.print(
            "[red]Secrets store not initialized.[/red]\n"
            "Run 'gaol secret init' first."
        )
        raise typer.Exit(1) from None
    except SecretExistsError:
        err_console.print(
            f"[red]Secret '{name}' already exists.[/red]\n"
            "Use --force to overwrite."
        )
        raise typer.Exit(1) from None
    except InvalidSecretNameError as e:
        err_console.print(f"[red]{e}[/red]")
        raise typer.Exit(1) from None
    except Exception as e:
        err_console.print(f"[red]Failed to create secret:[/red] {e}")
        raise typer.Exit(1) from None


@secret_app.command(name="list")
def secret_list(
    json_output: Annotated[
        bool, typer.Option("--json", "-j", help="Output as JSON")
    ] = False,
) -> None:
    """List all secrets (names only, not values)."""
    import json

    from gaol.secrets import get_secret_store

    store = get_secret_store()

    if not store.is_initialized():
        if json_output:
            console.print("[]")
        else:
            console.print("[dim]Secrets store not initialized[/dim]")
        return

    secrets = store.list()

    if json_output:
        data = [
            {
                "name": s.name,
                "created_at": s.created_at.isoformat(),
                "updated_at": s.updated_at.isoformat(),
                "description": s.description,
            }
            for s in secrets
        ]
        console.print(json.dumps(data, indent=2))
        return

    if not secrets:
        console.print("[dim]No secrets stored[/dim]")
        return

    table = Table(title="Secrets")
    table.add_column("Name", style="cyan")
    table.add_column("Created")
    table.add_column("Updated")
    table.add_column("Description", style="dim")

    for s in secrets:
        table.add_row(
            s.name,
            s.created_at.strftime("%Y-%m-%d %H:%M"),
            s.updated_at.strftime("%Y-%m-%d %H:%M"),
            s.description or "-",
        )

    console.print(table)


@secret_app.command(name="show")
def secret_show(
    name: Annotated[str, typer.Argument(help="Secret name")],
) -> None:
    """Show the value of a secret.

    Warning: This displays the secret value in plaintext.
    """
    from gaol.secrets import (
        SecretNotFoundError,
        SecretStoreNotInitializedError,
        get_secret_store,
    )

    store = get_secret_store()

    try:
        value = store.get(name)
        console.print(value)
    except SecretStoreNotInitializedError:
        err_console.print("[red]Secrets store not initialized[/red]")
        raise typer.Exit(1) from None
    except SecretNotFoundError:
        err_console.print(f"[red]Secret not found:[/red] {name}")
        raise typer.Exit(1) from None
    except Exception as e:
        err_console.print(f"[red]Failed to retrieve secret:[/red] {e}")
        raise typer.Exit(1) from None


@secret_app.command(name="delete")
def secret_delete(
    name: Annotated[str, typer.Argument(help="Secret name")],
    force: Annotated[bool, typer.Option("--force", "-f", help="Skip confirmation")] = False,
) -> None:
    """Delete a secret."""
    from gaol.secrets import (
        SecretNotFoundError,
        SecretStoreNotInitializedError,
        get_secret_store,
    )

    store = get_secret_store()

    if not force:
        confirm = typer.confirm(f"Delete secret '{name}'?")
        if not confirm:
            console.print("[yellow]Cancelled[/yellow]")
            raise typer.Exit(0)

    try:
        store.delete(name)
        console.print(f"[green]Deleted secret:[/green] {name}")
    except SecretStoreNotInitializedError:
        err_console.print("[red]Secrets store not initialized[/red]")
        raise typer.Exit(1) from None
    except SecretNotFoundError:
        err_console.print(f"[red]Secret not found:[/red] {name}")
        raise typer.Exit(1) from None
    except Exception as e:
        err_console.print(f"[red]Failed to delete secret:[/red] {e}")
        raise typer.Exit(1) from None


@secret_app.command(name="update")
def secret_update(
    name: Annotated[str, typer.Argument(help="Secret name")],
    value: Annotated[str | None, typer.Option("--value", "-v", help="New value")] = None,
    from_file: Annotated[
        str | None, typer.Option("--from-file", "-f", help="Read value from file")
    ] = None,
    description: Annotated[
        str | None, typer.Option("--description", "-d", help="New description")
    ] = None,
) -> None:
    """Update an existing secret."""
    import getpass
    from pathlib import Path

    from gaol.secrets import (
        SecretNotFoundError,
        SecretStoreNotInitializedError,
        get_secret_store,
    )

    store = get_secret_store()

    # Determine the value
    if from_file:
        file_path = Path(from_file).expanduser()
        if not file_path.exists():
            err_console.print(f"[red]File not found:[/red] {from_file}")
            raise typer.Exit(1)
        secret_value = file_path.read_text()
    elif value is not None:
        secret_value = value
    else:
        # Prompt for value
        try:
            secret_value = getpass.getpass("New secret value: ")
            if not secret_value:
                err_console.print("[red]Secret value cannot be empty[/red]")
                raise typer.Exit(1)
        except KeyboardInterrupt:
            console.print("\n[yellow]Cancelled[/yellow]")
            raise typer.Exit(1) from None

    try:
        metadata = store.update(name, secret_value, description=description)
        console.print(f"[green]Updated secret:[/green] {metadata.name}")
    except SecretStoreNotInitializedError:
        err_console.print("[red]Secrets store not initialized[/red]")
        raise typer.Exit(1) from None
    except SecretNotFoundError:
        err_console.print(f"[red]Secret not found:[/red] {name}")
        raise typer.Exit(1) from None
    except Exception as e:
        err_console.print(f"[red]Failed to update secret:[/red] {e}")
        raise typer.Exit(1) from None


@secret_app.command(name="check")
def secret_check() -> None:
    """Check secrets store status and keyring availability.

    Shows the resolved config directory path, which is useful to verify
    correct path resolution when running under sudo.
    """
    from gaol.config import get_config_dir
    from gaol.secrets import get_key_manager, get_secret_store

    store = get_secret_store()

    # Show resolved config directory
    config_dir = get_config_dir()
    console.print(f"[bold]Config Directory[/bold]: {config_dir}")
    console.print()

    # Check store status
    console.print("[bold]Secrets Store Status[/bold]")
    if store.is_initialized():
        console.print("  Initialized: [green]yes[/green]")
        secrets = store.list()
        console.print(f"  Secrets stored: {len(secrets)}")
    else:
        console.print("  Initialized: [yellow]no[/yellow]")
        console.print("  [dim]Run 'gaol secret init' to initialize[/dim]")

    console.print()

    # Check key providers
    console.print("[bold]Key Providers[/bold]")
    key_manager = get_key_manager()

    for provider in key_manager._providers:
        available = provider.is_available()
        status = "[green]available[/green]" if available else "[dim]not available[/dim]"
        console.print(f"  {provider.source.value}: {status}")


# =============================================================================
# Security subcommands
# =============================================================================

# Security subcommand
security_app = typer.Typer(help="Security monitoring and policy management")
app.add_typer(security_app, name="security", rich_help_panel=PANEL_MONITORING)

# Security rules sub-subcommand
security_rules_app = typer.Typer(help="Manage Falco security rules")
security_app.add_typer(security_rules_app, name="rules")

# Security policy sub-subcommand
security_policy_app = typer.Typer(help="Manage security policies (seccomp/AppArmor)")
security_app.add_typer(security_policy_app, name="policy")


@security_app.command(name="init")
def security_init(
    auto_install: Annotated[
        bool, typer.Option("--auto-install", "-y", help="Automatically install Falco if not present")
    ] = False,
    driver: Annotated[
        str, typer.Option("--driver", "-d", help="Falco driver type (modern_ebpf, ebpf, kmod)")
    ] = "modern_ebpf",
) -> None:
    """Initialize security monitoring.

    Checks Falco status and optionally installs and configures it.

    Examples:
        gaol security init
        gaol security init --auto-install
        gaol security init --auto-install --driver ebpf
    """
    from gaol.security import (
        FalcoInstallError,
        UnsupportedDistroError,
        get_falco_installer,
    )

    installer = get_falco_installer()
    status = installer.get_status()

    if status.installed:
        console.print(f"[green]Falco is installed[/green] (version {status.version or 'unknown'})")
    else:
        console.print("[yellow]Falco is not installed[/yellow]")

        if auto_install:
            console.print("Installing Falco...")
            try:
                installer.install(driver_type=driver, enable_grpc=True)
                console.print("[green]Falco installed successfully[/green]")
                status = installer.get_status()
            except UnsupportedDistroError as e:
                err_console.print(f"[red]Unsupported distribution:[/red] {e.distro}")
                err_console.print("Please install Falco manually: https://falco.org/docs/getting-started/installation/")
                raise typer.Exit(1) from None
            except FalcoInstallError as e:
                err_console.print(f"[red]Installation failed:[/red] {e}")
                raise typer.Exit(1) from None
        else:
            console.print("Run with --auto-install to install Falco automatically")
            raise typer.Exit(0)

    if status.running:
        console.print("[green]Falco service is running[/green]")
    else:
        console.print("[yellow]Falco service is not running[/yellow]")
        if auto_install:
            try:
                installer.enable_service()
                installer.restart_service()
                console.print("[green]Falco service started[/green]")
            except Exception as e:
                err_console.print(f"[yellow]Could not start service:[/yellow] {e}")
                console.print("Try: sudo systemctl start falco")

    if status.grpc_enabled:
        console.print(f"[green]gRPC output enabled[/green] ({status.grpc_socket or status.grpc_address})")
    else:
        console.print("[yellow]gRPC output not enabled[/yellow]")
        if auto_install:
            try:
                installer.configure_grpc(socket_path="/run/falco/falco.sock")
                installer.restart_service()
                console.print("[green]gRPC output configured[/green]")
            except Exception as e:
                err_console.print(f"[yellow]Could not configure gRPC:[/yellow] {e}")

    if status.driver_type:
        console.print(f"[bold]Driver:[/bold] {status.driver_type}")


@security_app.command(name="status")
def security_status(
    json_output: Annotated[
        bool, typer.Option("--json", "-j", help="Output as JSON")
    ] = False,
) -> None:
    """Show Falco security monitoring status."""
    import json

    from gaol.security import get_falco_client

    client = get_falco_client()
    status = client.get_status()

    if json_output:
        console.print(json.dumps(status.model_dump(), indent=2))
        return

    console.print("[bold]Falco Security Status[/bold]")
    console.print()

    # Installation
    if status.installed:
        console.print(f"  Installed: [green]yes[/green] (version {status.version or 'unknown'})")
    else:
        console.print("  Installed: [red]no[/red]")
        console.print("  [dim]Run 'gaol security init --auto-install' to install[/dim]")
        return

    # Service status
    if status.running:
        console.print("  Running: [green]yes[/green]")
    else:
        console.print("  Running: [red]no[/red]")

    # Driver
    if status.driver_type:
        console.print(f"  Driver: {status.driver_type}")

    # gRPC
    if status.grpc_enabled:
        endpoint = status.grpc_socket or status.grpc_address or "enabled"
        console.print(f"  gRPC: [green]enabled[/green] ({endpoint})")
    else:
        console.print("  gRPC: [dim]disabled[/dim]")

    # Rules loaded
    if status.rules_loaded:
        console.print(f"  Rules loaded: {status.rules_loaded}")


@security_app.command(name="events")
def security_events(
    follow: Annotated[
        bool, typer.Option("--follow", "-f", help="Follow events in real-time")
    ] = False,
    tail: Annotated[
        int, typer.Option("--tail", "-n", help="Show last N events")
    ] = 50,
    container: Annotated[
        str | None, typer.Option("--container", "-c", help="Filter by container name")
    ] = None,
    priority: Annotated[
        str | None, typer.Option("--priority", "-p", help="Minimum priority (Emergency, Alert, Critical, Error, Warning, Notice, Informational, Debug)")
    ] = None,
    rule: Annotated[
        str | None, typer.Option("--rule", "-r", help="Filter by rule name")
    ] = None,
    json_output: Annotated[
        bool, typer.Option("--json", "-j", help="Output as JSON")
    ] = False,
) -> None:
    """Show security events from Falco.

    Examples:
        gaol security events
        gaol security events --follow
        gaol security events --container myapp --priority Warning
        gaol security events --tail 100 --json
    """
    import json

    from gaol.security import (
        FalcoConnectionError,
        FalcoNotRunningError,
        FalcoPriority,
        get_falco_client,
    )

    client = get_falco_client()

    # Parse priority filter
    min_priority = None
    if priority:
        try:
            min_priority = FalcoPriority.from_string(priority)
        except ValueError:
            err_console.print(f"[red]Invalid priority:[/red] {priority}")
            err_console.print("Valid priorities: Emergency, Alert, Critical, Error, Warning, Notice, Informational, Debug")
            raise typer.Exit(1) from None

    try:
        client.connect()
    except FalcoNotRunningError:
        err_console.print("[red]Falco is not running[/red]")
        console.print("Start it with: sudo systemctl start falco")
        raise typer.Exit(1) from None
    except FalcoConnectionError as e:
        err_console.print(f"[red]Connection error:[/red] {e}")
        raise typer.Exit(1) from None

    container_filters = [container] if container else None
    rule_filters = [rule] if rule else None

    if follow:
        # Real-time streaming
        console.print("[dim]Streaming events (Ctrl+C to stop)...[/dim]")
        try:
            for event in client.stream_events(
                filter_priority=min_priority,
                filter_containers=container_filters,
                filter_rules=rule_filters,
            ):
                if json_output:
                    console.print(json.dumps(event.model_dump(mode="json")))
                else:
                    priority_style = {
                        "Emergency": "red bold",
                        "Alert": "red",
                        "Critical": "red",
                        "Error": "yellow",
                        "Warning": "yellow",
                        "Notice": "cyan",
                        "Informational": "dim",
                        "Debug": "dim",
                    }.get(event.priority.value, "")
                    console.print(
                        f"[{priority_style}]{event.priority.value}[/{priority_style}] "
                        f"[cyan]{event.rule}[/cyan]: {event.output}"
                    )
        except KeyboardInterrupt:
            console.print("\n[dim]Stopped[/dim]")
    else:
        # Show recent events from buffer
        # For now, we can't show historical events without a buffer
        # So we'll just start streaming and show first N events
        console.print(f"[dim]Showing last {tail} events (streaming not available, showing live events)...[/dim]")
        count = 0
        try:
            for event in client.stream_events(
                filter_priority=min_priority,
                filter_containers=container_filters,
                filter_rules=rule_filters,
            ):
                if json_output:
                    console.print(json.dumps(event.model_dump(mode="json")))
                else:
                    priority_style = {
                        "Emergency": "red bold",
                        "Alert": "red",
                        "Critical": "red",
                        "Error": "yellow",
                        "Warning": "yellow",
                        "Notice": "cyan",
                        "Informational": "dim",
                        "Debug": "dim",
                    }.get(event.priority.value, "")
                    console.print(
                        f"[{priority_style}]{event.priority.value}[/{priority_style}] "
                        f"[cyan]{event.rule}[/cyan]: {event.output}"
                    )
                count += 1
                if count >= tail:
                    break
        except KeyboardInterrupt:
            pass

    client.disconnect()


# --- Security rules subcommands ---


@security_rules_app.command(name="list")
def security_rules_list(
    json_output: Annotated[
        bool, typer.Option("--json", "-j", help="Output as JSON")
    ] = False,
) -> None:
    """List security rules."""
    import json

    from gaol.security import get_rule_manager

    manager = get_rule_manager()
    rules = manager.list_rules()

    if json_output:
        data = [r.model_dump() for r in rules]
        console.print(json.dumps(data, indent=2))
        return

    if not rules:
        console.print("[dim]No rules found[/dim]")
        return

    table = Table(title="Security Rules")
    table.add_column("Name", style="cyan")
    table.add_column("Priority")
    table.add_column("Enabled")
    table.add_column("Tags", style="dim")
    table.add_column("Description")

    for rule in rules:
        priority_style = {
            "Emergency": "red bold",
            "Alert": "red",
            "Critical": "red",
            "Error": "yellow",
            "Warning": "yellow",
        }.get(rule.priority.value, "")
        enabled = "[green]yes[/green]" if rule.enabled else "[dim]no[/dim]"
        table.add_row(
            rule.name,
            f"[{priority_style}]{rule.priority.value}[/{priority_style}]",
            enabled,
            ", ".join(rule.tags[:3]) + ("..." if len(rule.tags) > 3 else ""),
            (rule.description[:40] + "...") if len(rule.description) > 43 else rule.description,
        )

    console.print(table)


@security_rules_app.command(name="show")
def security_rules_show(
    name: Annotated[str, typer.Argument(help="Rule name")],
) -> None:
    """Show details of a security rule."""
    from gaol.security import RuleNotFoundError, get_rule_manager

    manager = get_rule_manager()

    try:
        rule = manager.get_rule(name)
    except RuleNotFoundError:
        err_console.print(f"[red]Rule not found:[/red] {name}")
        raise typer.Exit(1) from None

    console.print(f"[bold cyan]Rule:[/bold cyan] {rule.name}")
    console.print(f"[bold]Description:[/bold] {rule.description or '-'}")
    console.print(f"[bold]Priority:[/bold] {rule.priority.value}")
    console.print(f"[bold]Enabled:[/bold] {'yes' if rule.enabled else 'no'}")
    console.print(f"[bold]Source:[/bold] {rule.source}")
    if rule.tags:
        console.print(f"[bold]Tags:[/bold] {', '.join(rule.tags)}")
    console.print()
    console.print("[bold]Condition:[/bold]")
    console.print(f"  {rule.condition}")
    console.print()
    console.print("[bold]Output:[/bold]")
    console.print(f"  {rule.output}")


@security_rules_app.command(name="create")
def security_rules_create(
    name: Annotated[str, typer.Argument(help="Rule name")],
    condition: Annotated[str, typer.Option("--condition", "-c", help="Rule condition")] = "",
    output: Annotated[str, typer.Option("--output", "-o", help="Output message")] = "",
    priority: Annotated[str, typer.Option("--priority", "-p", help="Priority level")] = "Warning",
    description: Annotated[str | None, typer.Option("--description", "-d", help="Rule description")] = None,
    tags: Annotated[list[str] | None, typer.Option("--tag", "-t", help="Rule tags")] = None,
) -> None:
    """Create a new security rule.

    Examples:
        gaol security rules create my_rule \\
            --condition "container and proc.name=bash" \\
            --output "Shell in container" \\
            --priority Warning
    """
    from gaol.security import (
        FalcoPriority,
        RuleExistsError,
        RuleValidationError,
        SecurityRule,
        get_rule_manager,
    )

    # Parse priority
    try:
        rule_priority = FalcoPriority.from_string(priority)
    except ValueError:
        err_console.print(f"[red]Invalid priority:[/red] {priority}")
        raise typer.Exit(1) from None

    rule = SecurityRule(
        name=name,
        description=description or "",
        condition=condition,
        output=output,
        priority=rule_priority,
        tags=tags or [],
    )

    manager = get_rule_manager()

    try:
        path = manager.create_rule(rule)
        console.print(f"[green]Created rule:[/green] {name}")
        console.print(f"  Saved to: {path}")
    except RuleExistsError:
        err_console.print(f"[red]Rule already exists:[/red] {name}")
        raise typer.Exit(1) from None
    except RuleValidationError as e:
        err_console.print(f"[red]Invalid rule:[/red]")
        for error in e.errors:
            err_console.print(f"  - {error}")
        raise typer.Exit(1) from None


@security_rules_app.command(name="enable")
def security_rules_enable(
    name: Annotated[str, typer.Argument(help="Rule name")],
) -> None:
    """Enable a security rule."""
    from gaol.security import RuleNotFoundError, get_rule_manager

    manager = get_rule_manager()

    try:
        manager.enable_rule(name)
        console.print(f"[green]Enabled rule:[/green] {name}")
    except RuleNotFoundError:
        err_console.print(f"[red]Rule not found:[/red] {name}")
        raise typer.Exit(1) from None


@security_rules_app.command(name="disable")
def security_rules_disable(
    name: Annotated[str, typer.Argument(help="Rule name")],
) -> None:
    """Disable a security rule."""
    from gaol.security import RuleNotFoundError, get_rule_manager

    manager = get_rule_manager()

    try:
        manager.disable_rule(name)
        console.print(f"[yellow]Disabled rule:[/yellow] {name}")
    except RuleNotFoundError:
        err_console.print(f"[red]Rule not found:[/red] {name}")
        raise typer.Exit(1) from None


@security_rules_app.command(name="delete")
def security_rules_delete(
    name: Annotated[str, typer.Argument(help="Rule name")],
    force: Annotated[bool, typer.Option("--force", "-f", help="Skip confirmation")] = False,
) -> None:
    """Delete a security rule."""
    from gaol.security import RuleNotFoundError, get_rule_manager

    if not force:
        confirm = typer.confirm(f"Delete rule '{name}'?")
        if not confirm:
            console.print("[dim]Cancelled[/dim]")
            raise typer.Exit(0)

    manager = get_rule_manager()

    try:
        manager.delete_rule(name)
        console.print(f"[green]Deleted rule:[/green] {name}")
    except RuleNotFoundError:
        err_console.print(f"[red]Rule not found:[/red] {name}")
        raise typer.Exit(1) from None


@security_rules_app.command(name="import")
def security_rules_import(
    path: Annotated[str, typer.Argument(help="Path to rules YAML file")],
) -> None:
    """Import rules from a YAML file."""
    from pathlib import Path

    from gaol.security import get_rule_manager

    file_path = Path(path)
    if not file_path.exists():
        err_console.print(f"[red]File not found:[/red] {path}")
        raise typer.Exit(1)

    manager = get_rule_manager()
    count = manager.import_rules(file_path)

    console.print(f"[green]Imported {count} rules[/green]")


@security_rules_app.command(name="export")
def security_rules_export(
    path: Annotated[str, typer.Option("--output", "-o", help="Output file path")] = "rules.yaml",
) -> None:
    """Export rules to a YAML file."""
    from pathlib import Path

    from gaol.security import get_rule_manager

    manager = get_rule_manager()
    count = manager.export_rules(Path(path))

    if count == 0:
        console.print("[yellow]No rules to export[/yellow]")
    else:
        console.print(f"[green]Exported {count} rules to:[/green] {path}")


@security_rules_app.command(name="install-builtin")
def security_rules_install_builtin() -> None:
    """Install gaol's built-in security rules."""
    from gaol.security import get_rule_manager

    manager = get_rule_manager()
    count = manager.install_builtin_rules()

    if count == 0:
        console.print("[dim]Built-in rules already installed[/dim]")
    else:
        console.print(f"[green]Installed {count} built-in rules[/green]")

    # Reload Falco rules
    if manager.reload_falco_rules():
        console.print("[green]Falco rules reloaded[/green]")
    else:
        console.print("[yellow]Could not reload Falco rules. Run:[/yellow] sudo pkill -HUP falco")


# --- Security policy subcommands ---


@security_policy_app.command(name="list")
def security_policy_list(
    policy_type: Annotated[
        str | None, typer.Option("--type", "-t", help="Policy type (seccomp, apparmor)")
    ] = None,
) -> None:
    """List saved security policies."""
    from gaol.security import get_apparmor_generator, get_seccomp_generator

    if policy_type is None or policy_type == "seccomp":
        seccomp_gen = get_seccomp_generator()
        seccomp_profiles = seccomp_gen.list_profiles()

        if seccomp_profiles:
            console.print("[bold]Seccomp Profiles:[/bold]")
            for p in seccomp_profiles:
                console.print(f"  - {p.name}")
            console.print()

    if policy_type is None or policy_type == "apparmor":
        apparmor_gen = get_apparmor_generator()
        apparmor_profiles = apparmor_gen.list_profiles()

        if apparmor_profiles:
            console.print("[bold]AppArmor Profiles:[/bold]")
            for p in apparmor_profiles:
                console.print(f"  - {p.name}")
            console.print()

    if policy_type is None:
        seccomp_gen = get_seccomp_generator()
        apparmor_gen = get_apparmor_generator()
        if not seccomp_gen.list_profiles() and not apparmor_gen.list_profiles():
            console.print("[dim]No saved policies found[/dim]")


@security_policy_app.command(name="generate")
def security_policy_generate(
    container: Annotated[str, typer.Argument(help="Container name")],
    policy_type: Annotated[
        str, typer.Option("--type", "-t", help="Policy type (seccomp, apparmor)")
    ] = "seccomp",
    mode: Annotated[
        str, typer.Option("--mode", "-m", help="Profile mode (default, restrictive)")
    ] = "default",
    learn: Annotated[
        bool, typer.Option("--learn", "-l", help="Learn from container activity")
    ] = False,
    duration: Annotated[
        int, typer.Option("--duration", "-d", help="Learning duration in seconds")
    ] = 300,
    output: Annotated[
        str | None, typer.Option("--output", "-o", help="Output file path")
    ] = None,
) -> None:
    """Generate a security policy for a container.

    Examples:
        gaol security policy generate mycontainer --type seccomp
        gaol security policy generate mycontainer --type apparmor --mode restrictive
        gaol security policy generate mycontainer --learn --duration 60
    """
    import time
    from pathlib import Path

    from gaol.security import (
        AppArmorNotAvailableError,
        ProfileGenerationError,
        get_apparmor_generator,
        get_seccomp_generator,
    )

    if policy_type == "seccomp":
        generator = get_seccomp_generator()

        if learn:
            console.print(f"[dim]Learning syscalls for {duration} seconds...[/dim]")
            try:
                generator.start_learning(container, duration)
                # Wait for learning to complete
                time.sleep(duration)
                profile = generator.stop_learning()
            except ProfileGenerationError as e:
                err_console.print(f"[red]Learning failed:[/red] {e}")
                raise typer.Exit(1) from None
        elif mode == "restrictive":
            profile = generator.generate_restrictive(container)
        else:
            profile = generator.generate_default(container)

        # Save profile
        output_path = Path(output) if output else None
        saved_path = generator.save_profile(profile, output_path)
        console.print(f"[green]Generated seccomp profile:[/green] {saved_path}")

    elif policy_type == "apparmor":
        generator = get_apparmor_generator()

        if not generator.is_apparmor_available():
            err_console.print("[red]AppArmor is not available on this system[/red]")
            raise typer.Exit(1)

        if learn:
            console.print(f"[dim]Learning access patterns for {duration} seconds...[/dim]")
            try:
                generator.start_learning(container, duration)
                # Wait for learning to complete
                time.sleep(duration)
                profile = generator.stop_learning()
            except ProfileGenerationError as e:
                err_console.print(f"[red]Learning failed:[/red] {e}")
                raise typer.Exit(1) from None
        elif mode == "restrictive":
            profile = generator.generate_restrictive(container)
        else:
            profile = generator.generate_default(container)

        # Save profile
        output_path = Path(output) if output else None
        saved_path = generator.save_profile(profile, output_path)
        console.print(f"[green]Generated AppArmor profile:[/green] {saved_path}")

    else:
        err_console.print(f"[red]Invalid policy type:[/red] {policy_type}")
        err_console.print("Use 'seccomp' or 'apparmor'")
        raise typer.Exit(1)


@security_policy_app.command(name="apply")
def security_policy_apply(
    container: Annotated[str, typer.Argument(help="Container name")],
    profile: Annotated[str, typer.Argument(help="Profile file path")],
    policy_type: Annotated[
        str | None, typer.Option("--type", "-t", help="Policy type (auto-detected from file)")
    ] = None,
) -> None:
    """Apply a security policy to a container.

    Note: The container must be recreated with the policy for it to take effect.

    Examples:
        gaol security policy apply mycontainer profile.json
    """
    from pathlib import Path

    from gaol.security import (
        AppArmorNotAvailableError,
        ProfileGenerationError,
        get_apparmor_generator,
        get_seccomp_generator,
    )

    profile_path = Path(profile)
    if not profile_path.exists():
        err_console.print(f"[red]Profile not found:[/red] {profile}")
        raise typer.Exit(1)

    # Auto-detect type from file extension
    if policy_type is None:
        if profile_path.suffix == ".json":
            policy_type = "seccomp"
        else:
            policy_type = "apparmor"

    if policy_type == "seccomp":
        generator = get_seccomp_generator()
        try:
            loaded_profile = generator.load_profile(profile_path)
            console.print(f"[green]Loaded seccomp profile:[/green] {loaded_profile.name}")
            console.print()
            console.print("[yellow]Note:[/yellow] To apply this profile, recreate the container with:")
            console.print(f"  docker run --security-opt seccomp={profile_path} ...")
        except ProfileGenerationError as e:
            err_console.print(f"[red]Invalid profile:[/red] {e}")
            raise typer.Exit(1) from None

    elif policy_type == "apparmor":
        generator = get_apparmor_generator()

        if not generator.is_apparmor_available():
            err_console.print("[red]AppArmor is not available on this system[/red]")
            raise typer.Exit(1)

        try:
            loaded_profile = generator.load_profile(profile_path)
            generator.install_profile(loaded_profile)
            console.print(f"[green]Installed AppArmor profile:[/green] {loaded_profile.name}")
            console.print()
            console.print("[yellow]Note:[/yellow] To apply this profile, recreate the container with:")
            console.print(f"  docker run --security-opt apparmor={loaded_profile.name} ...")
        except (ProfileGenerationError, AppArmorNotAvailableError) as e:
            err_console.print(f"[red]Failed to apply profile:[/red] {e}")
            raise typer.Exit(1) from None


@security_policy_app.command(name="show")
def security_policy_show(
    profile: Annotated[str, typer.Argument(help="Profile file path")],
) -> None:
    """Show contents of a security profile."""
    import json
    from pathlib import Path

    profile_path = Path(profile)
    if not profile_path.exists():
        err_console.print(f"[red]Profile not found:[/red] {profile}")
        raise typer.Exit(1)

    content = profile_path.read_text()

    if profile_path.suffix == ".json":
        # Pretty-print JSON
        try:
            data = json.loads(content)
            console.print(json.dumps(data, indent=2))
        except json.JSONDecodeError:
            console.print(content)
    else:
        console.print(content)


# =============================================================================
# Container Group subcommands
# =============================================================================

# Group subcommand
group_app = typer.Typer(help="Container group orchestration (pod-like)")
app.add_typer(group_app, name="group", rich_help_panel=PANEL_ORCHESTRATION)


@group_app.command(name="up")
def group_up(
    spec_file: Annotated[str, typer.Argument(help="Path to group spec file (YAML)")],
    detach: Annotated[
        bool, typer.Option("--detach", "-d", help="Run in background")
    ] = True,
    no_health_wait: Annotated[
        bool, typer.Option("--no-health-wait", help="Don't wait for health checks")
    ] = False,
    timeout: Annotated[
        int, typer.Option("--timeout", "-t", help="Health check timeout in seconds")
    ] = 60,
) -> None:
    """Bring up a container group.

    Creates the shared network, then creates and starts all containers
    in dependency order.

    Examples:
        gaol group up group.yaml
        gaol group up group.yaml --detach
        gaol group up group.yaml --no-health-wait
    """
    from pathlib import Path

    import yaml

    from gaol.groups import (
        CircularDependencyError,
        GroupError,
        GroupManager,
        GroupSpec,
    )

    spec_path = Path(spec_file)
    if not spec_path.exists():
        err_console.print(f"[red]Spec file not found:[/red] {spec_file}")
        raise typer.Exit(1)

    try:
        data = yaml.safe_load(spec_path.read_text())
        spec = GroupSpec(**data)
    except Exception as e:
        err_console.print(f"[red]Invalid spec file:[/red] {e}")
        raise typer.Exit(1) from None

    manager = GroupManager()

    try:
        console.print(f"[bold]Bringing up group:[/bold] {spec.name}")
        status = manager.up(
            spec,
            detach=detach,
            wait_for_healthy=not no_health_wait,
            timeout=timeout,
        )

        console.print(f"[green]Group '{spec.name}' is up![/green]")
        console.print()

        # Show container status
        table = Table(show_header=True)
        table.add_column("Container", style="cyan")
        table.add_column("State")
        table.add_column("Runtime")
        table.add_column("IP Address")

        for member in status.containers:
            state_style = "green" if member.state.value == "running" else "yellow"
            table.add_row(
                member.name,
                f"[{state_style}]{member.state.value}[/{state_style}]",
                member.runtime,
                member.ip_address or "-",
            )

        console.print(table)

    except CircularDependencyError as e:
        err_console.print(f"[red]Circular dependency:[/red] {e}")
        raise typer.Exit(1) from None
    except GroupError as e:
        err_console.print(f"[red]Group error:[/red] {e}")
        raise typer.Exit(1) from None


@group_app.command(name="down")
def group_down(
    name: Annotated[str, typer.Argument(help="Group name")],
    volumes: Annotated[
        bool, typer.Option("--volumes", "-v", help="Also remove volumes")
    ] = False,
) -> None:
    """Bring down a container group.

    Stops and removes all containers, then destroys the shared network.

    Examples:
        gaol group down web-stack
        gaol group down web-stack --volumes
    """
    from gaol.groups import GroupError, GroupManager

    manager = GroupManager()

    try:
        console.print(f"[bold]Bringing down group:[/bold] {name}")
        manager.down(name, remove_volumes=volumes)
        console.print(f"[green]Group '{name}' is down[/green]")

    except GroupError as e:
        err_console.print(f"[red]Group error:[/red] {e}")
        raise typer.Exit(1) from None


@group_app.command(name="start")
def group_start(
    name: Annotated[str, typer.Argument(help="Group name")],
) -> None:
    """Start a stopped group.

    Examples:
        gaol group start web-stack
    """
    from gaol.groups import GroupError, GroupManager

    manager = GroupManager()

    try:
        console.print(f"[bold]Starting group:[/bold] {name}")
        status = manager.start(name)
        console.print(f"[green]Group '{name}' started ({status.running_count}/{status.total_count} running)[/green]")

    except GroupError as e:
        err_console.print(f"[red]Group error:[/red] {e}")
        raise typer.Exit(1) from None


@group_app.command(name="stop")
def group_stop(
    name: Annotated[str, typer.Argument(help="Group name")],
) -> None:
    """Stop a running group without removing containers.

    Examples:
        gaol group stop web-stack
    """
    from gaol.groups import GroupError, GroupManager

    manager = GroupManager()

    try:
        console.print(f"[bold]Stopping group:[/bold] {name}")
        manager.stop(name)
        console.print(f"[green]Group '{name}' stopped[/green]")

    except GroupError as e:
        err_console.print(f"[red]Group error:[/red] {e}")
        raise typer.Exit(1) from None


@group_app.command(name="status")
def group_status(
    name: Annotated[str, typer.Argument(help="Group name")],
    json_output: Annotated[
        bool, typer.Option("--json", "-j", help="Output as JSON")
    ] = False,
) -> None:
    """Show status of a container group.

    Examples:
        gaol group status web-stack
        gaol group status web-stack --json
    """
    import json

    from gaol.groups import GroupError, GroupManager

    manager = GroupManager()

    try:
        status = manager.status(name)

        if json_output:
            console.print(json.dumps(status.model_dump(mode="json"), indent=2, default=str))
            return

        state_style = "green" if status.state.value == "running" else "yellow"
        console.print(f"[bold]Group:[/bold] {status.name}")
        console.print(f"[bold]State:[/bold] [{state_style}]{status.state.value}[/{state_style}]")
        if status.network_name:
            console.print(f"[bold]Network:[/bold] {status.network_name}")
        console.print()

        table = Table(show_header=True)
        table.add_column("Container", style="cyan")
        table.add_column("State")
        table.add_column("Runtime")
        table.add_column("Container ID")
        table.add_column("IP Address")

        for member in status.containers:
            state_style = "green" if member.state.value == "running" else "yellow"
            table.add_row(
                member.name,
                f"[{state_style}]{member.state.value}[/{state_style}]",
                member.runtime,
                member.container_id[:12] if member.container_id else "-",
                member.ip_address or "-",
            )

        console.print(table)

    except GroupError as e:
        err_console.print(f"[red]Group error:[/red] {e}")
        raise typer.Exit(1) from None


@group_app.command(name="list")
def group_list(
    all_groups: Annotated[
        bool, typer.Option("--all", "-a", help="Include stopped groups")
    ] = False,
) -> None:
    """List container groups.

    Examples:
        gaol group list
        gaol group list --all
    """
    from gaol.groups import GroupManager

    manager = GroupManager()
    groups = manager.list(include_stopped=all_groups)

    if not groups:
        console.print("[dim]No groups found[/dim]")
        return

    table = Table(show_header=True)
    table.add_column("Name", style="cyan")
    table.add_column("State")
    table.add_column("Containers")
    table.add_column("Running")

    for status in groups:
        state_style = "green" if status.state.value == "running" else "yellow"
        table.add_row(
            status.name,
            f"[{state_style}]{status.state.value}[/{state_style}]",
            str(status.total_count),
            f"{status.running_count}/{status.total_count}",
        )

    console.print(table)


@group_app.command(name="logs")
def group_logs(
    name: Annotated[str, typer.Argument(help="Group name")],
    container: Annotated[
        str | None, typer.Option("--container", "-c", help="Specific container")
    ] = None,
    follow: Annotated[
        bool, typer.Option("--follow", "-f", help="Follow log output")
    ] = False,
    tail: Annotated[
        int | None, typer.Option("--tail", "-n", help="Number of lines from end")
    ] = None,
) -> None:
    """View logs from group containers.

    Examples:
        gaol group logs web-stack
        gaol group logs web-stack --container api
        gaol group logs web-stack --follow
    """
    from gaol.groups import GroupError, GroupManager

    manager = GroupManager()

    try:
        logs = manager.logs(name, container_name=container, follow=follow, tail=tail)
        console.print(logs)

    except GroupError as e:
        err_console.print(f"[red]Group error:[/red] {e}")
        raise typer.Exit(1) from None


@group_app.command(name="exec")
def group_exec(
    name: Annotated[str, typer.Argument(help="Group name")],
    container: Annotated[str, typer.Argument(help="Container name in group")],
    command: Annotated[list[str], typer.Argument(help="Command to execute")],
    interactive: Annotated[
        bool, typer.Option("--interactive", "-i", help="Keep STDIN open")
    ] = False,
    tty: Annotated[
        bool, typer.Option("--tty", "-t", help="Allocate pseudo-TTY")
    ] = False,
) -> None:
    """Execute a command in a group container.

    Examples:
        gaol group exec web-stack api -- python manage.py migrate
        gaol group exec web-stack db -- psql -U postgres
        gaol group exec web-stack api -it -- /bin/bash
    """
    from gaol.groups import GroupError, GroupManager

    manager = GroupManager()

    try:
        exit_code, stdout, stderr = manager.exec(
            name, container, command, interactive=interactive, tty=tty
        )

        if stdout:
            console.print(stdout)
        if stderr:
            err_console.print(stderr)

        raise typer.Exit(exit_code)

    except GroupError as e:
        err_console.print(f"[red]Group error:[/red] {e}")
        raise typer.Exit(1) from None


# =============================================================================
# Devcontainer subcommands
# =============================================================================

# Devcontainer subcommand
devcontainer_app = typer.Typer(help="VS Code devcontainer.json support")
app.add_typer(devcontainer_app, name="devcontainer", rich_help_panel=PANEL_DEVELOPMENT)


@devcontainer_app.command(name="import")
def devcontainer_import(
    path: Annotated[
        str | None,
        typer.Argument(help="Path to devcontainer.json (auto-discovers if not provided)"),
    ] = None,
    output: Annotated[
        str | None,
        typer.Option("--output", "-o", help="Output YAML spec file path"),
    ] = None,
    runtime: Annotated[
        str, typer.Option("--runtime", "-r", help="Runtime to use")
    ] = "docker",
    name: Annotated[
        str | None,
        typer.Option("--name", "-n", help="Override container name"),
    ] = None,
) -> None:
    """Import devcontainer.json as gaol spec.

    Converts a VS Code devcontainer.json file to a gaol container
    specification in YAML format.

    Examples:
        gaol devcontainer import
        gaol devcontainer import .devcontainer/devcontainer.json
        gaol devcontainer import -o container.yaml
    """
    from pathlib import Path

    import yaml

    from gaol.devcontainer import (
        DevcontainerNotFoundError,
        DevcontainerParseError,
        devcontainer_to_spec,
        load_devcontainer,
    )

    try:
        devcontainer = load_devcontainer(Path(path) if path else None)
    except DevcontainerNotFoundError as e:
        err_console.print(f"[red]Not found:[/red] {e}")
        raise typer.Exit(1) from None
    except DevcontainerParseError as e:
        err_console.print(f"[red]Parse error:[/red] {e}")
        raise typer.Exit(1) from None

    # Convert to gaol spec
    spec = devcontainer_to_spec(devcontainer, name=name, runtime=runtime)

    # Output
    spec_dict = spec.model_dump(exclude_none=True, exclude_defaults=True)

    if output:
        output_path = Path(output)
        output_path.write_text(yaml.dump(spec_dict, default_flow_style=False, sort_keys=False))
        console.print(f"[green]Spec saved to:[/green] {output_path}")
    else:
        console.print(yaml.dump(spec_dict, default_flow_style=False, sort_keys=False))


@devcontainer_app.command(name="export")
def devcontainer_export(
    spec_file: Annotated[str, typer.Argument(help="Path to gaol spec file (YAML/TOML)")],
    output_dir: Annotated[
        str | None,
        typer.Option("--output-dir", "-o", help="Output directory (default: .devcontainer)"),
    ] = None,
    extensions: Annotated[
        list[str] | None,
        typer.Option("--extension", "-e", help="VS Code extensions to include"),
    ] = None,
    post_create: Annotated[
        str | None,
        typer.Option("--post-create", help="postCreateCommand to include"),
    ] = None,
) -> None:
    """Export gaol spec to devcontainer.json.

    Converts a gaol container specification to VS Code devcontainer.json
    format.

    Examples:
        gaol devcontainer export container.yaml
        gaol devcontainer export container.yaml --output-dir .devcontainer
        gaol devcontainer export container.yaml -e ms-python.python
    """
    from pathlib import Path

    from gaol.devcontainer import (
        DevcontainerLoader,
        spec_to_devcontainer,
    )
    from gaol.specs.loader import SpecLoader

    spec_path = Path(spec_file)
    if not spec_path.exists():
        err_console.print(f"[red]Spec file not found:[/red] {spec_file}")
        raise typer.Exit(1)

    try:
        loader = SpecLoader()
        spec = loader.load(spec_path)
    except Exception as e:
        err_console.print(f"[red]Failed to load spec:[/red] {e}")
        raise typer.Exit(1) from None

    # Convert to devcontainer
    devcontainer = spec_to_devcontainer(
        spec,
        vscode_extensions=extensions,
        post_create_command=post_create,
    )

    # Save to file
    output_path = Path(output_dir) if output_dir else Path(".devcontainer")
    dc_loader = DevcontainerLoader()
    saved_path = dc_loader.save(devcontainer, output_path / "devcontainer.json")

    console.print(f"[green]Devcontainer saved to:[/green] {saved_path}")


@devcontainer_app.command(name="generate")
def devcontainer_generate(
    container: Annotated[str, typer.Argument(help="Container name from store")],
    output_dir: Annotated[
        str | None,
        typer.Option("--output-dir", "-o", help="Output directory (default: .devcontainer)"),
    ] = None,
    extensions: Annotated[
        list[str] | None,
        typer.Option("--extension", "-e", help="VS Code extensions to include"),
    ] = None,
    post_create: Annotated[
        str | None,
        typer.Option("--post-create", help="postCreateCommand to include"),
    ] = None,
    remote_user: Annotated[
        str | None,
        typer.Option("--remote-user", help="Remote user for VS Code"),
    ] = None,
) -> None:
    """Generate devcontainer.json for an existing container.

    Creates a devcontainer.json file from a container stored in gaol.

    Examples:
        gaol devcontainer generate my-dev-container
        gaol devcontainer generate my-dev-container -o .devcontainer
        gaol devcontainer generate my-dev-container -e ms-python.python
    """
    from pathlib import Path

    from gaol.devcontainer import (
        DevcontainerGenerator,
        DevcontainerLoader,
    )

    try:
        generator = DevcontainerGenerator()
        devcontainer = generator.generate_for_container(
            container,
            vscode_extensions=extensions,
            post_create_command=post_create,
            remote_user=remote_user,
        )
    except ValueError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None

    # Save to file
    output_path = Path(output_dir) if output_dir else Path(".devcontainer")
    dc_loader = DevcontainerLoader()
    saved_path = dc_loader.save(devcontainer, output_path / "devcontainer.json")

    console.print(f"[green]Devcontainer saved to:[/green] {saved_path}")


@devcontainer_app.command(name="validate")
def devcontainer_validate(
    path: Annotated[
        str | None,
        typer.Argument(help="Path to devcontainer.json (auto-discovers if not provided)"),
    ] = None,
) -> None:
    """Validate devcontainer.json file.

    Checks a devcontainer.json file for errors and reports any issues found.

    Examples:
        gaol devcontainer validate
        gaol devcontainer validate .devcontainer/devcontainer.json
    """
    from pathlib import Path

    from gaol.devcontainer import validate_devcontainer

    errors = validate_devcontainer(Path(path) if path else None)

    if errors:
        err_console.print("[red]Validation errors:[/red]")
        for error in errors:
            err_console.print(f"  - {error}")
        raise typer.Exit(1)
    else:
        console.print("[green]Valid devcontainer.json[/green]")


@devcontainer_app.command(name="show")
def devcontainer_show(
    path: Annotated[
        str | None,
        typer.Argument(help="Path to devcontainer.json (auto-discovers if not provided)"),
    ] = None,
    format_: Annotated[
        str,
        typer.Option("--format", "-f", help="Output format (json, yaml, table)"),
    ] = "table",
) -> None:
    """Show parsed devcontainer.json contents.

    Displays the contents of a devcontainer.json file in various formats.

    Examples:
        gaol devcontainer show
        gaol devcontainer show --format json
        gaol devcontainer show --format yaml
    """
    import json
    from pathlib import Path

    import yaml
    from rich.table import Table

    from gaol.devcontainer import (
        DevcontainerNotFoundError,
        DevcontainerParseError,
        load_devcontainer,
    )

    try:
        devcontainer = load_devcontainer(Path(path) if path else None)
    except DevcontainerNotFoundError as e:
        err_console.print(f"[red]Not found:[/red] {e}")
        raise typer.Exit(1) from None
    except DevcontainerParseError as e:
        err_console.print(f"[red]Parse error:[/red] {e}")
        raise typer.Exit(1) from None

    if format_ == "json":
        console.print(json.dumps(devcontainer.to_json(), indent=2))
    elif format_ == "yaml":
        console.print(yaml.dump(devcontainer.to_json(), default_flow_style=False))
    else:
        # Table format
        table = Table(title="Devcontainer Configuration")
        table.add_column("Property", style="cyan")
        table.add_column("Value", style="green")

        if devcontainer.name:
            table.add_row("Name", devcontainer.name)
        if devcontainer.image:
            table.add_row("Image", devcontainer.image)
        if devcontainer.build:
            table.add_row("Dockerfile", devcontainer.build.dockerfile)
        if devcontainer.forward_ports:
            table.add_row("Ports", ", ".join(str(p) for p in devcontainer.forward_ports))
        if devcontainer.features:
            features = ", ".join(devcontainer.features.keys())
            table.add_row("Features", features)
        if devcontainer.workspace_folder:
            table.add_row("Workspace", devcontainer.workspace_folder)
        if devcontainer.remote_user:
            table.add_row("Remote User", devcontainer.remote_user)
        if devcontainer.post_create_command:
            cmd = devcontainer.post_create_command
            if isinstance(cmd, list):
                cmd = " ".join(cmd)
            elif isinstance(cmd, dict):
                cmd = str(cmd)
            table.add_row("Post Create", cmd)

        extensions = devcontainer.get_extensions()
        if extensions:
            table.add_row("Extensions", ", ".join(extensions))

        env = devcontainer.get_all_environment()
        if env:
            env_str = ", ".join(f"{k}={v}" for k, v in list(env.items())[:3])
            if len(env) > 3:
                env_str += f" (+{len(env) - 3} more)"
            table.add_row("Environment", env_str)

        console.print(table)


@devcontainer_app.command(name="init")
def devcontainer_init(
    image: Annotated[
        str | None,
        typer.Option("--image", "-i", help="Container image to use"),
    ] = None,
    name: Annotated[
        str | None,
        typer.Option("--name", "-n", help="Display name for the container"),
    ] = None,
    profile: Annotated[
        str | None,
        typer.Option("--profile", "-p", help="Gaol profile to use"),
    ] = None,
    output_dir: Annotated[
        str | None,
        typer.Option("--output-dir", "-o", help="Output directory (default: .devcontainer)"),
    ] = None,
    extensions: Annotated[
        list[str] | None,
        typer.Option("--extension", "-e", help="VS Code extensions to include"),
    ] = None,
) -> None:
    """Initialize a new devcontainer.json.

    Creates a new devcontainer.json file with sensible defaults.
    Can use a gaol profile for pre-configured setups.

    Examples:
        gaol devcontainer init --image python:3.11
        gaol devcontainer init --profile dev-python
        gaol devcontainer init --image debian:bookworm --name my-dev
    """
    from pathlib import Path

    from gaol.devcontainer import (
        DevcontainerGenerator,
        DevcontainerLoader,
    )

    generator = DevcontainerGenerator()

    if profile:
        devcontainer = generator.generate_for_profile(
            profile,
            name=name,
            image=image,
        )
    elif image:
        devcontainer = generator.generate_minimal(
            image,
            name=name,
            vscode_extensions=extensions,
        )
    else:
        # Default to a basic Debian container
        devcontainer = generator.generate_minimal(
            "debian:bookworm",
            name=name or "dev-container",
            vscode_extensions=extensions,
        )

    # Save to file
    output_path = Path(output_dir) if output_dir else Path(".devcontainer")
    dc_loader = DevcontainerLoader()
    saved_path = dc_loader.save(devcontainer, output_path / "devcontainer.json")

    console.print(f"[green]Devcontainer created at:[/green] {saved_path}")
    console.print("\nTo use this devcontainer:")
    console.print("  1. Open this folder in VS Code")
    console.print("  2. When prompted, click 'Reopen in Container'")
    console.print("  Or use: 'Dev Containers: Reopen in Container' from command palette")


# =============================================================================
# Logs subcommands
# =============================================================================


@logs_app.command(name="search")
def logs_search(
    pattern: Annotated[str, typer.Argument(help="Search pattern")],
    containers: Annotated[
        list[str] | None, typer.Option("--container", "-c", help="Container names to search")
    ] = None,
    since: Annotated[
        str | None, typer.Option("--since", "-s", help="Start time (ISO format or relative like '1h', '30m')")
    ] = None,
    until: Annotated[
        str | None, typer.Option("--until", "-u", help="End time")
    ] = None,
    level: Annotated[
        list[str] | None, typer.Option("--level", "-l", help="Log levels to include (debug, info, warning, error, critical)")
    ] = None,
    regex: Annotated[
        bool, typer.Option("--regex", "-E", help="Treat pattern as regex")
    ] = False,
    limit: Annotated[
        int | None, typer.Option("--limit", "-n", help="Maximum entries to show")
    ] = None,
    context: Annotated[
        int, typer.Option("--context", "-C", help="Lines of context around matches")
    ] = 0,
) -> None:
    """Search logs across containers.

    Search for a pattern in logs from one or more containers.
    Supports text and regex patterns with time filtering.

    Examples:
        gaol log search "error"
        gaol log search "connection refused" --container myapp
        gaol log search "HTTP [45]\\d\\d" --regex --since 1h
        gaol log search "timeout" --level error --level critical
    """
    from gaol.logs import LogSearcher, create_query, get_log_store

    store = get_log_store()
    query = create_query(
        since=since,
        until=until,
        containers=containers,
        levels=level,
        limit=limit,
    )

    # Get entries
    if containers:
        entries = []
        for name in containers:
            entries.extend(store.query(name, query))
    else:
        entries = store.query_all(query)

    if not entries:
        console.print("[dim]No log entries found[/dim]")
        return

    # Search
    searcher = LogSearcher(entries)
    if context > 0:
        results = searcher.grep(pattern, context=context)
        for match, before, after in results:
            for e in before:
                console.print(f"[dim]{_format_log_entry(e)}[/dim]")
            console.print(f"[bold yellow]{_format_log_entry(match)}[/bold yellow]")
            for e in after:
                console.print(f"[dim]{_format_log_entry(e)}[/dim]")
            console.print("---")
    else:
        matches = searcher.search(pattern, regex=regex)
        if limit:
            matches = matches[:limit]
        for entry in matches:
            console.print(_format_log_entry(entry))

    console.print(f"\n[dim]Found {len(matches) if context == 0 else len(results)} matches[/dim]")


@logs_app.command(name="stats")
def logs_stats(
    name: Annotated[str | None, typer.Argument(help="Container name (all if not specified)")] = None,
) -> None:
    """Show log statistics.

    Display statistics about stored logs including counts by level,
    total size, and time range.

    Examples:
        gaol log stats
        gaol log stats mycontainer
    """
    from gaol.logs import get_log_store

    store = get_log_store()

    if name:
        containers = [name]
    else:
        containers = store.list_containers()

    if not containers:
        console.print("[dim]No stored logs found[/dim]")
        return

    table = Table(title="Log Statistics")
    table.add_column("Container", style="cyan")
    table.add_column("Entries", justify="right")
    table.add_column("Size", justify="right")
    table.add_column("Oldest")
    table.add_column("Newest")
    table.add_column("Errors", justify="right", style="red")

    for cname in containers:
        stats = store.get_stats(cname)
        size_str = _format_size(stats.total_size_bytes)
        oldest = stats.oldest_entry.strftime("%Y-%m-%d %H:%M") if stats.oldest_entry else "-"
        newest = stats.newest_entry.strftime("%Y-%m-%d %H:%M") if stats.newest_entry else "-"
        errors = stats.entries_by_level.get("error", 0) + stats.entries_by_level.get("critical", 0)

        table.add_row(
            cname,
            str(stats.total_entries),
            size_str,
            oldest,
            newest,
            str(errors) if errors else "-",
        )

    console.print(table)


@logs_app.command(name="collect")
def logs_collect(
    name: Annotated[str, typer.Argument(help="Container name")],
    tail: Annotated[
        int | None, typer.Option("--tail", "-n", help="Number of recent lines")
    ] = None,
    since: Annotated[
        str | None, typer.Option("--since", "-s", help="Collect logs since time")
    ] = None,
    store_logs: Annotated[
        bool, typer.Option("--store/--no-store", help="Store collected logs")
    ] = True,
) -> None:
    """Collect and store logs from a container.

    Retrieves logs from a container and stores them in the log store
    for later searching and analysis.

    Examples:
        gaol log collect mycontainer
        gaol log collect mycontainer --tail 1000
        gaol log collect mycontainer --since 1h
    """
    from gaol.logs import MultiRuntimeCollector, get_log_store

    collector = MultiRuntimeCollector()
    store = get_log_store()

    console.print(f"Collecting logs from {name}...")

    entries = collector.collect(name, tail=tail, since=since)

    if not entries:
        console.print("[dim]No logs collected[/dim]")
        return

    if store_logs:
        store.append_many(name, entries)
        store.flush(name)
        console.print(f"[green]Stored {len(entries)} log entries[/green]")
    else:
        for entry in entries:
            console.print(_format_log_entry(entry))


@logs_app.command(name="rotate")
def logs_rotate(
    name: Annotated[str | None, typer.Argument(help="Container name (all if not specified)")] = None,
) -> None:
    """Rotate and clean up old logs.

    Applies retention policy to compress old logs and delete
    logs exceeding size or age limits.

    Examples:
        gaol log rotate
        gaol log rotate mycontainer
    """
    from gaol.logs import get_log_store

    store = get_log_store()

    if name:
        console.print(f"Rotating logs for {name}...")
        store.rotate(name)
        console.print(f"[green]Log rotation complete for {name}[/green]")
    else:
        containers = store.list_containers()
        if not containers:
            console.print("[dim]No stored logs found[/dim]")
            return

        for cname in containers:
            console.print(f"Rotating logs for {cname}...")
            store.rotate(cname)

        console.print(f"[green]Log rotation complete for {len(containers)} containers[/green]")


@logs_app.command(name="export")
def logs_export(
    name: Annotated[str, typer.Argument(help="Container name")],
    output: Annotated[str, typer.Argument(help="Output file path")],
    since: Annotated[
        str | None, typer.Option("--since", "-s", help="Export logs since time")
    ] = None,
    until: Annotated[
        str | None, typer.Option("--until", "-u", help="Export logs until time")
    ] = None,
    format: Annotated[
        str, typer.Option("--format", "-f", help="Output format (jsonl or text)")
    ] = "text",
) -> None:
    """Export logs to a file.

    Export stored logs to a file in JSONL or text format.

    Examples:
        gaol log export mycontainer logs.txt
        gaol log export mycontainer logs.jsonl --format jsonl
        gaol log export mycontainer errors.txt --since 1d
    """
    from pathlib import Path

    from gaol.logs import create_query, get_log_store

    store = get_log_store()
    query = create_query(since=since, until=until)

    output_path = Path(output)
    store.export(name, output_path, query=query, format=format)

    console.print(f"[green]Exported logs to {output}[/green]")


@logs_app.command(name="delete")
def logs_delete(
    name: Annotated[str, typer.Argument(help="Container name")],
    force: Annotated[
        bool, typer.Option("--force", "-f", help="Skip confirmation")
    ] = False,
) -> None:
    """Delete all stored logs for a container.

    Permanently removes all stored log entries for a container.

    Examples:
        gaol log delete mycontainer
        gaol log delete mycontainer --force
    """
    from gaol.logs import get_log_store

    if not force:
        confirm = typer.confirm(f"Delete all logs for '{name}'?")
        if not confirm:
            console.print("[yellow]Cancelled[/yellow]")
            raise typer.Exit(0)

    store = get_log_store()
    store.delete(name)
    console.print(f"[green]Deleted logs for {name}[/green]")


@logs_app.command(name="summary")
def logs_summary(
    name: Annotated[str | None, typer.Argument(help="Container name (all if not specified)")] = None,
    since: Annotated[
        str | None, typer.Option("--since", "-s", help="Analyze logs since time")
    ] = "1h",
) -> None:
    """Show error summary for logs.

    Analyze logs and show a summary of errors and warnings,
    including top recurring patterns.

    Examples:
        gaol log summary
        gaol log summary mycontainer --since 24h
    """
    from gaol.logs import LogSearcher, create_query, get_log_store

    store = get_log_store()
    query = create_query(since=since)

    if name:
        entries = store.query(name, query)
    else:
        entries = store.query_all(query)

    if not entries:
        console.print("[dim]No log entries found[/dim]")
        return

    searcher = LogSearcher(entries)
    summary = searcher.get_error_summary()

    console.print(f"\n[bold]Log Summary[/bold] (since {since})")
    console.print(f"  Total entries: {len(entries)}")
    console.print(f"  [red]Errors: {summary['total_errors']}[/red]")
    console.print(f"  [yellow]Warnings: {summary['total_warnings']}[/yellow]")

    if summary["errors_by_container"]:
        console.print("\n[bold]Errors by Container:[/bold]")
        for cname, count in sorted(summary["errors_by_container"].items(), key=lambda x: -x[1]):
            console.print(f"  {cname}: {count}")

    if summary["top_error_patterns"]:
        console.print("\n[bold]Top Error Patterns:[/bold]")
        for pattern, count in summary["top_error_patterns"]:
            console.print(f"  [{count}x] {pattern[:80]}...")

    if summary["top_warning_patterns"]:
        console.print("\n[bold]Top Warning Patterns:[/bold]")
        for pattern, count in summary["top_warning_patterns"]:
            console.print(f"  [{count}x] {pattern[:80]}...")


def _format_log_entry(entry) -> str:
    """Format a log entry for display."""
    ts = entry.timestamp.strftime("%Y-%m-%d %H:%M:%S")
    level = entry.level.value.upper()

    level_colors = {
        "DEBUG": "dim",
        "INFO": "blue",
        "WARNING": "yellow",
        "ERROR": "red",
        "CRITICAL": "red bold",
        "UNKNOWN": "dim",
    }
    color = level_colors.get(level, "dim")

    return f"[dim]{ts}[/dim] [{color}]{level:8}[/{color}] [cyan]{entry.container_name}[/cyan] {entry.message}"


def _format_size(bytes_val: int) -> str:
    """Format bytes as human-readable size."""
    for unit in ['B', 'KB', 'MB', 'GB']:
        if bytes_val < 1024:
            return f"{bytes_val:.1f} {unit}"
        bytes_val /= 1024
    return f"{bytes_val:.1f} TB"


# =============================================================================
# Desktop commands
# =============================================================================


@desktop_app.command(name="create")
def desktop_create(
    container: Annotated[str, typer.Argument(help="Container name")],
    exec_command: Annotated[str, typer.Argument(help="Command to execute in container")],
    name: Annotated[str | None, typer.Option("--name", "-n", help="Display name")] = None,
    icon: Annotated[str | None, typer.Option("--icon", "-i", help="Icon name or path")] = None,
    comment: Annotated[str | None, typer.Option("--comment", "-c", help="Description/tooltip")] = None,
    category: Annotated[list[str] | None, typer.Option("--category", help="Menu category")] = None,
    terminal: Annotated[bool, typer.Option("--terminal", "-t", help="Run in terminal")] = False,
    no_start: Annotated[bool, typer.Option("--no-start", help="Don't auto-start container")] = False,
    no_install: Annotated[bool, typer.Option("--no-install", help="Generate but don't install")] = False,
    overwrite: Annotated[bool, typer.Option("--overwrite", help="Overwrite existing entry")] = False,
) -> None:
    """Create and install a desktop entry for a containerized app.

    Creates a .desktop file that launches the specified command inside
    the container, with optional auto-start and menu integration.

    Examples:
        gaol desktop create my-browser /usr/bin/firefox --name "Firefox"
        gaol desktop create my-browser /usr/bin/firefox --icon firefox --category Network
        gaol desktop create dev-env /usr/bin/code --terminal
    """
    from gaol.desktop import (
        DesktopCategory,
        DesktopEntry,
        DesktopEntryExistsError,
        DesktopEntryGenerator,
        DesktopInstaller,
    )

    # Verify container exists
    store = get_container_store()
    stored = store.get(container)
    if not stored:
        err_console.print(f"[red]Error:[/red] Container '{container}' not found")
        raise typer.Exit(1)

    # Parse categories
    categories = []
    if category:
        for cat in category:
            try:
                categories.append(DesktopCategory(cat))
            except ValueError:
                valid = ", ".join(c.value for c in DesktopCategory)
                err_console.print(f"[red]Invalid category:[/red] {cat}")
                err_console.print(f"[dim]Valid categories: {valid}[/dim]")
                raise typer.Exit(1) from None

    # Create entry
    entry = DesktopEntry(
        name=name or container,
        container_name=container,
        exec_command=exec_command,
        icon=icon,
        comment=comment,
        categories=categories,
        terminal=terminal,
        start_container=not no_start,
    )

    generator = DesktopEntryGenerator()
    content = generator.generate(entry)

    if no_install:
        # Just print the generated content
        console.print(content)
        return

    # Install the entry
    installer = DesktopInstaller()
    try:
        installed = installer.install(entry, overwrite=overwrite)
        console.print(f"[green]Created desktop entry:[/green] {entry.name}")
        console.print(f"  File: {installed.desktop_file}")
        console.print(f"  Container: {container}")
        console.print(f"  Command: {exec_command}")
    except DesktopEntryExistsError:
        err_console.print(f"[red]Error:[/red] Desktop entry already exists")
        err_console.print("[dim]Use --overwrite to replace it[/dim]")
        raise typer.Exit(1) from None


@desktop_app.command(name="list")
def desktop_list(
    container: Annotated[str | None, typer.Argument(help="Filter by container")] = None,
) -> None:
    """List installed desktop entries.

    Shows all desktop entries created by gaol, optionally
    filtered to a specific container.

    Examples:
        gaol desktop list
        gaol desktop list my-browser
    """
    from gaol.desktop import DesktopInstaller

    installer = DesktopInstaller()
    entries = installer.list_installed(container)

    if not entries:
        if container:
            console.print(f"[dim]No desktop entries for container '{container}'[/dim]")
        else:
            console.print("[dim]No desktop entries installed[/dim]")
        return

    table = Table(title="Desktop Entries")
    table.add_column("Name", style="cyan")
    table.add_column("Container", style="green")
    table.add_column("Command", style="dim")
    table.add_column("File", style="dim")

    for entry in entries:
        # Truncate command for display
        cmd = entry.exec_command or ""
        if len(cmd) > 30:
            cmd = cmd[:27] + "..."

        table.add_row(
            entry.entry_name,
            entry.container_name,
            cmd,
            entry.desktop_file,
        )

    console.print(table)


@desktop_app.command(name="show")
def desktop_show(
    container: Annotated[str, typer.Argument(help="Container name")],
    name: Annotated[str | None, typer.Option("--name", "-n", help="Entry name")] = None,
) -> None:
    """Show content of an installed .desktop file.

    Displays the raw content of the .desktop file for inspection
    or debugging.

    Examples:
        gaol desktop show my-browser
        gaol desktop show my-browser --name Firefox
    """
    from gaol.desktop import DesktopInstaller

    installer = DesktopInstaller()
    content = installer.get_entry_content(container, name)

    if not content:
        err_console.print(f"[red]Error:[/red] No desktop entry found for '{container}'")
        raise typer.Exit(1)

    console.print(content)


@desktop_app.command(name="uninstall")
def desktop_uninstall(
    container: Annotated[str, typer.Argument(help="Container name")],
    name: Annotated[str | None, typer.Option("--name", "-n", help="Entry name")] = None,
    all_entries: Annotated[bool, typer.Option("--all", "-a", help="Remove all entries")] = False,
) -> None:
    """Uninstall desktop entries for a container.

    Removes the .desktop file and any extracted icons from
    the XDG directories.

    Examples:
        gaol desktop uninstall my-browser --all
        gaol desktop uninstall my-browser --name Firefox
    """
    from gaol.desktop import DesktopEntryNotFoundError, DesktopInstaller

    if not name and not all_entries:
        err_console.print("[red]Error:[/red] Specify --name or --all")
        raise typer.Exit(1)

    installer = DesktopInstaller()

    try:
        if all_entries:
            removed = installer.uninstall(container)
        else:
            removed = installer.uninstall(container, name)

        console.print(f"[green]Removed {removed} desktop entry(ies)[/green]")
    except DesktopEntryNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@desktop_app.command(name="run")
def desktop_run(
    container: Annotated[str, typer.Argument(help="Container name")],
    command: Annotated[list[str], typer.Argument(help="Command and arguments")],
    start: Annotated[bool, typer.Option("--start", "-s", help="Start container if not running")] = True,
    workdir: Annotated[str | None, typer.Option("--workdir", "-w", help="Working directory")] = None,
) -> None:
    """Run a GUI command in a container.

    This is the entry point called by .desktop Exec= lines.
    Handles starting the container if needed and setting up
    the display environment.

    Examples:
        gaol desktop run my-browser -- /usr/bin/firefox
        gaol desktop run --start my-container -- /usr/bin/xeyes
    """
    import os
    import subprocess

    # Verify container exists
    store = get_container_store()
    stored = store.get(container)
    if not stored:
        err_console.print(f"[red]Error:[/red] Container '{container}' not found")
        raise typer.Exit(1)

    runtime = get_runtime(stored.runtime)

    # Check if container is running
    try:
        state = runtime.get_state(container)
        is_running = state.status == ContainerStatus.RUNNING
    except Exception:
        is_running = False

    # Start container if needed
    if not is_running and start:
        try:
            runtime.start(container)
            store.update_started(container)
        except RuntimeError as e:
            err_console.print(f"[red]Error starting container:[/red] {e}")
            raise typer.Exit(1) from e
    elif not is_running:
        err_console.print(f"[red]Error:[/red] Container '{container}' is not running")
        err_console.print("[dim]Use --start to auto-start[/dim]")
        raise typer.Exit(1)

    # Set up environment for GUI forwarding
    env = os.environ.copy()

    # Pass through display environment
    for var in ["DISPLAY", "WAYLAND_DISPLAY", "XDG_RUNTIME_DIR", "XAUTHORITY"]:
        if var in os.environ:
            env[var] = os.environ[var]

    # Execute command in container
    try:
        exit_code, stdout, stderr = runtime.exec(
            container,
            list(command),
            workdir=workdir,
            environment={
                "DISPLAY": os.environ.get("DISPLAY", ":0"),
            },
        )
        if stdout:
            console.print(stdout, end="")
        if stderr:
            err_console.print(stderr, end="")
        raise typer.Exit(exit_code)
    except RuntimeError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from e


@desktop_app.command(name="extract-icon")
def desktop_extract_icon(
    container: Annotated[str, typer.Argument(help="Container name")],
    icon_path: Annotated[str, typer.Argument(help="Path to icon inside container")],
    output: Annotated[str | None, typer.Option("--output", "-o", help="Output path")] = None,
) -> None:
    """Extract an icon from a container.

    Copies an icon file from inside the container to the local
    icons directory for use in desktop entries.

    Examples:
        gaol desktop extract-icon my-container /usr/share/icons/app.png
        gaol desktop extract-icon my-container /app/icon.svg -o ./icon.svg
    """
    from pathlib import Path

    from gaol.desktop import DesktopInstaller, IconExtractionError

    # Verify container exists
    store = get_container_store()
    stored = store.get(container)
    if not stored:
        err_console.print(f"[red]Error:[/red] Container '{container}' not found")
        raise typer.Exit(1)

    runtime = get_runtime(stored.runtime)
    installer = DesktopInstaller()

    try:
        dest = installer.extract_icon(
            container,
            icon_path,
            stored.container_id,
            runtime,
            output=Path(output) if output else None,
        )
        console.print(f"[green]Extracted icon to:[/green] {dest}")
    except IconExtractionError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


# =============================================================================
# Brew commands
# =============================================================================


@brew_app.command(name="install")
def brew_install(
    package: Annotated[str, typer.Argument(help="Homebrew formula to install")],
    no_wrapper: Annotated[
        bool, typer.Option("--no-wrapper", help="Don't create wrapper script")
    ] = False,
) -> None:
    """Install a Homebrew package in a container.

    Creates wrapper scripts in ~/.local/bin/ for transparent execution.
    The first install will create the Homebrew container if it doesn't exist.

    Examples:
        gaol brew install ffmpeg
        gaol brew install ripgrep --no-wrapper
    """
    from gaol.brew import BrewContainer, BrewContainerError, WrapperInstaller

    container = BrewContainer()

    # Check if container needs to be created
    if not container.is_container_ready():
        console.print("[cyan]Creating Homebrew container (this may take a few minutes)...[/cyan]")

    console.print(f"[cyan]Installing {package}...[/cyan]")

    try:
        exit_code, stdout, stderr = container.install_package(package)
    except BrewContainerError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None

    if exit_code != 0:
        err_console.print(f"[red]Installation failed:[/red]")
        if stderr:
            err_console.print(stderr)
        raise typer.Exit(1)

    if stdout:
        console.print(stdout)

    if not no_wrapper:
        try:
            binaries = container.get_package_binaries(package)
            installer = WrapperInstaller()
            installed = installer.install(package, binaries, overwrite=True)

            console.print(f"\n[green]Installed wrappers:[/green]")
            for entry in installed:
                console.print(f"  {entry.wrapper_path}")

            wrapper_dir = installer.get_wrapper_dir()
            console.print(f"\n[dim]Ensure {wrapper_dir} is in your PATH[/dim]")
        except Exception as e:
            err_console.print(f"[yellow]Warning: Could not create wrappers:[/yellow] {e}")

    console.print(f"\n[green]Successfully installed {package}[/green]")


@brew_app.command(name="run")
def brew_run(
    binary: Annotated[str, typer.Argument(help="Binary to run")],
    args: Annotated[list[str], typer.Argument(help="Arguments to pass")] = None,
) -> None:
    """Run a Homebrew binary in the container.

    This is called by wrapper scripts. Usually not invoked directly.

    Examples:
        gaol brew run ffmpeg -- -i input.mp4 output.webm
        gaol brew run rg -- -n "pattern" .
    """
    import os

    from gaol.brew import BrewContainer, BrewContainerError

    container = BrewContainer()
    workdir = os.getcwd()

    # Build command with binary
    command = [binary] + list(args or [])

    try:
        exit_code, stdout, stderr = container.run_command(command, workdir=workdir)
    except BrewContainerError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None

    if stdout:
        console.print(stdout, end="")
    if stderr:
        err_console.print(stderr, end="")

    raise typer.Exit(exit_code)


@brew_app.command(name="list")
def brew_list(
    all_packages: Annotated[
        bool, typer.Option("--all", "-a", help="Show all Homebrew packages")
    ] = False,
) -> None:
    """List installed packages with wrappers.

    By default shows packages with wrapper scripts.
    Use --all to show all packages in the Homebrew container.

    Examples:
        gaol brew list
        gaol brew list --all
    """
    from gaol.brew import BrewContainer, WrapperInstaller

    if all_packages:
        container = BrewContainer()

        if not container.is_container_ready():
            console.print("[dim]No Homebrew container found[/dim]")
            return

        packages = container.list_installed()

        if not packages:
            console.print("[dim]No Homebrew packages installed[/dim]")
            return

        console.print("[bold]Homebrew packages:[/bold]")
        for pkg in packages:
            console.print(f"  {pkg}")
    else:
        installer = WrapperInstaller()
        entries = installer.list_installed()

        if not entries:
            console.print("[dim]No wrapper scripts installed[/dim]")
            return

        table = Table(title="Installed Wrappers")
        table.add_column("Package", style="cyan")
        table.add_column("Binaries", style="green")
        table.add_column("Wrapper Dir", style="dim")

        for entry in entries:
            binaries = ", ".join(entry.binaries[:3])
            if len(entry.binaries) > 3:
                binaries += f" (+{len(entry.binaries) - 3} more)"
            table.add_row(
                entry.package_name,
                binaries,
                str(Path(entry.wrapper_path).parent),
            )

        console.print(table)


@brew_app.command(name="uninstall")
def brew_uninstall(
    package: Annotated[str, typer.Argument(help="Package to uninstall")],
    keep_wrapper: Annotated[
        bool, typer.Option("--keep-wrapper", help="Keep wrapper script")
    ] = False,
) -> None:
    """Uninstall a Homebrew package.

    Removes the package from the container and the wrapper script.

    Examples:
        gaol brew uninstall ffmpeg
        gaol brew uninstall ripgrep --keep-wrapper
    """
    from gaol.brew import BrewContainer, BrewContainerError, WrapperInstaller, WrapperNotFoundError

    container = BrewContainer()

    if not container.is_container_ready():
        err_console.print("[red]Error:[/red] Homebrew container not found")
        raise typer.Exit(1)

    console.print(f"[cyan]Uninstalling {package}...[/cyan]")

    try:
        exit_code, stdout, stderr = container.uninstall_package(package)
    except BrewContainerError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None

    if exit_code != 0:
        err_console.print(f"[red]Uninstall failed:[/red]")
        if stderr:
            err_console.print(stderr)
        raise typer.Exit(1)

    if not keep_wrapper:
        installer = WrapperInstaller()
        try:
            removed = installer.uninstall(package)
            console.print(f"[green]Removed {removed} wrapper(s)[/green]")
        except WrapperNotFoundError:
            pass  # No wrappers to remove

    console.print(f"[green]Uninstalled {package}[/green]")


@brew_app.command(name="search")
def brew_search(
    query: Annotated[str, typer.Argument(help="Search query")],
) -> None:
    """Search for Homebrew packages.

    Examples:
        gaol brew search video
        gaol brew search ffmpeg
    """
    from gaol.brew import BrewContainer, BrewContainerError

    container = BrewContainer()

    try:
        exit_code, stdout, stderr = container.search(query)
    except BrewContainerError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None

    if stdout:
        console.print(stdout)
    if stderr:
        err_console.print(stderr)

    raise typer.Exit(exit_code)


@brew_app.command(name="info")
def brew_info(
    package: Annotated[str, typer.Argument(help="Package name")],
) -> None:
    """Show information about a Homebrew package.

    Examples:
        gaol brew info ffmpeg
        gaol brew info ripgrep
    """
    from gaol.brew import BrewContainer, BrewContainerError

    container = BrewContainer()

    try:
        exit_code, stdout, stderr = container.info(package)
    except BrewContainerError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None

    if stdout:
        console.print(stdout)
    if stderr:
        err_console.print(stderr)

    raise typer.Exit(exit_code)


# =============================================================================
# Tunnel commands
# =============================================================================


@tunnel_app.command(name="start")
def tunnel_start(
    container: Annotated[str, typer.Argument(help="Container name")],
    port: Annotated[int, typer.Argument(help="Container port to expose")],
    host_port: Annotated[
        int | None,
        typer.Option("--host-port", "-p", help="Host port (auto-detected if not specified)"),
    ] = None,
) -> None:
    """Start a tunnel to expose a container port to the internet.

    Creates a public URL using cloudflared quick tunnels.
    No Cloudflare account needed - provides a random subdomain.

    Examples:
        gaol tunnel start myapp 8080
        gaol tunnel start myapp 3000 --host-port 3001
    """
    from gaol.tunnel import (
        CloudflaredNotFoundError,
        TunnelAlreadyExistsError,
        TunnelManager,
        TunnelStartError,
    )

    manager = TunnelManager()

    console.print(f"[cyan]Starting tunnel for {container}:{port}...[/cyan]")

    try:
        tunnel = manager.start(container, port, host_port)
    except CloudflaredNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None
    except TunnelAlreadyExistsError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None
    except TunnelStartError as e:
        err_console.print(f"[red]Failed to start tunnel:[/red] {e}")
        raise typer.Exit(1) from None

    console.print("[green]Tunnel started![/green]")
    console.print(f"  Public URL: [cyan]{tunnel.public_url}[/cyan]")
    console.print(f"  Container:  {container}:{port}")
    console.print(f"  Host port:  {tunnel.host_port}")
    console.print(f"  Tunnel ID:  {tunnel.id}")
    console.print(f"\n[dim]Stop with: gaol tunnel stop {tunnel.id}[/dim]")


@tunnel_app.command(name="stop")
def tunnel_stop(
    tunnel_id: Annotated[str, typer.Argument(help="Tunnel ID to stop")],
) -> None:
    """Stop a running tunnel.

    Examples:
        gaol tunnel stop a1b2c3d4
    """
    from gaol.tunnel import TunnelManager, TunnelNotFoundError

    manager = TunnelManager()

    try:
        manager.stop(tunnel_id)
    except TunnelNotFoundError:
        err_console.print(f"[red]Tunnel not found:[/red] {tunnel_id}")
        raise typer.Exit(1) from None

    console.print("[green]Tunnel stopped[/green]")


@tunnel_app.command(name="list")
def tunnel_list(
    container: Annotated[
        str | None,
        typer.Option("--container", "-c", help="Filter by container name"),
    ] = None,
) -> None:
    """List active tunnels.

    Examples:
        gaol tunnel list
        gaol tunnel list --container myapp
    """
    from gaol.tunnel import TunnelManager

    manager = TunnelManager()
    tunnels = manager.list(container)

    if not tunnels:
        console.print("[dim]No active tunnels[/dim]")
        return

    table = Table(title="Active Tunnels")
    table.add_column("ID", style="cyan")
    table.add_column("Container")
    table.add_column("Port")
    table.add_column("Public URL", style="green")
    table.add_column("Started")

    for t in tunnels:
        table.add_row(
            t.id,
            t.container_name,
            f"{t.container_port} -> {t.host_port}",
            t.public_url,
            t.started_at.strftime("%H:%M:%S"),
        )

    console.print(table)


@tunnel_app.command(name="stop-all")
def tunnel_stop_all(
    container: Annotated[
        str | None,
        typer.Option("--container", "-c", help="Only stop tunnels for this container"),
    ] = None,
) -> None:
    """Stop all running tunnels.

    Examples:
        gaol tunnel stop-all
        gaol tunnel stop-all --container myapp
    """
    from gaol.tunnel import TunnelManager

    manager = TunnelManager()
    count = manager.stop_all(container)

    if count == 0:
        console.print("[dim]No tunnels to stop[/dim]")
    else:
        console.print(f"[green]Stopped {count} tunnel(s)[/green]")


# =============================================================================
# Migrate command
# =============================================================================


@app.command(rich_help_panel=PANEL_INFRASTRUCTURE)
def migrate(
    name: Annotated[str, typer.Argument(help="Container name to migrate")],
    target: Annotated[
        str, typer.Option("--to", "-t", help="Target runtime to migrate to")
    ],
    dry_run: Annotated[
        bool, typer.Option("--dry-run", "-n", help="Show plan without executing")
    ] = False,
    preserve_source: Annotated[
        bool, typer.Option("--preserve-source/--no-preserve-source", help="Keep source container after migration")
    ] = True,
    force: Annotated[
        bool, typer.Option("--force", "-f", help="Proceed despite warnings")
    ] = False,
    verbose: Annotated[
        bool, typer.Option("--verbose", "-v", help="Show detailed progress")
    ] = False,
) -> None:
    """Migrate a container to a different runtime.

    Exports the container's filesystem and recreates it on the target runtime,
    preserving all files and configuration where possible.

    Examples:
        gaol migrate mycontainer --to nspawn
        gaol migrate mycontainer --to libvirt --dry-run
        gaol migrate mycontainer --to docker --no-preserve-source
        gaol migrate mycontainer --to nspawn --force -v
    """
    from gaol.migration import (
        FeatureWarningSeverity,
        MigrationError,
        MigrationStatus,
        MigrationValidationError,
        get_migrator,
    )

    migrator = get_migrator()

    # Create migration plan
    try:
        plan = migrator.create_plan(name, target, preserve_source=preserve_source)
    except MigrationValidationError as e:
        err_console.print(f"[red]Migration validation failed:[/red] {e}")
        raise typer.Exit(1) from None

    # Set dry run flag
    plan.dry_run = dry_run

    # Display plan
    console.print(f"\n[bold cyan]Migration Plan[/bold cyan]")
    console.print(f"  Container: {plan.container_name}")
    console.print(f"  Source runtime: {plan.source_runtime}")
    console.print(f"  Target runtime: {plan.target_runtime}")
    console.print(f"  Preserve source: {'yes' if plan.preserve_source else 'no'}")

    if plan.estimated_size_bytes:
        size_mb = plan.estimated_size_bytes / (1024 * 1024)
        console.print(f"  Estimated size: {size_mb:.1f} MB")

    # Display warnings
    if plan.feature_warnings:
        console.print("\n[bold yellow]Warnings:[/bold yellow]")
        has_critical = False
        for warning in plan.feature_warnings:
            if warning.severity == FeatureWarningSeverity.CRITICAL:
                prefix = "[red]CRITICAL[/red]"
                has_critical = True
            elif warning.severity == FeatureWarningSeverity.WARNING:
                prefix = "[yellow]WARNING[/yellow]"
            else:
                prefix = "[blue]INFO[/blue]"

            console.print(f"  {prefix}: {warning.feature}")
            if warning.source_value:
                console.print(f"    Source value: {warning.source_value}")
            console.print(f"    Target behavior: {warning.target_behavior}")

        if has_critical and not force:
            err_console.print("\n[red]Critical warnings found. Use --force to proceed anyway.[/red]")
            raise typer.Exit(1)

    if dry_run:
        console.print("\n[yellow]Dry run - no changes made[/yellow]")
        return

    # Confirm unless force
    if not force and plan.feature_warnings:
        confirm = typer.confirm("\nProceed with migration?")
        if not confirm:
            console.print("[yellow]Migration cancelled[/yellow]")
            raise typer.Exit(0)

    # Progress callback for verbose mode
    def progress_callback(status: MigrationStatus, message: str) -> None:
        if verbose:
            console.print(f"  [{status.value}] {message}")

    # Execute migration
    console.print("\n[bold]Executing migration...[/bold]")

    try:
        state = migrator.execute(plan, progress_callback if verbose else None)
    except MigrationError as e:
        err_console.print(f"\n[red]Migration failed:[/red] {e}")

        # Show rollback actions if any
        if hasattr(e, 'partial_state') and e.partial_state:
            if e.partial_state.rollback_actions:
                console.print("\n[yellow]Rollback actions taken:[/yellow]")
                for action in e.partial_state.rollback_actions:
                    console.print(f"  - {action}")

        raise typer.Exit(1) from None

    # Success!
    if state.status == MigrationStatus.COMPLETED:
        console.print(f"\n[green]Migration completed successfully![/green]")
        console.print(f"  Container '{name}' is now using runtime: {target}")

        if state.target_container_id:
            console.print(f"  New container ID: {state.target_container_id}")

        if not plan.preserve_source:
            console.print(f"  Source container removed")
        else:
            console.print(f"  [dim]Source container preserved (you may want to remove it manually)[/dim]")
    elif state.status == MigrationStatus.ROLLED_BACK:
        console.print(f"\n[yellow]Migration was rolled back due to errors[/yellow]")
        if state.error:
            console.print(f"  Error: {state.error}")
    else:
        console.print(f"\n[red]Migration ended in unexpected state: {state.status.value}[/red]")
        if state.error:
            console.print(f"  Error: {state.error}")
        raise typer.Exit(1)


# =============================================================================
# Shell integration commands
# =============================================================================


@shell_int_app.command(name="completions")
def shell_completions(
    shell: Annotated[str, typer.Argument(help="Shell type: bash, zsh, fish")],
    install: Annotated[
        bool, typer.Option("--install", "-i", help="Install to standard location")
    ] = False,
) -> None:
    """Generate or install shell completions.

    Generates shell completion scripts for tab-completion of commands.

    Examples:
        gaol shell-integration completions bash
        gaol shell-integration completions zsh --install
        gaol shell-integration completions fish --install
    """
    from gaol.shell import CompletionGenerator, get_supported_shells

    shells = get_supported_shells()
    if shell.lower() not in shells:
        err_console.print(f"[red]Error:[/red] Unsupported shell: {shell}")
        err_console.print(f"Supported shells: {', '.join(shells)}")
        raise typer.Exit(1)

    gen = CompletionGenerator()

    if install:
        path = gen.install(shell)
        console.print(f"[green]Completions installed to:[/green] {path}")
        console.print("\n[dim]Restart your shell or source the file to enable completions[/dim]")
    else:
        script = gen.generate(shell)
        console.print(script)


@shell_int_app.command(name="prompt")
def shell_prompt(
    shell: Annotated[str, typer.Argument(help="Shell type: bash, zsh, fish")],
) -> None:
    """Generate prompt integration script.

    Generates shell functions for container context management:
    - gaol_prompt() - Returns container context for PS1
    - muse <container> - Set current container
    - munuse - Clear container context
    - msh - Shell into current container
    - mstatus - Show container status
    - mexec - Execute command in container

    Examples:
        gaol shell-integration prompt bash >> ~/.bashrc
        gaol shell-integration prompt zsh >> ~/.zshrc
        gaol shell-integration prompt fish >> ~/.config/fish/config.fish
    """
    from gaol.shell import PromptIntegration, get_supported_shells

    shells = get_supported_shells()
    if shell.lower() not in shells:
        err_console.print(f"[red]Error:[/red] Unsupported shell: {shell}")
        err_console.print(f"Supported shells: {', '.join(shells)}")
        raise typer.Exit(1)

    prompt = PromptIntegration()
    script = prompt.generate(shell)
    console.print(script)


@shell_int_app.command(name="init")
def shell_init(
    shell: Annotated[str, typer.Argument(help="Shell type: bash, zsh, fish")],
) -> None:
    """Generate combined initialization script.

    Outputs both completions and prompt integration for easy sourcing.

    Usage (add to shell rc file):
        Bash:  eval "$(gaol shell-integration init bash)"
        Zsh:   eval "$(gaol shell-integration init zsh)"
        Fish:  gaol shell-integration init fish | source

    Examples:
        gaol shell-integration init bash
        gaol shell-integration init zsh
        gaol shell-integration init fish
    """
    from gaol.shell import CompletionGenerator, PromptIntegration, get_supported_shells

    shells = get_supported_shells()
    if shell.lower() not in shells:
        err_console.print(f"[red]Error:[/red] Unsupported shell: {shell}")
        err_console.print(f"Supported shells: {', '.join(shells)}")
        raise typer.Exit(1)

    gen = CompletionGenerator()
    prompt = PromptIntegration()

    # Output combined script
    console.print(gen.generate(shell))
    console.print("\n")
    console.print(prompt.generate(shell))


@shell_int_app.command(name="context")
def shell_context(
    container: Annotated[
        str | None, typer.Argument(help="Container name to set as context")
    ] = None,
    clear: Annotated[
        bool, typer.Option("--clear", "-c", help="Clear current context")
    ] = False,
    short: Annotated[
        bool, typer.Option("--short", "-s", help="Output short status only")
    ] = False,
    format: Annotated[
        str, typer.Option("--format", "-f", help="Output format: text, short")
    ] = "text",
) -> None:
    """Show or query container context.

    When called without arguments, shows the current container context.
    With a container name, shows info about that container.
    Use --short or --format short for prompt-friendly output.

    Examples:
        gaol shell-integration context
        gaol shell-integration context mycontainer
        gaol shell-integration context mycontainer --format short
    """
    from gaol.shell import ContainerContext

    # If --clear, just print instructions
    if clear:
        console.print("To clear context, run: [cyan]munuse[/cyan]")
        console.print("Or: [cyan]unset GAOL_CONTAINER[/cyan]")
        return

    # Determine which container to query
    target = container or ContainerContext.get_current()

    if not target:
        console.print("[dim]No container context set[/dim]")
        console.print("Set with: [cyan]muse <container>[/cyan]")
        console.print("Or: [cyan]export GAOL_CONTAINER=<container>[/cyan]")
        return

    # Short format for prompts
    if short or format == "short":
        status = ContainerContext.get_status_short(target)
        console.print(status)
        return

    # Full info
    info = ContainerContext.get_context_info(target)
    current = ContainerContext.get_current()

    if current == target:
        console.print(f"[green]Current context:[/green] {target}")
    else:
        console.print(f"[cyan]Container:[/cyan] {target}")

    console.print(f"  Runtime: {info['runtime'] or '[dim]unknown[/dim]'}")
    console.print(f"  Status:  {info['status'] or '[dim]unknown[/dim]'}")
    if info['image']:
        console.print(f"  Image:   {info['image']}")
    if info['ip_address']:
        console.print(f"  IP:      {info['ip_address']}")


# ============================================================================
# GPU commands
# ============================================================================


@gpu_app.command(name="list")
def gpu_list(
    vendor: Annotated[
        str | None, typer.Option("--vendor", "-v", help="Filter by vendor (nvidia, amd, intel)")
    ] = None,
    json_output: Annotated[
        bool, typer.Option("--json", "-j", help="Output as JSON")
    ] = False,
) -> None:
    """List available GPUs on the system.

    Detects NVIDIA, AMD, and Intel GPUs and shows their details.

    Examples:
        gaol gpu list
        gaol gpu list --vendor nvidia
        gaol gpu list --json
    """
    from gaol.gpu import GPUDetector, GPUVendor

    gpus = GPUDetector.detect_all()

    # Filter by vendor if specified
    if vendor:
        try:
            filter_vendor = GPUVendor(vendor.lower())
            gpus = [g for g in gpus if g.vendor == filter_vendor]
        except ValueError:
            err_console.print(f"[red]Unknown vendor:[/red] {vendor}")
            err_console.print("Valid vendors: nvidia, amd, intel")
            raise typer.Exit(1) from None

    if json_output:
        import json

        console.print(json.dumps([g.model_dump() for g in gpus], indent=2, default=str))
        return

    if not gpus:
        console.print("[dim]No GPUs detected[/dim]")
        return

    table = Table(title="Available GPUs")
    table.add_column("ID", style="cyan")
    table.add_column("Vendor", style="green")
    table.add_column("Name")
    table.add_column("Memory")
    table.add_column("PCI Address", style="dim")

    for gpu in gpus:
        memory_str = f"{gpu.memory_mb} MB" if gpu.memory_mb else "-"
        table.add_row(
            str(gpu.index),
            gpu.vendor.value,
            gpu.name,
            memory_str,
            gpu.pci_bus_id or "-",
        )

    console.print(table)


@gpu_app.command(name="info")
def gpu_info(
    device_id: Annotated[int, typer.Argument(help="GPU device ID")],
) -> None:
    """Show detailed information about a specific GPU.

    Examples:
        gaol gpu info 0
        gaol gpu info 1
    """
    from gaol.gpu import GPUDetector

    gpus = GPUDetector.detect_all()
    gpu = next((g for g in gpus if g.index == device_id), None)

    if not gpu:
        err_console.print(f"[red]GPU not found:[/red] device {device_id}")
        raise typer.Exit(1)

    console.print(f"[bold]GPU {gpu.index}[/bold]")
    console.print(f"  Name:        {gpu.name}")
    console.print(f"  Vendor:      {gpu.vendor.value}")
    if gpu.uuid:
        console.print(f"  UUID:        {gpu.uuid}")
    if gpu.memory_mb:
        console.print(f"  Memory:      {gpu.memory_mb} MB")
    if gpu.pci_bus_id:
        console.print(f"  PCI Address: {gpu.pci_bus_id}")


@gpu_app.command(name="check")
def gpu_check() -> None:
    """Check GPU support and driver installation.

    Verifies that GPU drivers and container runtime support are properly installed.

    Examples:
        gaol gpu check
    """
    from gaol.gpu import GPUDetector

    support = GPUDetector.get_support_info()

    console.print("[bold]GPU Support Status[/bold]\n")

    # NVIDIA
    console.print("[cyan]NVIDIA:[/cyan]")
    if support.nvidia_available:
        console.print(f"  Driver:           [green]installed[/green]")
        if support.nvidia_driver_version:
            console.print(f"  Driver Version:   {support.nvidia_driver_version}")
        if support.nvidia_runtime_available:
            console.print(f"  Container Runtime:[green] installed[/green]")
        else:
            console.print(f"  Container Runtime:[yellow] not installed[/yellow]")
            console.print("                    Install nvidia-container-toolkit for Docker support")
    else:
        console.print(f"  Driver:           [dim]not detected[/dim]")

    console.print()

    # AMD
    console.print("[cyan]AMD (ROCm):[/cyan]")
    if support.rocm_available:
        console.print(f"  ROCm:             [green]installed[/green]")
    else:
        console.print(f"  ROCm:             [dim]not installed[/dim]")

    console.print()

    # Intel
    console.print("[cyan]Intel:[/cyan]")
    if support.intel_available:
        console.print(f"  DRI:              [green]available[/green]")
    else:
        console.print(f"  DRI:              [dim]not available[/dim]")

    console.print()

    # Summary
    gpus = GPUDetector.detect_all()
    if gpus:
        console.print(f"[green]Found {len(gpus)} GPU(s)[/green]")
        console.print("Run 'gaol gpu list' to see details")
    else:
        console.print("[yellow]No GPUs detected[/yellow]")


# =============================================================================
# Volume Commands
# =============================================================================


@volume_app.command(name="create")
def volume_create(
    name: Annotated[str, typer.Argument(help="Volume name")],
    size: Annotated[int, typer.Option("--size", "-s", help="Size in MB")] = 1024,
    backend: Annotated[
        str, typer.Option("--type", "-t", help="Encryption type (luks or gocryptfs)")
    ] = "luks",
    filesystem: Annotated[str, typer.Option("--fs", help="Filesystem (for LUKS)")] = "ext4",
    description: Annotated[str | None, typer.Option("--desc", "-d", help="Description")] = None,
) -> None:
    """Create a new encrypted volume.

    Examples:
        gaol volume create mydata --size 2048
        gaol volume create secrets --type gocryptfs
    """
    from gaol.storage import VolumeConfig, get_volume_manager
    from gaol.storage.models import EncryptionBackend

    backend_type = EncryptionBackend.LUKS if backend == "luks" else EncryptionBackend.GOCRYPTFS

    config = VolumeConfig(
        name=name,
        backend=backend_type,
        size_mb=size,
        filesystem=filesystem,
        description=description,
    )

    manager = get_volume_manager()

    try:
        state = manager.create(config)
        console.print(f"[green]Created encrypted volume:[/green] {name}")
        console.print(f"  Backend: {state.backend.value}")
        console.print(f"  Size: {state.size_mb} MB")
        console.print(f"  Key stored as: {state.key_secret_name}")
    except Exception as e:
        err_console.print(f"[red]Failed to create volume:[/red] {e}")
        raise typer.Exit(1) from None


@volume_app.command(name="unlock")
def volume_unlock(
    name: Annotated[str, typer.Argument(help="Volume name")],
) -> None:
    """Unlock (decrypt) an encrypted volume.

    Examples:
        gaol volume unlock mydata
    """
    from gaol.storage import get_volume_manager

    manager = get_volume_manager()

    try:
        state = manager.unlock(name)
        console.print(f"[green]Unlocked volume:[/green] {name}")
        console.print(f"  Mount point: {state.mount_point}")
    except Exception as e:
        err_console.print(f"[red]Failed to unlock volume:[/red] {e}")
        raise typer.Exit(1) from None


@volume_app.command(name="lock")
def volume_lock(
    name: Annotated[str, typer.Argument(help="Volume name")],
) -> None:
    """Lock (re-encrypt) a volume.

    Examples:
        gaol volume lock mydata
    """
    from gaol.storage import get_volume_manager

    manager = get_volume_manager()

    try:
        manager.lock(name)
        console.print(f"[green]Locked volume:[/green] {name}")
    except Exception as e:
        err_console.print(f"[red]Failed to lock volume:[/red] {e}")
        raise typer.Exit(1) from None


@volume_app.command(name="list")
def volume_list(
    json_output: Annotated[
        bool, typer.Option("--json", "-j", help="Output as JSON")
    ] = False,
) -> None:
    """List all encrypted volumes."""
    import json

    from gaol.storage import get_volume_manager

    manager = get_volume_manager()
    volumes = manager.list()

    if json_output:
        data = [v.model_dump(mode="json") for v in volumes]
        console.print(json.dumps(data, indent=2, default=str))
        return

    if not volumes:
        console.print("[dim]No volumes found[/dim]")
        return

    table = Table(title="Encrypted Volumes")
    table.add_column("Name", style="cyan")
    table.add_column("Backend", style="blue")
    table.add_column("Status", style="green")
    table.add_column("Size", style="yellow")
    table.add_column("Mount Point")

    for vol in volumes:
        status_color = {
            "locked": "red",
            "unlocked": "green",
            "mounted": "cyan",
        }.get(vol.status.value, "white")

        table.add_row(
            vol.name,
            vol.backend.value,
            f"[{status_color}]{vol.status.value}[/{status_color}]",
            f"{vol.size_mb} MB",
            str(vol.mount_point) if vol.mount_point else "-",
        )

    console.print(table)


@volume_app.command(name="delete")
def volume_delete(
    name: Annotated[str, typer.Argument(help="Volume name")],
    force: Annotated[bool, typer.Option("--force", "-f", help="Force deletion")] = False,
) -> None:
    """Delete an encrypted volume.

    Warning: This permanently destroys all data in the volume.
    """
    from gaol.storage import get_volume_manager

    manager = get_volume_manager()

    if not force:
        confirm = typer.confirm(f"Delete volume '{name}'? This cannot be undone.")
        if not confirm:
            console.print("[yellow]Cancelled[/yellow]")
            raise typer.Exit(0)

    try:
        manager.destroy(name, force=force)
        console.print(f"[green]Deleted volume:[/green] {name}")
    except Exception as e:
        err_console.print(f"[red]Failed to delete volume:[/red] {e}")
        raise typer.Exit(1) from None


@volume_app.command(name="show")
def volume_show(
    name: Annotated[str, typer.Argument(help="Volume name")],
    mount_path: Annotated[
        bool, typer.Option("--mount-path", help="Only print mount path")
    ] = False,
) -> None:
    """Show details of an encrypted volume."""
    from gaol.storage import get_volume_manager

    manager = get_volume_manager()
    state = manager.get(name)

    if not state:
        err_console.print(f"[red]Volume not found:[/red] {name}")
        raise typer.Exit(1)

    if mount_path:
        if state.mount_point:
            console.print(str(state.mount_point))
        else:
            err_console.print(f"[red]Volume is not unlocked[/red]")
            raise typer.Exit(1)
        return

    console.print(f"[bold]Volume: {state.name}[/bold]")
    console.print(f"  Backend: {state.backend.value}")
    console.print(f"  Status: {state.status.value}")
    console.print(f"  Size: {state.size_mb} MB")
    console.print(f"  Storage: {state.storage_path}")
    if state.mount_point:
        console.print(f"  Mount: {state.mount_point}")
    if state.filesystem:
        console.print(f"  Filesystem: {state.filesystem}")
    console.print(f"  Created: {state.created_at}")
    if state.unlocked_at:
        console.print(f"  Unlocked: {state.unlocked_at}")
    if state.description:
        console.print(f"  Description: {state.description}")


# =============================================================================
# CoW Commands
# =============================================================================


@cow_app.command(name="init")
def cow_init(
    name: Annotated[str, typer.Argument(help="Pool name")],
    mount_point: Annotated[Path, typer.Argument(help="Path to CoW filesystem mount")],
    backend: Annotated[
        str, typer.Option("--backend", "-b", help="Backend type (btrfs or zfs)")
    ] = "btrfs",
    compression: Annotated[
        str | None, typer.Option("--compression", "-c", help="Compression algorithm")
    ] = None,
) -> None:
    """Initialize a CoW pool on an existing filesystem.

    Examples:
        gaol cow init mypool /mnt/btrfs --backend btrfs
        gaol cow init tank /tank --backend zfs --compression lz4
    """
    from gaol.cow import CoWBackendType, PoolConfig, get_cow_manager

    backend_type = CoWBackendType.BTRFS if backend == "btrfs" else CoWBackendType.ZFS

    config = PoolConfig(
        name=name,
        backend=backend_type,
        mount_point=mount_point,
        compression=compression,
    )

    manager = get_cow_manager()

    try:
        state = manager.init_pool(config)
        console.print(f"[green]Initialized CoW pool:[/green] {name}")
        console.print(f"  Backend: {state.backend.value}")
        console.print(f"  Mount: {state.mount_point}")
        console.print(f"  Path: {state.gaol_path}")
    except Exception as e:
        err_console.print(f"[red]Failed to initialize pool:[/red] {e}")
        raise typer.Exit(1) from None


@cow_app.command(name="pool-list")
def cow_pool_list(
    json_output: Annotated[
        bool, typer.Option("--json", "-j", help="Output as JSON")
    ] = False,
) -> None:
    """List all CoW pools."""
    import json

    from gaol.cow import get_cow_manager

    manager = get_cow_manager()
    pools = manager.list_pools()

    if json_output:
        data = [p.model_dump(mode="json") for p in pools]
        console.print(json.dumps(data, indent=2, default=str))
        return

    if not pools:
        console.print("[dim]No pools found[/dim]")
        return

    table = Table(title="CoW Pools")
    table.add_column("Name", style="cyan")
    table.add_column("Backend", style="blue")
    table.add_column("Status", style="green")
    table.add_column("Mount", style="yellow")
    table.add_column("Usage")

    for pool in pools:
        status_color = {
            "online": "green",
            "degraded": "yellow",
            "offline": "red",
            "error": "red",
        }.get(pool.status.value, "white")

        if pool.total_bytes > 0:
            usage = f"{pool.used_bytes / (1024**3):.1f} / {pool.total_bytes / (1024**3):.1f} GB"
        else:
            usage = "-"

        table.add_row(
            pool.name,
            pool.backend.value,
            f"[{status_color}]{pool.status.value}[/{status_color}]",
            str(pool.mount_point),
            usage,
        )

    console.print(table)


@cow_app.command(name="pool-info")
def cow_pool_info(
    name: Annotated[str, typer.Argument(help="Pool name")],
) -> None:
    """Show details of a CoW pool."""
    from gaol.cow import get_cow_manager

    manager = get_cow_manager()
    pool = manager.get_pool(name)

    if not pool:
        err_console.print(f"[red]Pool not found:[/red] {name}")
        raise typer.Exit(1)

    console.print(f"[bold]Pool: {pool.name}[/bold]")
    console.print(f"  Backend: {pool.backend.value}")
    console.print(f"  Status: {pool.status.value}")
    console.print(f"  Mount: {pool.mount_point}")
    console.print(f"  Path: {pool.gaol_path}")
    if pool.total_bytes > 0:
        console.print(f"  Total: {pool.total_bytes / (1024**3):.2f} GB")
        console.print(f"  Used: {pool.used_bytes / (1024**3):.2f} GB")
        console.print(f"  Free: {pool.free_bytes / (1024**3):.2f} GB")
        console.print(f"  Usage: {pool.usage_percent:.1f}%")
    if pool.compression:
        console.print(f"  Compression: {pool.compression}")
    console.print(f"  Created: {pool.created_at}")


@cow_app.command(name="pool-destroy")
def cow_pool_destroy(
    name: Annotated[str, typer.Argument(help="Pool name")],
    force: Annotated[bool, typer.Option("--force", "-f", help="Force deletion")] = False,
) -> None:
    """Destroy a CoW pool.

    Warning: This permanently destroys all datasets in the pool.
    """
    from gaol.cow import get_cow_manager

    manager = get_cow_manager()

    if not force:
        confirm = typer.confirm(f"Destroy pool '{name}'? This cannot be undone.")
        if not confirm:
            console.print("[yellow]Cancelled[/yellow]")
            raise typer.Exit(0)

    try:
        manager.destroy_pool(name, force=force)
        console.print(f"[green]Destroyed pool:[/green] {name}")
    except Exception as e:
        err_console.print(f"[red]Failed to destroy pool:[/red] {e}")
        raise typer.Exit(1) from None


@cow_app.command(name="dataset-create")
def cow_dataset_create(
    name: Annotated[str, typer.Argument(help="Dataset name (pool/dataset format)")],
    quota: Annotated[
        int | None, typer.Option("--quota", "-q", help="Quota in MB")
    ] = None,
    compression: Annotated[
        str | None, typer.Option("--compression", "-c", help="Compression algorithm")
    ] = None,
) -> None:
    """Create a new dataset.

    Examples:
        gaol cow dataset-create mypool/data
        gaol cow dataset-create mypool/containers --quota 10240
    """
    from gaol.cow import DatasetConfig, get_cow_manager

    if "/" not in name:
        err_console.print("[red]Dataset name must be in pool/dataset format[/red]")
        raise typer.Exit(1)

    pool, dataset = name.split("/", 1)

    config = DatasetConfig(
        name=dataset,
        pool=pool,
        quota_mb=quota,
        compression=compression,
    )

    manager = get_cow_manager()

    try:
        state = manager.create_dataset(config)
        console.print(f"[green]Created dataset:[/green] {state.full_name}")
        console.print(f"  Path: {state.path}")
        if state.quota_mb:
            console.print(f"  Quota: {state.quota_mb} MB")
        if state.compression:
            console.print(f"  Compression: {state.compression}")
    except Exception as e:
        err_console.print(f"[red]Failed to create dataset:[/red] {e}")
        raise typer.Exit(1) from None


@cow_app.command(name="dataset-list")
def cow_dataset_list(
    pool: Annotated[str | None, typer.Option("--pool", "-p", help="Filter by pool")] = None,
    json_output: Annotated[
        bool, typer.Option("--json", "-j", help="Output as JSON")
    ] = False,
) -> None:
    """List all datasets."""
    import json

    from gaol.cow import get_cow_manager

    manager = get_cow_manager()
    datasets = manager.list_datasets(pool)

    if json_output:
        data = [d.model_dump(mode="json") for d in datasets]
        console.print(json.dumps(data, indent=2, default=str))
        return

    if not datasets:
        console.print("[dim]No datasets found[/dim]")
        return

    table = Table(title="CoW Datasets")
    table.add_column("Name", style="cyan")
    table.add_column("Pool", style="blue")
    table.add_column("Used")
    table.add_column("Quota")
    table.add_column("Snapshots", style="green")

    for ds in datasets:
        used = f"{ds.used_bytes / (1024**2):.1f} MB" if ds.used_bytes > 0 else "-"
        quota = f"{ds.quota_mb} MB" if ds.quota_mb else "-"

        table.add_row(
            ds.name,
            ds.pool,
            used,
            quota,
            str(len(ds.snapshots)),
        )

    console.print(table)


@cow_app.command(name="dataset-destroy")
def cow_dataset_destroy(
    name: Annotated[str, typer.Argument(help="Dataset name (pool/dataset format)")],
    force: Annotated[bool, typer.Option("--force", "-f", help="Force deletion")] = False,
) -> None:
    """Destroy a dataset.

    Warning: This permanently destroys all data in the dataset.
    """
    from gaol.cow import get_cow_manager

    if "/" not in name:
        err_console.print("[red]Dataset name must be in pool/dataset format[/red]")
        raise typer.Exit(1)

    pool, dataset = name.split("/", 1)
    manager = get_cow_manager()

    if not force:
        confirm = typer.confirm(f"Destroy dataset '{name}'? This cannot be undone.")
        if not confirm:
            console.print("[yellow]Cancelled[/yellow]")
            raise typer.Exit(0)

    try:
        manager.destroy_dataset(pool, dataset, force=force)
        console.print(f"[green]Destroyed dataset:[/green] {name}")
    except Exception as e:
        err_console.print(f"[red]Failed to destroy dataset:[/red] {e}")
        raise typer.Exit(1) from None


@cow_app.command(name="snapshot-create")
def cow_snapshot_create(
    name: Annotated[str, typer.Argument(help="Snapshot name (pool/dataset@snapshot)")],
) -> None:
    """Create a snapshot.

    Examples:
        gaol cow snapshot-create mypool/data@v1
        gaol cow snapshot-create mypool/containers@backup-20260113
    """
    from gaol.cow import get_cow_manager

    if "@" not in name:
        err_console.print("[red]Snapshot name must be in pool/dataset@snapshot format[/red]")
        raise typer.Exit(1)

    dataset_part, snapshot = name.rsplit("@", 1)
    if "/" not in dataset_part:
        err_console.print("[red]Snapshot name must be in pool/dataset@snapshot format[/red]")
        raise typer.Exit(1)

    pool, dataset = dataset_part.split("/", 1)
    manager = get_cow_manager()

    try:
        snap = manager.create_snapshot(pool, dataset, snapshot)
        console.print(f"[green]Created snapshot:[/green] {snap.full_name}")
        console.print(f"  Created: {snap.created_at}")
    except Exception as e:
        err_console.print(f"[red]Failed to create snapshot:[/red] {e}")
        raise typer.Exit(1) from None


@cow_app.command(name="snapshot-list")
def cow_snapshot_list(
    name: Annotated[str, typer.Argument(help="Dataset name (pool/dataset)")],
    json_output: Annotated[
        bool, typer.Option("--json", "-j", help="Output as JSON")
    ] = False,
) -> None:
    """List snapshots of a dataset."""
    import json

    from gaol.cow import get_cow_manager

    if "/" not in name:
        err_console.print("[red]Dataset name must be in pool/dataset format[/red]")
        raise typer.Exit(1)

    pool, dataset = name.split("/", 1)
    manager = get_cow_manager()

    try:
        snapshots = manager.list_snapshots(pool, dataset)
    except Exception as e:
        err_console.print(f"[red]Failed to list snapshots:[/red] {e}")
        raise typer.Exit(1) from None

    if json_output:
        data = [s.model_dump(mode="json") for s in snapshots]
        console.print(json.dumps(data, indent=2, default=str))
        return

    if not snapshots:
        console.print(f"[dim]No snapshots for {name}[/dim]")
        return

    table = Table(title=f"Snapshots: {name}")
    table.add_column("Name", style="cyan")
    table.add_column("Created")
    table.add_column("Used")
    table.add_column("Referenced")

    for snap in snapshots:
        used = f"{snap.used_bytes / (1024**2):.1f} MB" if snap.used_bytes > 0 else "-"
        ref = f"{snap.referenced_bytes / (1024**2):.1f} MB" if snap.referenced_bytes > 0 else "-"

        table.add_row(
            snap.name,
            str(snap.created_at),
            used,
            ref,
        )

    console.print(table)


@cow_app.command(name="snapshot-delete")
def cow_snapshot_delete(
    name: Annotated[str, typer.Argument(help="Snapshot name (pool/dataset@snapshot)")],
) -> None:
    """Delete a snapshot.

    Examples:
        gaol cow snapshot-delete mypool/data@v1
    """
    from gaol.cow import get_cow_manager

    if "@" not in name:
        err_console.print("[red]Snapshot name must be in pool/dataset@snapshot format[/red]")
        raise typer.Exit(1)

    dataset_part, snapshot = name.rsplit("@", 1)
    if "/" not in dataset_part:
        err_console.print("[red]Snapshot name must be in pool/dataset@snapshot format[/red]")
        raise typer.Exit(1)

    pool, dataset = dataset_part.split("/", 1)
    manager = get_cow_manager()

    try:
        manager.delete_snapshot(pool, dataset, snapshot)
        console.print(f"[green]Deleted snapshot:[/green] {name}")
    except Exception as e:
        err_console.print(f"[red]Failed to delete snapshot:[/red] {e}")
        raise typer.Exit(1) from None


@cow_app.command(name="rollback")
def cow_rollback(
    name: Annotated[str, typer.Argument(help="Snapshot name (pool/dataset@snapshot)")],
) -> None:
    """Rollback a dataset to a snapshot.

    Warning: This may destroy more recent snapshots.

    Examples:
        gaol cow rollback mypool/data@v1
    """
    from gaol.cow import get_cow_manager

    if "@" not in name:
        err_console.print("[red]Snapshot name must be in pool/dataset@snapshot format[/red]")
        raise typer.Exit(1)

    dataset_part, snapshot = name.rsplit("@", 1)
    if "/" not in dataset_part:
        err_console.print("[red]Snapshot name must be in pool/dataset@snapshot format[/red]")
        raise typer.Exit(1)

    pool, dataset = dataset_part.split("/", 1)

    confirm = typer.confirm(
        f"Rollback '{pool}/{dataset}' to '{snapshot}'? This may destroy newer snapshots."
    )
    if not confirm:
        console.print("[yellow]Cancelled[/yellow]")
        raise typer.Exit(0)

    manager = get_cow_manager()

    try:
        state = manager.rollback(pool, dataset, snapshot)
        console.print(f"[green]Rolled back to:[/green] {name}")
        console.print(f"  Dataset path: {state.path}")
    except Exception as e:
        err_console.print(f"[red]Failed to rollback:[/red] {e}")
        raise typer.Exit(1) from None


@cow_app.command(name="clone")
def cow_clone(
    source: Annotated[str, typer.Argument(help="Source snapshot (pool/dataset@snapshot)")],
    name: Annotated[str, typer.Argument(help="Clone name")],
) -> None:
    """Create a writable clone from a snapshot.

    Examples:
        gaol cow clone mypool/data@v1 data-test
        gaol cow clone mypool/containers@backup containers-dev
    """
    from gaol.cow import CloneConfig, get_cow_manager

    if "@" not in source:
        err_console.print("[red]Source must be in pool/dataset@snapshot format[/red]")
        raise typer.Exit(1)

    dataset_part, _ = source.rsplit("@", 1)
    if "/" not in dataset_part:
        err_console.print("[red]Source must be in pool/dataset@snapshot format[/red]")
        raise typer.Exit(1)

    pool = dataset_part.split("/")[0]

    config = CloneConfig(
        name=name,
        source_snapshot=source,
        pool=pool,
    )

    manager = get_cow_manager()

    try:
        state = manager.create_clone(config)
        console.print(f"[green]Created clone:[/green] {state.full_name}")
        console.print(f"  Path: {state.path}")
        console.print(f"  Source: {source}")
    except Exception as e:
        err_console.print(f"[red]Failed to create clone:[/red] {e}")
        raise typer.Exit(1) from None


@cow_app.command(name="promote")
def cow_promote(
    name: Annotated[str, typer.Argument(help="Clone name (pool/name)")],
) -> None:
    """Promote a clone to an independent dataset.

    After promotion, the clone no longer depends on its source snapshot.

    Examples:
        gaol cow promote mypool/data-test
    """
    from gaol.cow import get_cow_manager

    if "/" not in name:
        err_console.print("[red]Clone name must be in pool/name format[/red]")
        raise typer.Exit(1)

    pool, clone = name.split("/", 1)
    manager = get_cow_manager()

    try:
        state = manager.promote_clone(pool, clone)
        console.print(f"[green]Promoted clone:[/green] {state.full_name}")
    except Exception as e:
        err_console.print(f"[red]Failed to promote clone:[/red] {e}")
        raise typer.Exit(1) from None


@cow_app.command(name="send")
def cow_send(
    name: Annotated[str, typer.Argument(help="Snapshot name (pool/dataset@snapshot)")],
    output: Annotated[Path, typer.Option("--output", "-o", help="Output file")],
    base: Annotated[
        str | None, typer.Option("--base", "-b", help="Base snapshot for incremental send")
    ] = None,
) -> None:
    """Send a snapshot to a file (backup).

    Examples:
        gaol cow send mypool/data@v1 -o backup.stream
        gaol cow send mypool/data@v2 --base v1 -o backup-incr.stream
    """
    from gaol.cow import get_cow_manager

    if "@" not in name:
        err_console.print("[red]Snapshot name must be in pool/dataset@snapshot format[/red]")
        raise typer.Exit(1)

    dataset_part, snapshot = name.rsplit("@", 1)
    if "/" not in dataset_part:
        err_console.print("[red]Snapshot name must be in pool/dataset@snapshot format[/red]")
        raise typer.Exit(1)

    pool, dataset = dataset_part.split("/", 1)
    manager = get_cow_manager()

    try:
        result = manager.send(pool, dataset, snapshot, output, base)
        console.print(f"[green]Sent snapshot to:[/green] {result}")
        console.print(f"  Size: {result.stat().st_size / (1024**2):.2f} MB")
    except Exception as e:
        err_console.print(f"[red]Failed to send snapshot:[/red] {e}")
        raise typer.Exit(1) from None


@cow_app.command(name="receive")
def cow_receive(
    pool: Annotated[str, typer.Argument(help="Pool to receive into")],
    source: Annotated[Path, typer.Argument(help="Source stream file")],
    name: Annotated[
        str | None, typer.Option("--name", "-n", help="Name for received dataset")
    ] = None,
) -> None:
    """Receive a snapshot stream into a new dataset.

    Examples:
        gaol cow receive mypool backup.stream
        gaol cow receive mypool backup.stream --name restored-data
    """
    from gaol.cow import get_cow_manager

    if not source.exists():
        err_console.print(f"[red]Source file not found:[/red] {source}")
        raise typer.Exit(1)

    manager = get_cow_manager()

    try:
        state = manager.receive(pool, source, name)
        console.print(f"[green]Received dataset:[/green] {state.full_name}")
        console.print(f"  Path: {state.path}")
    except Exception as e:
        err_console.print(f"[red]Failed to receive stream:[/red] {e}")
        raise typer.Exit(1) from None


# ============================================================================
# Remote Host Management Commands
# ============================================================================


@remote_app.command(name="add")
def remote_add(
    name: Annotated[str, typer.Argument(help="Friendly name for the host")],
    destination: Annotated[str, typer.Argument(help="SSH destination (user@hostname or hostname)")],
    port: Annotated[int, typer.Option("--port", "-p", help="SSH port")] = 22,
    key: Annotated[
        str | None, typer.Option("--key", "-k", help="Path to SSH private key")
    ] = None,
    timeout: Annotated[
        int, typer.Option("--timeout", "-t", help="Connection timeout in seconds")
    ] = 10,
    gaol_path: Annotated[
        str, typer.Option("--gaol-path", help="Path to gaol on remote")
    ] = "gaol",
    no_verify: Annotated[
        bool, typer.Option("--no-verify", help="Skip connectivity verification")
    ] = False,
    no_sudo: Annotated[
        bool, typer.Option("--no-sudo", help="Don't use sudo for gaol commands")
    ] = False,
    description: Annotated[
        str | None, typer.Option("--description", "-d", help="Host description")
    ] = None,
) -> None:
    """Add a new remote host.

    Examples:
        gaol remote add production deploy@prod.example.com
        gaol remote add dev user@dev.local --port 2222 --key ~/.ssh/dev_key
    """
    from gaol.remote import HostConfig, get_remote_manager
    from gaol.remote.exceptions import HostExistsError, SSHConnectionError

    # Parse destination
    if "@" in destination:
        user, hostname = destination.rsplit("@", 1)
    else:
        user = "root"
        hostname = destination

    config = HostConfig(
        name=name,
        hostname=hostname,
        user=user,
        port=port,
        key_path=key,
        connect_timeout=timeout,
        use_sudo=not no_sudo,
        gaol_path=gaol_path,
        description=description,
    )

    try:
        manager = get_remote_manager()
        host = manager.add_host(config, verify=not no_verify)

        console.print(f"[green]Added host:[/green] {name}")
        console.print(f"  Destination: {config.ssh_destination}")
        console.print(f"  Port: {port}")
        if host.state.status.value == "reachable":
            console.print(f"  Status: [green]{host.state.status.value}[/green]")
            if host.state.gaol_version:
                console.print(f"  Gaol: {host.state.gaol_version}")
        elif not no_verify:
            console.print(f"  Status: [yellow]{host.state.status.value}[/yellow]")
            if host.state.error_message:
                console.print(f"  Warning: {host.state.error_message}")

    except HostExistsError:
        err_console.print(f"[red]Host already exists:[/red] {name}")
        raise typer.Exit(1) from None
    except SSHConnectionError as e:
        err_console.print(f"[red]Connection failed:[/red] {e.message}")
        raise typer.Exit(1) from None
    except Exception as e:
        err_console.print(f"[red]Failed to add host:[/red] {e}")
        raise typer.Exit(1) from None


@remote_app.command(name="remove")
def remote_remove(
    name: Annotated[str, typer.Argument(help="Host name to remove")],
    force: Annotated[bool, typer.Option("--force", "-f", help="Skip confirmation")] = False,
) -> None:
    """Remove a remote host."""
    from gaol.remote import get_remote_manager

    manager = get_remote_manager()

    if not force:
        confirm = typer.confirm(f"Remove host '{name}'?")
        if not confirm:
            raise typer.Abort()

    if manager.remove_host(name):
        console.print(f"[green]Removed host:[/green] {name}")
    else:
        err_console.print(f"[red]Host not found:[/red] {name}")
        raise typer.Exit(1)


@remote_app.command(name="list")
def remote_list() -> None:
    """List registered remote hosts."""
    from gaol.remote import get_remote_manager

    manager = get_remote_manager()
    hosts = manager.list_hosts()

    if not hosts:
        console.print("[dim]No remote hosts registered[/dim]")
        return

    table = Table(title="Remote Hosts")
    table.add_column("Name", style="cyan")
    table.add_column("Destination")
    table.add_column("Port")
    table.add_column("Status")
    table.add_column("Gaol")
    table.add_column("Last Check")

    for host in hosts:
        status = host.state.status.value
        status_style = {
            "reachable": "green",
            "unreachable": "red",
            "auth_failed": "red",
            "timeout": "yellow",
            "unknown": "dim",
            "error": "red",
        }.get(status, "dim")

        last_check = ""
        if host.state.last_check:
            last_check = host.state.last_check.strftime("%Y-%m-%d %H:%M")

        table.add_row(
            host.config.name,
            host.config.ssh_destination,
            str(host.config.port),
            f"[{status_style}]{status}[/{status_style}]",
            host.state.gaol_version or "-",
            last_check,
        )

    console.print(table)


@remote_app.command(name="check")
def remote_check(
    name: Annotated[str, typer.Argument(help="Host name to check")],
) -> None:
    """Check connectivity to a remote host."""
    from gaol.remote import get_remote_manager
    from gaol.remote.exceptions import HostNotFoundError

    try:
        manager = get_remote_manager()
        state = manager.check_host(name)

        console.print(f"Host: {name}")
        status_style = {
            "reachable": "green",
            "unreachable": "red",
            "auth_failed": "red",
            "timeout": "yellow",
        }.get(state.status.value, "dim")

        console.print(f"Status: [{status_style}]{state.status.value}[/{status_style}]")

        if state.status.value == "reachable":
            if state.gaol_version:
                console.print(f"Gaol: {state.gaol_version}")
            if state.last_successful_connection:
                console.print(f"Connected at: {state.last_successful_connection.strftime('%Y-%m-%d %H:%M:%S')}")
        elif state.error_message:
            console.print(f"[red]Error:[/red] {state.error_message}")

    except HostNotFoundError:
        err_console.print(f"[red]Host not found:[/red] {name}")
        raise typer.Exit(1) from None


@remote_app.command(name="info")
def remote_info(
    name: Annotated[str, typer.Argument(help="Host name")],
) -> None:
    """Show detailed information about a remote host."""
    from gaol.remote import get_remote_manager
    from gaol.remote.exceptions import HostNotFoundError

    try:
        manager = get_remote_manager()
        host = manager.get_host(name)

        console.print(f"[bold]Host: {host.config.name}[/bold]")
        console.print()
        console.print("[bold]Configuration:[/bold]")
        console.print(f"  Hostname: {host.config.hostname}")
        console.print(f"  User: {host.config.user}")
        console.print(f"  Port: {host.config.port}")
        if host.config.key_path:
            console.print(f"  Key: {host.config.key_path}")
        console.print(f"  Timeout: {host.config.connect_timeout}s")
        console.print(f"  Gaol path: {host.config.gaol_path}")
        if host.config.description:
            console.print(f"  Description: {host.config.description}")

        console.print()
        console.print("[bold]State:[/bold]")
        status_style = "green" if host.state.status.value == "reachable" else "red"
        console.print(f"  Status: [{status_style}]{host.state.status.value}[/{status_style}]")
        if host.state.gaol_version:
            console.print(f"  Gaol version: {host.state.gaol_version}")
        if host.state.last_check:
            console.print(f"  Last check: {host.state.last_check.strftime('%Y-%m-%d %H:%M:%S')}")
        if host.state.last_successful_connection:
            console.print(f"  Last connected: {host.state.last_successful_connection.strftime('%Y-%m-%d %H:%M:%S')}")
        if host.state.error_message:
            console.print(f"  [red]Error: {host.state.error_message}[/red]")

        console.print()
        console.print("[bold]Metadata:[/bold]")
        console.print(f"  Created: {host.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
        console.print(f"  Updated: {host.updated_at.strftime('%Y-%m-%d %H:%M:%S')}")

    except HostNotFoundError:
        err_console.print(f"[red]Host not found:[/red] {name}")
        raise typer.Exit(1) from None


@remote_app.command(name="exec")
def remote_exec(
    name: Annotated[str, typer.Argument(help="Host name")],
    command: Annotated[list[str], typer.Argument(help="Command to execute")],
    timeout: Annotated[
        int | None, typer.Option("--timeout", "-t", help="Command timeout in seconds")
    ] = None,
) -> None:
    """Execute a command on a remote host.

    Example:
        gaol remote exec production -- ls -la
        gaol remote exec dev -- gaol list
    """
    from gaol.remote import get_remote_manager
    from gaol.remote.exceptions import HostNotFoundError, SSHConnectionError

    try:
        manager = get_remote_manager()
        result = manager.execute(name, command, timeout=timeout)

        if result.stdout:
            console.print(result.stdout, end="")
        if result.stderr:
            err_console.print(result.stderr, end="")

        if result.exit_code != 0:
            raise typer.Exit(result.exit_code)

    except HostNotFoundError:
        err_console.print(f"[red]Host not found:[/red] {name}")
        raise typer.Exit(1) from None
    except SSHConnectionError as e:
        err_console.print(f"[red]Connection failed:[/red] {e.message}")
        raise typer.Exit(1) from None


@remote_app.command(name="sync")
def remote_sync(
    container: Annotated[str, typer.Argument(help="Container name")],
    to_host: Annotated[
        str | None, typer.Option("--to", help="Sync config TO this remote host")
    ] = None,
    from_host: Annotated[
        str | None, typer.Option("--from", help="Sync config FROM this remote host")
    ] = None,
) -> None:
    """Sync container configuration between local and remote hosts.

    Examples:
        gaol remote sync mycontainer --to production
        gaol remote sync mycontainer --from staging
    """
    from gaol.remote import get_remote_manager
    from gaol.remote.exceptions import ConfigSyncError, HostNotFoundError

    if not to_host and not from_host:
        err_console.print("[red]Specify --to or --from[/red]")
        raise typer.Exit(1)

    if to_host and from_host:
        err_console.print("[red]Cannot specify both --to and --from[/red]")
        raise typer.Exit(1)

    try:
        manager = get_remote_manager()

        if to_host:
            manager.sync_config_to(container, to_host)
            console.print(f"[green]Synced config to {to_host}:[/green] {container}")
        else:
            path = manager.sync_config_from(container, from_host)
            console.print(f"[green]Synced config from {from_host}:[/green] {container}")
            console.print(f"  Saved to: {path}")

    except HostNotFoundError as e:
        err_console.print(f"[red]Host not found:[/red] {e.host_name}")
        raise typer.Exit(1) from None
    except ConfigSyncError as e:
        err_console.print(f"[red]Sync failed:[/red] {e.reason}")
        raise typer.Exit(1) from None


@remote_app.command(name="install")
def remote_install(
    name: Annotated[str, typer.Argument(help="Host name to install gaol on")],
    runtime: Annotated[
        list[str] | None,
        typer.Option("--runtime", "-r", help="Runtimes to set up (can repeat)"),
    ] = None,
) -> None:
    """Install gaol and prerequisites on a remote host.

    Bootstraps the remote with uv, gaol, and runtime-specific packages.
    The host must already be registered (gaol remote add).

    Examples:
        gaol remote install euclid
        gaol remote install euclid --runtime nspawn --runtime docker
        gaol remote install prod --runtime docker
    """
    from gaol.remote import get_remote_manager
    from gaol.remote.exceptions import HostNotFoundError

    runtimes = runtime or ["nspawn"]

    try:
        manager = get_remote_manager()

        console.print(f"[bold]Installing gaol on {name}[/bold]")
        console.print(f"Runtimes: {', '.join(runtimes)}")
        console.print()

        completed = manager.install_host(name, runtimes)

        console.print()
        console.print(f"[green]Installation complete ({len(completed)} steps):[/green]")
        for step in completed:
            console.print(f"  [green]\u2713[/green] {step}")

    except HostNotFoundError:
        err_console.print(f"[red]Host not found:[/red] {name}")
        err_console.print("Register it first: gaol remote add <name> <user@host>")
        raise typer.Exit(1) from None
    except RuntimeError as e:
        err_console.print(f"[red]Installation failed:[/red] {e}")
        raise typer.Exit(1) from None
    except Exception as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


# ============================================================================
# Update Management Commands
# ============================================================================


@update_app.command(name="check")
def update_check(
    container: Annotated[str, typer.Argument(help="Container name")],
) -> None:
    """Check for available package updates in a container.

    Examples:
        gaol update check mycontainer
    """
    from gaol.update import get_update_manager
    from gaol.update.exceptions import ContainerNotFoundError, ContainerNotRunningError

    try:
        manager = get_update_manager()
        check = manager.check(container)

        if check.error_message:
            err_console.print(f"[red]Error:[/red] {check.error_message}")
            raise typer.Exit(1)

        if not check.updates_available:
            console.print(f"[green]{container}:[/green] All packages up to date")
            return

        console.print(f"[yellow]{container}:[/yellow] {check.package_count} updates available")

        if check.packages:
            table = Table(title="Available Updates")
            table.add_column("Package", style="cyan")
            table.add_column("Current Version")
            table.add_column("New Version", style="green")

            for pkg in check.packages:
                table.add_row(
                    pkg.package_name,
                    pkg.old_version or "-",
                    pkg.new_version or "-",
                )

            console.print(table)

    except ContainerNotFoundError:
        err_console.print(f"[red]Container not found:[/red] {container}")
        raise typer.Exit(1) from None
    except ContainerNotRunningError:
        err_console.print(f"[red]Container not running:[/red] {container}")
        raise typer.Exit(1) from None


@update_app.command(name="apply")
def update_apply(
    container: Annotated[str, typer.Argument(help="Container name")],
    force: Annotated[
        bool, typer.Option("--force", "-f", help="Apply even if no updates available")
    ] = False,
    no_restart: Annotated[
        bool, typer.Option("--no-restart", help="Don't restart container after update")
    ] = False,
    no_snapshot: Annotated[
        bool, typer.Option("--no-snapshot", help="Skip pre-update snapshot")
    ] = False,
) -> None:
    """Apply package updates to a container.

    Examples:
        gaol update apply mycontainer
        gaol update apply mycontainer --force --no-restart
    """
    from gaol.update import get_update_manager
    from gaol.update.exceptions import ContainerNotFoundError, ContainerNotRunningError
    from gaol.update.models import UpdateStatus

    try:
        manager = get_update_manager()

        console.print(f"[dim]Applying updates to {container}...[/dim]")
        result = manager.apply(
            container,
            force=force,
            auto_restart=not no_restart,
        )

        if result.status == UpdateStatus.COMPLETED:
            console.print(f"[green]Update completed:[/green] {container}")
            console.print(f"  Packages upgraded: {result.packages_upgraded}")
            if result.snapshot_name:
                console.print(f"  Snapshot: {result.snapshot_name}")
            if result.duration_seconds:
                console.print(f"  Duration: {result.duration_seconds:.1f}s")
        elif result.status == UpdateStatus.FAILED:
            err_console.print(f"[red]Update failed:[/red] {container}")
            if result.error_message:
                err_console.print(f"  Error: {result.error_message}")
            if result.rolled_back:
                console.print(f"  [yellow]Rolled back to:[/yellow] {result.snapshot_name}")
            raise typer.Exit(1)
        else:
            console.print(f"[yellow]Update status:[/yellow] {result.status.value}")

    except ContainerNotFoundError:
        err_console.print(f"[red]Container not found:[/red] {container}")
        raise typer.Exit(1) from None
    except ContainerNotRunningError:
        err_console.print(f"[red]Container not running:[/red] {container}")
        raise typer.Exit(1) from None


@update_app.command(name="schedule")
def update_schedule(
    container: Annotated[str, typer.Argument(help="Container name")],
    schedule_type: Annotated[
        str, typer.Argument(help="Schedule type: manual, daily, weekly, monthly")
    ],
    time: Annotated[
        str, typer.Option("--time", "-t", help="Time of day for updates (HH:MM)")
    ] = "03:00",
    day: Annotated[
        int, typer.Option("--day", "-d", help="Day of week (0-6) or month (1-28)")
    ] = 0,
) -> None:
    """Set or update the update schedule for a container.

    Examples:
        gaol update schedule mycontainer daily --time 03:00
        gaol update schedule mycontainer weekly --day 6 --time 02:00
        gaol update schedule mycontainer monthly --day 15
        gaol update schedule mycontainer manual  # Disable automatic updates
    """
    from gaol.update import get_update_manager
    from gaol.update.exceptions import ContainerNotFoundError
    from gaol.update.models import UpdateScheduleType

    try:
        schedule_map = {
            "manual": UpdateScheduleType.MANUAL,
            "daily": UpdateScheduleType.DAILY,
            "weekly": UpdateScheduleType.WEEKLY,
            "monthly": UpdateScheduleType.MONTHLY,
        }

        if schedule_type.lower() not in schedule_map:
            err_console.print(
                f"[red]Invalid schedule type:[/red] {schedule_type}. "
                "Use: manual, daily, weekly, monthly"
            )
            raise typer.Exit(1)

        manager = get_update_manager()
        stype = schedule_map[schedule_type.lower()]

        # Determine day_of_week or day_of_month based on schedule type
        day_of_week = 0
        day_of_month = 1
        if stype == UpdateScheduleType.WEEKLY:
            day_of_week = day
        elif stype == UpdateScheduleType.MONTHLY:
            day_of_month = day

        policy = manager.set_schedule(
            container,
            stype,
            time_of_day=time,
            day_of_week=day_of_week,
            day_of_month=day_of_month,
        )

        if stype == UpdateScheduleType.MANUAL:
            console.print(f"[green]Disabled automatic updates for:[/green] {container}")
        else:
            console.print(f"[green]Scheduled updates for:[/green] {container}")
            console.print(f"  Type: {schedule_type}")
            console.print(f"  Time: {time}")
            if stype == UpdateScheduleType.WEEKLY:
                days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
                console.print(f"  Day: {days[day_of_week]}")
            elif stype == UpdateScheduleType.MONTHLY:
                console.print(f"  Day: {day_of_month}")

    except ContainerNotFoundError:
        err_console.print(f"[red]Container not found:[/red] {container}")
        raise typer.Exit(1) from None


@update_app.command(name="history")
def update_history(
    container: Annotated[str, typer.Argument(help="Container name")],
    limit: Annotated[
        int, typer.Option("--limit", "-n", help="Maximum number of results")
    ] = 20,
) -> None:
    """View update history for a container.

    Examples:
        gaol update history mycontainer
        gaol update history mycontainer --limit 10
    """
    from gaol.update import get_update_manager
    from gaol.update.models import UpdateStatus

    manager = get_update_manager()
    history = manager.history(container, limit=limit)

    if not history.results:
        console.print(f"[dim]No update history for {container}[/dim]")
        return

    console.print(f"[bold]Update History: {container}[/bold]")
    console.print(f"  Total updates: {history.total_updates}")
    console.print(f"  Successful: [green]{history.successful_updates}[/green]")
    console.print(f"  Failed: [red]{history.failed_updates}[/red]")
    console.print(f"  Rollbacks: [yellow]{history.rollbacks}[/yellow]")
    console.print()

    table = Table(title="Recent Updates")
    table.add_column("Date", style="dim")
    table.add_column("Status")
    table.add_column("Packages")
    table.add_column("Duration")
    table.add_column("Notes")

    for result in reversed(history.results[-limit:]):
        status_style = {
            UpdateStatus.COMPLETED: "green",
            UpdateStatus.FAILED: "red",
            UpdateStatus.ROLLING_BACK: "yellow",
        }.get(result.status, "dim")

        duration = f"{result.duration_seconds:.1f}s" if result.duration_seconds else "-"
        notes = []
        if result.rolled_back:
            notes.append("rolled back")
        if result.error_message:
            notes.append(result.error_message[:30])

        table.add_row(
            result.started_at.strftime("%Y-%m-%d %H:%M"),
            f"[{status_style}]{result.status.value}[/{status_style}]",
            str(result.packages_upgraded),
            duration,
            ", ".join(notes) if notes else "-",
        )

    console.print(table)


@update_app.command(name="list")
def update_list() -> None:
    """List all containers with scheduled updates.

    Examples:
        gaol update list
    """
    from gaol.update import get_update_manager

    manager = get_update_manager()
    scheduled = manager.list_scheduled()

    if not scheduled:
        console.print("[dim]No containers with scheduled updates[/dim]")
        return

    table = Table(title="Scheduled Updates")
    table.add_column("Container", style="cyan")
    table.add_column("Schedule")
    table.add_column("Time")
    table.add_column("Enabled")
    table.add_column("Rollback")

    for name, policy in scheduled:
        schedule = policy.schedule
        schedule_desc = schedule.schedule_type.value
        if schedule.schedule_type.value == "weekly":
            days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
            schedule_desc = f"weekly ({days[schedule.day_of_week]})"
        elif schedule.schedule_type.value == "monthly":
            schedule_desc = f"monthly (day {schedule.day_of_month})"

        enabled_style = "green" if schedule.enabled else "red"

        table.add_row(
            name,
            schedule_desc,
            schedule.time_of_day,
            f"[{enabled_style}]{'yes' if schedule.enabled else 'no'}[/{enabled_style}]",
            policy.rollback_strategy.value,
        )

    console.print(table)


@update_app.command(name="enable")
def update_enable(
    container: Annotated[str, typer.Argument(help="Container name")],
) -> None:
    """Enable scheduled updates for a container.

    Examples:
        gaol update enable mycontainer
    """
    from gaol.update import get_update_manager
    from gaol.update.exceptions import PolicyNotFoundError

    try:
        manager = get_update_manager()
        manager.enable_schedule(container)
        console.print(f"[green]Enabled scheduled updates for:[/green] {container}")
    except PolicyNotFoundError:
        err_console.print(f"[red]No schedule configured for:[/red] {container}")
        err_console.print("Use 'gaol update schedule' to configure a schedule first.")
        raise typer.Exit(1) from None


@update_app.command(name="disable")
def update_disable(
    container: Annotated[str, typer.Argument(help="Container name")],
) -> None:
    """Disable scheduled updates for a container.

    Examples:
        gaol update disable mycontainer
    """
    from gaol.update import get_update_manager
    from gaol.update.exceptions import PolicyNotFoundError

    try:
        manager = get_update_manager()
        manager.disable_schedule(container)
        console.print(f"[yellow]Disabled scheduled updates for:[/yellow] {container}")
    except PolicyNotFoundError:
        err_console.print(f"[red]No schedule configured for:[/red] {container}")
        raise typer.Exit(1) from None


# -------------------------------------------------------------------------
# Backup commands
# -------------------------------------------------------------------------


@backup_app.command(name="create")
def backup_create(
    container: Annotated[str, typer.Argument(help="Container name")],
    name: Annotated[
        str | None, typer.Option("--name", "-n", help="Backup name (auto-generated if not provided)")
    ] = None,
    backup_type: Annotated[
        str, typer.Option("--type", "-t", help="Backup type: full, incremental, config")
    ] = "full",
) -> None:
    """Create a backup of a container.

    Examples:
        gaol backup create mycontainer
        gaol backup create mycontainer --name daily-backup
        gaol backup create mycontainer --type config
    """
    from gaol.backup import get_backup_manager
    from gaol.backup.exceptions import ContainerNotFoundError
    from gaol.backup.models import BackupStatus, BackupType

    type_map = {
        "full": BackupType.FULL,
        "incremental": BackupType.INCREMENTAL,
        "config": BackupType.CONFIG_ONLY,
    }

    if backup_type.lower() not in type_map:
        err_console.print(
            f"[red]Invalid backup type:[/red] {backup_type}. "
            "Use: full, incremental, config"
        )
        raise typer.Exit(1)

    try:
        manager = get_backup_manager()
        btype = type_map[backup_type.lower()]

        console.print(f"[dim]Creating {backup_type} backup of {container}...[/dim]")
        result = manager.create(container, name=name, backup_type=btype)

        if result.status == BackupStatus.COMPLETED:
            console.print(f"[green]Backup created:[/green] {result.backup_name}")
            console.print(f"  Type: {result.backup_type.value}")
            console.print(f"  Size: {result.size_bytes / 1024 / 1024:.2f} MB")
            if result.storage_path:
                console.print(f"  Path: {result.storage_path}")
        else:
            err_console.print(f"[red]Backup failed:[/red] {container}")
            if result.error_message:
                err_console.print(f"  Error: {result.error_message}")
            raise typer.Exit(1)

    except ContainerNotFoundError:
        err_console.print(f"[red]Container not found:[/red] {container}")
        raise typer.Exit(1) from None


@backup_app.command(name="restore")
def backup_restore(
    container: Annotated[str, typer.Argument(help="Container name")],
    backup_name: Annotated[str, typer.Argument(help="Backup name to restore")],
    no_snapshot: Annotated[
        bool, typer.Option("--no-snapshot", help="Skip safety snapshot before restore")
    ] = False,
) -> None:
    """Restore a container from a backup.

    Examples:
        gaol backup restore mycontainer daily-backup
        gaol backup restore mycontainer daily-backup --no-snapshot
    """
    from gaol.backup import get_backup_manager
    from gaol.backup.exceptions import BackupNotFoundError, ContainerNotFoundError
    from gaol.backup.models import BackupStatus

    try:
        manager = get_backup_manager()

        console.print(f"[dim]Restoring {container} from {backup_name}...[/dim]")
        result = manager.restore(
            container,
            backup_name,
            create_safety_snapshot=not no_snapshot,
        )

        if result.status == BackupStatus.COMPLETED:
            console.print(f"[green]Restore completed:[/green] {container}")
            if result.pre_restore_snapshot:
                console.print(f"  Safety snapshot: {result.pre_restore_snapshot}")
        else:
            err_console.print(f"[red]Restore failed:[/red] {container}")
            if result.error_message:
                err_console.print(f"  Error: {result.error_message}")
            raise typer.Exit(1)

    except ContainerNotFoundError:
        err_console.print(f"[red]Container not found:[/red] {container}")
        raise typer.Exit(1) from None
    except BackupNotFoundError:
        err_console.print(f"[red]Backup not found:[/red] {backup_name}")
        raise typer.Exit(1) from None


@backup_app.command(name="list")
def backup_list(
    container: Annotated[str, typer.Argument(help="Container name")],
) -> None:
    """List all backups for a container.

    Examples:
        gaol backup list mycontainer
    """
    from gaol.backup import get_backup_manager

    manager = get_backup_manager()
    backups = manager.list_backups(container)

    if not backups:
        console.print(f"[dim]No backups found for {container}[/dim]")
        return

    table = Table(title=f"Backups: {container}")
    table.add_column("Name", style="cyan")
    table.add_column("Type")
    table.add_column("Created", style="dim")
    table.add_column("Size")
    table.add_column("Verified")

    for backup in backups:
        verified = "[green]yes[/green]" if backup.verified else "[dim]no[/dim]"
        size_mb = backup.size_bytes / 1024 / 1024
        table.add_row(
            backup.backup_name,
            backup.backup_type.value,
            backup.created_at.strftime("%Y-%m-%d %H:%M"),
            f"{size_mb:.2f} MB",
            verified,
        )

    console.print(table)


@backup_app.command(name="delete")
def backup_delete(
    container: Annotated[str, typer.Argument(help="Container name")],
    backup_name: Annotated[str, typer.Argument(help="Backup name to delete")],
    force: Annotated[
        bool, typer.Option("--force", "-f", help="Delete without confirmation")
    ] = False,
) -> None:
    """Delete a specific backup.

    Examples:
        gaol backup delete mycontainer daily-backup
        gaol backup delete mycontainer daily-backup --force
    """
    from gaol.backup import get_backup_manager

    if not force:
        confirm = typer.confirm(f"Delete backup {backup_name} for {container}?")
        if not confirm:
            console.print("[dim]Aborted[/dim]")
            return

    manager = get_backup_manager()
    deleted = manager.delete_backup(container, backup_name)

    if deleted:
        console.print(f"[green]Deleted backup:[/green] {backup_name}")
    else:
        err_console.print(f"[red]Backup not found:[/red] {backup_name}")
        raise typer.Exit(1)


@backup_app.command(name="verify")
def backup_verify(
    container: Annotated[str, typer.Argument(help="Container name")],
    backup_name: Annotated[str, typer.Argument(help="Backup name to verify")],
) -> None:
    """Verify backup integrity.

    Examples:
        gaol backup verify mycontainer daily-backup
    """
    from gaol.backup import get_backup_manager
    from gaol.backup.exceptions import BackupNotFoundError, VerificationError

    try:
        manager = get_backup_manager()

        console.print(f"[dim]Verifying {backup_name}...[/dim]")
        verified = manager.verify(container, backup_name)

        if verified:
            console.print(f"[green]Verification passed:[/green] {backup_name}")
        else:
            err_console.print(f"[red]Verification failed:[/red] {backup_name}")
            raise typer.Exit(1)

    except BackupNotFoundError:
        err_console.print(f"[red]Backup not found:[/red] {backup_name}")
        raise typer.Exit(1) from None
    except VerificationError as e:
        err_console.print(f"[red]Verification failed:[/red] {e}")
        raise typer.Exit(1) from None


@backup_app.command(name="schedule")
def backup_schedule(
    container: Annotated[str, typer.Argument(help="Container name")],
    schedule_type: Annotated[
        str, typer.Argument(help="Schedule type: manual, daily, weekly, monthly")
    ],
    time: Annotated[
        str, typer.Option("--time", "-t", help="Time of day for backups (HH:MM)")
    ] = "02:00",
    day: Annotated[
        int, typer.Option("--day", "-d", help="Day of week (0-6) or month (1-28)")
    ] = 0,
) -> None:
    """Set or update the backup schedule for a container.

    Examples:
        gaol backup schedule mycontainer daily --time 02:00
        gaol backup schedule mycontainer weekly --day 6 --time 03:00
        gaol backup schedule mycontainer monthly --day 15
        gaol backup schedule mycontainer manual  # Disable automatic backups
    """
    from gaol.backup import get_backup_manager
    from gaol.backup.exceptions import ContainerNotFoundError
    from gaol.backup.models import BackupScheduleType

    try:
        schedule_map = {
            "manual": BackupScheduleType.MANUAL,
            "daily": BackupScheduleType.DAILY,
            "weekly": BackupScheduleType.WEEKLY,
            "monthly": BackupScheduleType.MONTHLY,
        }

        if schedule_type.lower() not in schedule_map:
            err_console.print(
                f"[red]Invalid schedule type:[/red] {schedule_type}. "
                "Use: manual, daily, weekly, monthly"
            )
            raise typer.Exit(1)

        manager = get_backup_manager()
        stype = schedule_map[schedule_type.lower()]

        # Determine day_of_week or day_of_month based on schedule type
        day_of_week = 0
        day_of_month = 1
        if stype == BackupScheduleType.WEEKLY:
            day_of_week = day
        elif stype == BackupScheduleType.MONTHLY:
            day_of_month = day

        policy = manager.set_schedule(
            container,
            stype,
            time_of_day=time,
            day_of_week=day_of_week,
            day_of_month=day_of_month,
        )

        if stype == BackupScheduleType.MANUAL:
            console.print(f"[green]Disabled automatic backups for:[/green] {container}")
        else:
            console.print(f"[green]Scheduled backups for:[/green] {container}")
            console.print(f"  Type: {schedule_type}")
            console.print(f"  Time: {time}")
            if stype == BackupScheduleType.WEEKLY:
                days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
                console.print(f"  Day: {days[day_of_week]}")
            elif stype == BackupScheduleType.MONTHLY:
                console.print(f"  Day: {day_of_month}")

    except ContainerNotFoundError:
        err_console.print(f"[red]Container not found:[/red] {container}")
        raise typer.Exit(1) from None


@backup_app.command(name="history")
def backup_history(
    container: Annotated[str, typer.Argument(help="Container name")],
    limit: Annotated[
        int, typer.Option("--limit", "-n", help="Maximum number of results")
    ] = 20,
) -> None:
    """View backup history for a container.

    Examples:
        gaol backup history mycontainer
        gaol backup history mycontainer --limit 10
    """
    from gaol.backup import get_backup_manager
    from gaol.backup.models import BackupStatus

    manager = get_backup_manager()
    history = manager.history(container, limit=limit)

    if not history.backups:
        console.print(f"[dim]No backup history for {container}[/dim]")
        return

    console.print(f"[bold]Backup History: {container}[/bold]")
    console.print(f"  Total backups: {history.total_backups}")
    console.print(f"  Successful: [green]{history.successful_backups}[/green]")
    console.print(f"  Failed: [red]{history.failed_backups}[/red]")
    if history.total_size_bytes:
        console.print(f"  Total size: {history.total_size_bytes / 1024 / 1024:.2f} MB")
    console.print()

    table = Table(title="Recent Backups")
    table.add_column("Date", style="dim")
    table.add_column("Name")
    table.add_column("Type")
    table.add_column("Status")
    table.add_column("Size")

    for backup in reversed(history.backups[-limit:]):
        status_style = {
            BackupStatus.COMPLETED: "green",
            BackupStatus.FAILED: "red",
        }.get(backup.status, "dim")

        size = f"{backup.size_bytes / 1024 / 1024:.2f} MB" if backup.size_bytes else "-"

        table.add_row(
            backup.created_at.strftime("%Y-%m-%d %H:%M"),
            backup.backup_name,
            backup.backup_type.value,
            f"[{status_style}]{backup.status.value}[/{status_style}]",
            size,
        )

    console.print(table)


@backup_app.command(name="policies")
def backup_policies() -> None:
    """List all containers with backup policies.

    Examples:
        gaol backup policies
    """
    from gaol.backup import get_backup_manager

    manager = get_backup_manager()
    scheduled = manager.list_scheduled()

    if not scheduled:
        console.print("[dim]No containers with scheduled backups[/dim]")
        return

    table = Table(title="Scheduled Backups")
    table.add_column("Container", style="cyan")
    table.add_column("Schedule")
    table.add_column("Time")
    table.add_column("Enabled")
    table.add_column("Retention")

    for name, policy in scheduled:
        schedule = policy.schedule
        schedule_desc = schedule.schedule_type.value
        if schedule.schedule_type.value == "weekly":
            days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
            schedule_desc = f"weekly ({days[schedule.day_of_week]})"
        elif schedule.schedule_type.value == "monthly":
            schedule_desc = f"monthly (day {schedule.day_of_month})"

        enabled_style = "green" if schedule.enabled else "red"

        retention_desc = []
        if policy.retention.keep_last:
            retention_desc.append(f"last {policy.retention.keep_last}")
        if policy.retention.keep_daily:
            retention_desc.append(f"{policy.retention.keep_daily}d")
        if policy.retention.keep_weekly:
            retention_desc.append(f"{policy.retention.keep_weekly}w")

        table.add_row(
            name,
            schedule_desc,
            schedule.time_of_day,
            f"[{enabled_style}]{'yes' if schedule.enabled else 'no'}[/{enabled_style}]",
            ", ".join(retention_desc) if retention_desc else "-",
        )

    console.print(table)


@backup_app.command(name="enable")
def backup_enable(
    container: Annotated[str, typer.Argument(help="Container name")],
) -> None:
    """Enable scheduled backups for a container.

    Examples:
        gaol backup enable mycontainer
    """
    from gaol.backup import get_backup_manager
    from gaol.backup.exceptions import PolicyNotFoundError

    try:
        manager = get_backup_manager()
        manager.enable_schedule(container)
        console.print(f"[green]Enabled scheduled backups for:[/green] {container}")
    except PolicyNotFoundError:
        err_console.print(f"[red]No schedule configured for:[/red] {container}")
        err_console.print("Use 'gaol backup schedule' to configure a schedule first.")
        raise typer.Exit(1) from None


@backup_app.command(name="disable")
def backup_disable(
    container: Annotated[str, typer.Argument(help="Container name")],
) -> None:
    """Disable scheduled backups for a container.

    Examples:
        gaol backup disable mycontainer
    """
    from gaol.backup import get_backup_manager
    from gaol.backup.exceptions import PolicyNotFoundError

    try:
        manager = get_backup_manager()
        manager.disable_schedule(container)
        console.print(f"[yellow]Disabled scheduled backups for:[/yellow] {container}")
    except PolicyNotFoundError:
        err_console.print(f"[red]No schedule configured for:[/red] {container}")
        raise typer.Exit(1) from None


@backup_app.command(name="retention")
def backup_retention(
    container: Annotated[str | None, typer.Argument(help="Container name (or omit for all)")] = None,
    dry_run: Annotated[
        bool, typer.Option("--dry-run", help="Show what would be deleted without deleting")
    ] = False,
) -> None:
    """Apply retention policy to backups.

    Examples:
        gaol backup retention mycontainer
        gaol backup retention --dry-run
        gaol backup retention  # Apply to all containers
    """
    from gaol.backup import get_backup_manager

    manager = get_backup_manager()

    if container:
        deleted = manager.apply_retention(container, dry_run=dry_run)
        if deleted:
            verb = "Would delete" if dry_run else "Deleted"
            console.print(f"[yellow]{verb} {len(deleted)} backup(s) for {container}:[/yellow]")
            for backup in deleted:
                console.print(f"  - {backup.backup_name} ({backup.created_at.strftime('%Y-%m-%d')})")
        else:
            console.print(f"[green]No backups to delete for {container}[/green]")
    else:
        results = manager.apply_retention_all(dry_run=dry_run)
        if results:
            verb = "Would delete" if dry_run else "Deleted"
            total = sum(len(v) for v in results.values())
            console.print(f"[yellow]{verb} {total} backup(s):[/yellow]")
            for cname, deleted in results.items():
                console.print(f"  {cname}: {len(deleted)} backup(s)")
        else:
            console.print("[green]No backups to delete[/green]")


@backup_app.command(name="config")
def backup_config(
    storage: Annotated[
        str | None, typer.Option("--storage", "-s", help="Storage path (local:/path or s3://bucket/prefix)")
    ] = None,
    show: Annotated[
        bool, typer.Option("--show", help="Show current configuration")
    ] = False,
) -> None:
    """Configure backup storage.

    Examples:
        gaol backup config --show
        gaol backup config --storage local:/path/to/backups
        gaol backup config --storage s3://mybucket/backups
    """
    from gaol.backup import get_backup_manager
    from gaol.backup.models import StorageBackendType, StorageConfig

    manager = get_backup_manager()

    if show or storage is None:
        config = manager.get_global_config()
        console.print("[bold]Backup Configuration[/bold]")
        console.print(f"  Backend: {config.default_storage.backend.value}")
        if config.default_storage.path:
            console.print(f"  Path: {config.default_storage.path}")
        if config.default_storage.bucket:
            console.print(f"  Bucket: {config.default_storage.bucket}")
        if config.default_storage.region:
            console.print(f"  Region: {config.default_storage.region}")
        if config.default_storage.prefix:
            console.print(f"  Prefix: {config.default_storage.prefix}")
        return

    # Parse storage URL
    if storage.startswith("local:"):
        path = storage[6:]
        storage_config = StorageConfig(backend=StorageBackendType.LOCAL, path=path)
    elif storage.startswith("s3://"):
        # Parse s3://bucket/prefix
        parts = storage[5:].split("/", 1)
        bucket = parts[0]
        prefix = parts[1] if len(parts) > 1 else None
        storage_config = StorageConfig(
            backend=StorageBackendType.S3,
            bucket=bucket,
            prefix=prefix,
        )
    else:
        err_console.print(
            "[red]Invalid storage format.[/red] Use: local:/path or s3://bucket/prefix"
        )
        raise typer.Exit(1)

    manager.set_storage(storage_config)
    console.print(f"[green]Updated storage configuration:[/green] {storage}")


# -------------------------------------------------------------------------
# Checkpoint commands
# -------------------------------------------------------------------------


@checkpoint_app.command(name="check")
def checkpoint_check(
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Runtime to check (nspawn, docker)")
    ] = None,
) -> None:
    """Check CRIU/checkpoint availability and capabilities.

    Examples:
        gaol checkpoint check
        gaol checkpoint check --runtime docker
    """
    from gaol.checkpoint import get_checkpoint_manager

    manager = get_checkpoint_manager()

    # Check CRIU capabilities
    criu_caps = manager.check_criu()
    console.print("[bold]CRIU Capabilities[/bold]")
    console.print(f"  Available: {'[green]yes[/green]' if criu_caps.available else '[red]no[/red]'}")
    if criu_caps.version:
        console.print(f"  Version: {criu_caps.version}")
    if criu_caps.available:
        console.print(f"  Kernel checkpoint_restore: {'[green]yes[/green]' if criu_caps.kernel_checkpoint_restore else '[red]no[/red]'}")
        console.print(f"  ns_last_pid: {'[green]yes[/green]' if criu_caps.kernel_ns_last_pid else '[red]no[/red]'}")
        console.print(f"  TCP established: {'[green]yes[/green]' if criu_caps.tcp_established else '[dim]no[/dim]'}")
        console.print(f"  File locks: {'[green]yes[/green]' if criu_caps.file_locks else '[dim]no[/dim]'}")
        console.print(f"  Lazy pages: {'[green]yes[/green]' if criu_caps.lazy_pages else '[dim]no[/dim]'}")
        console.print(f"  CAP_SYS_ADMIN: {'[green]yes[/green]' if criu_caps.has_cap_sys_admin else '[red]no[/red]'}")

    # Check Docker capabilities
    docker_caps = manager.check_docker()
    console.print("\n[bold]Docker Checkpoint Capabilities[/bold]")
    console.print(f"  Docker available: {'[green]yes[/green]' if docker_caps.available else '[red]no[/red]'}")
    if docker_caps.version:
        console.print(f"  Version: {docker_caps.version}")
    console.print(f"  Experimental enabled: {'[green]yes[/green]' if docker_caps.experimental_enabled else '[yellow]no[/yellow]'}")
    console.print(f"  Checkpoint supported: {'[green]yes[/green]' if docker_caps.checkpoint_supported else '[dim]no[/dim]'}")

    if not docker_caps.experimental_enabled and docker_caps.available:
        console.print("\n[dim]Tip: Enable Docker experimental mode by adding {\"experimental\": true}")
        console.print("to /etc/docker/daemon.json and restarting Docker.[/dim]")


@checkpoint_app.command(name="create")
def checkpoint_create(
    container: Annotated[str, typer.Argument(help="Container name")],
    name: Annotated[
        str | None, typer.Option("--name", "-n", help="Checkpoint name (auto-generated if not provided)")
    ] = None,
    leave_running: Annotated[
        bool, typer.Option("--leave-running", "-l", help="Keep container running after checkpoint")
    ] = False,
    tcp: Annotated[
        bool, typer.Option("--tcp", help="Checkpoint established TCP connections")
    ] = False,
) -> None:
    """Create a CRIU checkpoint of a running container.

    Captures the complete running state including memory, processes,
    and optionally network connections.

    Examples:
        gaol checkpoint create mycontainer
        gaol checkpoint create mycontainer --name before-upgrade
        gaol checkpoint create mycontainer --leave-running
        gaol checkpoint create mycontainer --tcp
    """
    from gaol.checkpoint import get_checkpoint_manager
    from gaol.checkpoint.exceptions import (
        ContainerNotFoundError,
        ContainerNotRunningError,
        CRIUNotAvailableError,
        DockerExperimentalError,
    )
    from gaol.checkpoint.models import CheckpointStatus

    try:
        manager = get_checkpoint_manager()

        console.print(f"[dim]Creating checkpoint of {container}...[/dim]")
        result = manager.create(
            container,
            name=name,
            leave_running=leave_running,
            tcp_established=tcp,
        )

        if result.status == CheckpointStatus.COMPLETED:
            console.print(f"[green]Checkpoint created:[/green] {result.checkpoint_name}")
            console.print(f"  Size: {result.size_bytes / 1024 / 1024:.2f} MB")
            if result.duration_seconds:
                console.print(f"  Duration: {result.duration_seconds:.2f}s")
            if result.image_dir:
                console.print(f"  Path: {result.image_dir}")
            if leave_running:
                console.print("  Container kept running")
        else:
            err_console.print(f"[red]Checkpoint failed:[/red] {container}")
            if result.error_message:
                err_console.print(f"  Error: {result.error_message}")
            if result.error_output:
                err_console.print(f"  Output: {result.error_output}")
            raise typer.Exit(1)

    except ContainerNotFoundError:
        err_console.print(f"[red]Container not found:[/red] {container}")
        raise typer.Exit(1) from None
    except ContainerNotRunningError:
        err_console.print(f"[red]Container is not running:[/red] {container}")
        err_console.print("[dim]Checkpoints can only be created from running containers.[/dim]")
        raise typer.Exit(1) from None
    except CRIUNotAvailableError as e:
        err_console.print(f"[red]CRIU not available:[/red] {e}")
        err_console.print("[dim]Install CRIU: apt install criu (or equivalent)[/dim]")
        raise typer.Exit(1) from None
    except DockerExperimentalError:
        err_console.print("[red]Docker checkpoint requires experimental mode.[/red]")
        err_console.print('[dim]Add {"experimental": true} to /etc/docker/daemon.json[/dim]')
        raise typer.Exit(1) from None


@checkpoint_app.command(name="restore")
def checkpoint_restore(
    container: Annotated[str, typer.Argument(help="Container name")],
    checkpoint_name: Annotated[str, typer.Argument(help="Checkpoint name to restore")],
    tcp: Annotated[
        bool, typer.Option("--tcp", help="Restore TCP connections")
    ] = False,
) -> None:
    """Restore a container from a checkpoint.

    Resumes the container from a previously created checkpoint,
    restoring its complete state including memory and processes.

    Examples:
        gaol checkpoint restore mycontainer before-upgrade
        gaol checkpoint restore mycontainer before-upgrade --tcp
    """
    from gaol.checkpoint import get_checkpoint_manager
    from gaol.checkpoint.exceptions import (
        CheckpointNotFoundError,
        ContainerNotFoundError,
        ContainerNotRunningError,
    )
    from gaol.checkpoint.models import CheckpointStatus

    try:
        manager = get_checkpoint_manager()

        console.print(f"[dim]Restoring {container} from {checkpoint_name}...[/dim]")
        result = manager.restore(container, checkpoint_name, tcp_established=tcp)

        if result.status == CheckpointStatus.COMPLETED:
            console.print(f"[green]Restore completed:[/green] {container}")
            if result.duration_seconds:
                console.print(f"  Duration: {result.duration_seconds:.2f}s")
            if result.new_container_id:
                console.print(f"  Container ID: {result.new_container_id}")
        else:
            err_console.print(f"[red]Restore failed:[/red] {container}")
            if result.error_message:
                err_console.print(f"  Error: {result.error_message}")
            raise typer.Exit(1)

    except ContainerNotFoundError:
        err_console.print(f"[red]Container not found:[/red] {container}")
        raise typer.Exit(1) from None
    except ContainerNotRunningError:
        err_console.print(f"[red]Container is not running:[/red] {container}")
        raise typer.Exit(1) from None
    except CheckpointNotFoundError:
        err_console.print(f"[red]Checkpoint not found:[/red] {checkpoint_name}")
        raise typer.Exit(1) from None


@checkpoint_app.command(name="list")
def checkpoint_list(
    container: Annotated[str, typer.Argument(help="Container name")],
) -> None:
    """List all checkpoints for a container.

    Examples:
        gaol checkpoint list mycontainer
    """
    from gaol.checkpoint import get_checkpoint_manager
    from gaol.checkpoint.exceptions import ContainerNotFoundError

    try:
        manager = get_checkpoint_manager()
        checkpoints = manager.list(container)

        if not checkpoints:
            console.print(f"[dim]No checkpoints found for {container}[/dim]")
            return

        table = Table(title=f"Checkpoints: {container}")
        table.add_column("Name", style="cyan")
        table.add_column("Created", style="dim")
        table.add_column("Size")
        table.add_column("Leave Running")
        table.add_column("TCP")

        for cp in checkpoints:
            size_mb = cp.size_bytes / 1024 / 1024
            leave_running = "[green]yes[/green]" if cp.leave_running else "[dim]no[/dim]"
            tcp = "[green]yes[/green]" if cp.tcp_established else "[dim]no[/dim]"
            table.add_row(
                cp.checkpoint_name,
                cp.created_at.strftime("%Y-%m-%d %H:%M"),
                f"{size_mb:.2f} MB",
                leave_running,
                tcp,
            )

        console.print(table)

    except ContainerNotFoundError:
        err_console.print(f"[red]Container not found:[/red] {container}")
        raise typer.Exit(1) from None


@checkpoint_app.command(name="delete")
def checkpoint_delete(
    container: Annotated[str, typer.Argument(help="Container name")],
    checkpoint_name: Annotated[str, typer.Argument(help="Checkpoint name to delete")],
    force: Annotated[
        bool, typer.Option("--force", "-f", help="Delete without confirmation")
    ] = False,
) -> None:
    """Delete a specific checkpoint.

    Examples:
        gaol checkpoint delete mycontainer before-upgrade
        gaol checkpoint delete mycontainer before-upgrade --force
    """
    from gaol.checkpoint import get_checkpoint_manager
    from gaol.checkpoint.exceptions import ContainerNotFoundError

    try:
        if not force:
            confirm = typer.confirm(f"Delete checkpoint {checkpoint_name} for {container}?")
            if not confirm:
                console.print("[dim]Aborted[/dim]")
                return

        manager = get_checkpoint_manager()
        deleted = manager.delete(container, checkpoint_name)

        if deleted:
            console.print(f"[green]Deleted checkpoint:[/green] {checkpoint_name}")
        else:
            err_console.print(f"[red]Checkpoint not found:[/red] {checkpoint_name}")
            raise typer.Exit(1)

    except ContainerNotFoundError:
        err_console.print(f"[red]Container not found:[/red] {container}")
        raise typer.Exit(1) from None


@checkpoint_app.command(name="history")
def checkpoint_history(
    container: Annotated[str, typer.Argument(help="Container name")],
    limit: Annotated[
        int | None, typer.Option("--limit", "-n", help="Limit number of results")
    ] = None,
) -> None:
    """Show checkpoint history for a container.

    Examples:
        gaol checkpoint history mycontainer
        gaol checkpoint history mycontainer --limit 10
    """
    from gaol.checkpoint import get_checkpoint_manager
    from gaol.checkpoint.exceptions import ContainerNotFoundError

    try:
        manager = get_checkpoint_manager()
        history = manager.history(container, limit=limit)

        console.print(f"[bold]Checkpoint History: {container}[/bold]")
        console.print(f"  Total checkpoints: {history.total_checkpoints}")
        console.print(f"  Successful: {history.successful_checkpoints}")
        console.print(f"  Failed: {history.failed_checkpoints}")
        console.print(f"  Total size: {history.total_size_bytes / 1024 / 1024:.2f} MB")

        if history.last_checkpoint:
            console.print(f"  Last checkpoint: {history.last_checkpoint.strftime('%Y-%m-%d %H:%M')}")
        if history.last_successful_checkpoint:
            console.print(f"  Last successful: {history.last_successful_checkpoint.strftime('%Y-%m-%d %H:%M')}")

    except ContainerNotFoundError:
        err_console.print(f"[red]Container not found:[/red] {container}")
        raise typer.Exit(1) from None


@checkpoint_app.command(name="clean")
def checkpoint_clean(
    container: Annotated[str, typer.Argument(help="Container name")],
    force: Annotated[
        bool, typer.Option("--force", "-f", help="Delete without confirmation")
    ] = False,
) -> None:
    """Delete all checkpoints for a container.

    Examples:
        gaol checkpoint clean mycontainer
        gaol checkpoint clean mycontainer --force
    """
    from gaol.checkpoint import get_checkpoint_manager
    from gaol.checkpoint.exceptions import ContainerNotFoundError

    try:
        if not force:
            confirm = typer.confirm(f"Delete all checkpoint data for {container}?")
            if not confirm:
                console.print("[dim]Aborted[/dim]")
                return

        manager = get_checkpoint_manager()
        deleted = manager.delete_all(container)

        if deleted:
            console.print(f"[green]Deleted all checkpoint data for:[/green] {container}")
        else:
            console.print(f"[dim]No checkpoint data found for:[/dim] {container}")

    except ContainerNotFoundError:
        err_console.print(f"[red]Container not found:[/red] {container}")
        raise typer.Exit(1) from None


# -------------------------------------------------------------------------
# Metrics commands
# -------------------------------------------------------------------------


@metrics_app.command(name="serve")
def metrics_serve(
    host: Annotated[
        str, typer.Option("--host", "-h", help="Host address to bind")
    ] = "127.0.0.1",
    port: Annotated[
        int, typer.Option("--port", "-p", help="Port number")
    ] = 9100,
    interval: Annotated[
        float, typer.Option("--interval", "-i", help="Metrics collection interval (seconds)")
    ] = 15.0,
    container: Annotated[
        list[str] | None, typer.Option("--container", "-c", help="Specific containers to monitor")
    ] = None,
) -> None:
    """Start the Prometheus metrics server.

    Exposes container metrics at /metrics endpoint for Prometheus scraping.

    Examples:
        gaol metrics serve
        gaol metrics serve --port 9200
        gaol metrics serve --host 0.0.0.0 --interval 30
        gaol metrics serve -c mycontainer -c another
    """
    import signal

    from gaol.metrics import start_metrics_server
    from gaol.metrics.exceptions import MetricsServerAlreadyRunningError, MetricsServerError

    try:
        server = start_metrics_server(
            host=host,
            port=port,
            interval=interval,
            containers=container,
        )

        console.print(f"[green]Metrics server started[/green]")
        console.print(f"  Endpoint: http://{host}:{port}/metrics")
        console.print(f"  Interval: {interval}s")
        if container:
            console.print(f"  Containers: {', '.join(container)}")
        console.print("\n[dim]Press Ctrl+C to stop[/dim]")

        # Handle signals for graceful shutdown
        def signal_handler(sig, frame):
            console.print("\n[dim]Stopping metrics server...[/dim]")
            server.stop()
            raise typer.Exit(0)

        signal.signal(signal.SIGINT, signal_handler)
        signal.signal(signal.SIGTERM, signal_handler)

        # Keep running until interrupted
        while server.is_running:
            signal.pause()

    except MetricsServerAlreadyRunningError as e:
        err_console.print(f"[red]Server already running:[/red] {e}")
        raise typer.Exit(1) from None
    except MetricsServerError as e:
        err_console.print(f"[red]Server error:[/red] {e}")
        raise typer.Exit(1) from None


@metrics_app.command(name="export")
def metrics_export(
    container: Annotated[
        list[str] | None, typer.Argument(help="Containers to export metrics for")
    ] = None,
) -> None:
    """Export metrics to stdout in Prometheus format.

    One-shot export without starting the HTTP server.

    Examples:
        gaol metrics export
        gaol metrics export mycontainer
        gaol metrics export container1 container2
    """
    from gaol.metrics import get_prometheus_exporter

    exporter = get_prometheus_exporter()
    exporter.collect(containers=list(container) if container else None)
    console.print(exporter.generate_metrics_text())


@metrics_app.command(name="dashboard")
def metrics_dashboard(
    title: Annotated[
        str, typer.Option("--title", "-t", help="Dashboard title")
    ] = "Gaol Container Metrics",
    datasource: Annotated[
        str, typer.Option("--datasource", "-d", help="Prometheus datasource name in Grafana")
    ] = "prometheus",
) -> None:
    """Generate a Grafana dashboard JSON.

    Output can be imported directly into Grafana.

    Examples:
        gaol metrics dashboard > dashboard.json
        gaol metrics dashboard --title "My Containers"
    """
    from gaol.metrics import generate_grafana_dashboard

    dashboard = generate_grafana_dashboard(title=title, datasource=datasource)
    console.print(dashboard)


# Alert subcommand for metrics
metrics_alert_app = typer.Typer(help="Manage metric alerts")
metrics_app.add_typer(metrics_alert_app, name="alert")


@metrics_alert_app.command(name="add")
def metrics_alert_add(
    name: Annotated[str, typer.Argument(help="Alert rule name")],
    metric: Annotated[
        str, typer.Option("--metric", "-m", help="Metric to monitor (cpu, memory, memory_percent, network_rx, network_tx, block_read, block_write, pids)")
    ],
    threshold: Annotated[
        float, typer.Option("--threshold", "-t", help="Threshold value")
    ],
    comparison: Annotated[
        str, typer.Option("--comparison", "-c", help="Comparison operator (gt, lt, gte, lte)")
    ] = "gt",
    severity: Annotated[
        str, typer.Option("--severity", "-s", help="Alert severity (info, warning, critical)")
    ] = "warning",
    container_pattern: Annotated[
        str, typer.Option("--pattern", "-p", help="Container name pattern (glob)")
    ] = "*",
    duration: Annotated[
        int, typer.Option("--duration", "-d", help="Duration in seconds before firing")
    ] = 0,
    description: Annotated[
        str, typer.Option("--description", help="Human-readable description")
    ] = "",
) -> None:
    """Add a new alert rule.

    Examples:
        gaol metrics alert add high-cpu --metric cpu --threshold 90
        gaol metrics alert add low-memory --metric memory_percent --threshold 10 --comparison lt
        gaol metrics alert add web-cpu --metric cpu --threshold 80 --pattern "web-*" --severity critical
    """
    from gaol.metrics import AlertRule, AlertSeverity, MetricType, get_alert_manager
    from gaol.metrics.exceptions import AlertRuleExistsError

    try:
        metric_type = MetricType(metric)
    except ValueError:
        err_console.print(f"[red]Invalid metric:[/red] {metric}")
        err_console.print(f"[dim]Valid metrics: {', '.join(m.value for m in MetricType)}[/dim]")
        raise typer.Exit(1) from None

    try:
        severity_level = AlertSeverity(severity)
    except ValueError:
        err_console.print(f"[red]Invalid severity:[/red] {severity}")
        err_console.print(f"[dim]Valid severities: {', '.join(s.value for s in AlertSeverity)}[/dim]")
        raise typer.Exit(1) from None

    if comparison not in ("gt", "lt", "gte", "lte"):
        err_console.print(f"[red]Invalid comparison:[/red] {comparison}")
        err_console.print("[dim]Valid comparisons: gt, lt, gte, lte[/dim]")
        raise typer.Exit(1) from None

    rule = AlertRule(
        name=name,
        container_pattern=container_pattern,
        metric=metric_type,
        threshold=threshold,
        comparison=comparison,  # type: ignore
        severity=severity_level,
        duration_seconds=duration,
        description=description,
    )

    try:
        manager = get_alert_manager()
        manager.add_rule(rule)
        console.print(f"[green]Added alert rule:[/green] {name}")
        console.print(f"  Metric: {metric}")
        console.print(f"  Threshold: {comparison} {threshold}")
        console.print(f"  Severity: {severity}")
        if container_pattern != "*":
            console.print(f"  Pattern: {container_pattern}")
    except AlertRuleExistsError:
        err_console.print(f"[red]Alert rule already exists:[/red] {name}")
        raise typer.Exit(1) from None


@metrics_alert_app.command(name="list")
def metrics_alert_list() -> None:
    """List all alert rules.

    Examples:
        gaol metrics alert list
    """
    from gaol.metrics import get_alert_manager

    manager = get_alert_manager()
    rules = manager.list_rules()

    if not rules:
        console.print("[dim]No alert rules configured[/dim]")
        return

    table = Table(title="Alert Rules")
    table.add_column("Name", style="cyan")
    table.add_column("Metric")
    table.add_column("Threshold")
    table.add_column("Severity")
    table.add_column("Pattern")
    table.add_column("Status")

    for rule in rules:
        op = {"gt": ">", "lt": "<", "gte": ">=", "lte": "<="}[rule.comparison]
        status = "[green]enabled[/green]" if rule.enabled else "[dim]disabled[/dim]"
        table.add_row(
            rule.name,
            rule.metric.value,
            f"{op} {rule.threshold}",
            rule.severity.value,
            rule.container_pattern,
            status,
        )

    console.print(table)


@metrics_alert_app.command(name="delete")
def metrics_alert_delete(
    name: Annotated[str, typer.Argument(help="Alert rule name to delete")],
) -> None:
    """Delete an alert rule.

    Examples:
        gaol metrics alert delete high-cpu
    """
    from gaol.metrics import get_alert_manager

    manager = get_alert_manager()
    if manager.delete_rule(name):
        console.print(f"[green]Deleted alert rule:[/green] {name}")
    else:
        err_console.print(f"[red]Alert rule not found:[/red] {name}")
        raise typer.Exit(1)


@metrics_alert_app.command(name="enable")
def metrics_alert_enable(
    name: Annotated[str, typer.Argument(help="Alert rule name to enable")],
) -> None:
    """Enable an alert rule.

    Examples:
        gaol metrics alert enable high-cpu
    """
    from gaol.metrics import get_alert_manager

    manager = get_alert_manager()
    if manager.enable_rule(name):
        console.print(f"[green]Enabled alert rule:[/green] {name}")
    else:
        err_console.print(f"[red]Alert rule not found:[/red] {name}")
        raise typer.Exit(1)


@metrics_alert_app.command(name="disable")
def metrics_alert_disable(
    name: Annotated[str, typer.Argument(help="Alert rule name to disable")],
) -> None:
    """Disable an alert rule.

    Examples:
        gaol metrics alert disable high-cpu
    """
    from gaol.metrics import get_alert_manager

    manager = get_alert_manager()
    if manager.disable_rule(name):
        console.print(f"[yellow]Disabled alert rule:[/yellow] {name}")
    else:
        err_console.print(f"[red]Alert rule not found:[/red] {name}")
        raise typer.Exit(1)


@metrics_alert_app.command(name="status")
def metrics_alert_status() -> None:
    """Show currently firing alerts.

    Examples:
        gaol metrics alert status
    """
    from gaol.metrics import get_alert_manager, get_prometheus_exporter

    exporter = get_prometheus_exporter()
    manager = get_alert_manager()

    # Collect latest metrics
    snapshot = exporter.collect()

    # Evaluate alerts
    firing = manager.evaluate(snapshot)

    if not firing:
        console.print("[green]No alerts firing[/green]")
        return

    console.print(f"[yellow]Firing Alerts ({len(firing)})[/yellow]\n")

    for alert in firing:
        severity_colors = {
            "info": "blue",
            "warning": "yellow",
            "critical": "red",
        }
        # Get rule for severity
        try:
            rule = manager.get_rule(alert.rule_name)
            color = severity_colors.get(rule.severity.value, "white")
        except Exception:
            color = "white"

        console.print(f"[{color}]{alert.message}[/{color}]")
        if alert.first_triggered:
            console.print(f"  [dim]Since: {alert.first_triggered.strftime('%Y-%m-%d %H:%M:%S')}[/dim]")


@metrics_app.command(name="status")
def metrics_status() -> None:
    """Show metrics server status and configuration.

    Examples:
        gaol metrics status
    """
    from gaol.metrics import get_metrics_server, get_metrics_store

    store = get_metrics_store()
    config = store.get_config()

    console.print("[bold]Metrics Configuration[/bold]")
    console.print(f"  Host: {config.host}")
    console.print(f"  Port: {config.port}")
    console.print(f"  Interval: {config.refresh_interval}s")
    console.print(f"  Enabled: {'yes' if config.enabled else 'no'}")
    if config.containers:
        console.print(f"  Containers: {', '.join(config.containers)}")

    # Check if server is running
    try:
        server = get_metrics_server()
        if server.is_running:
            console.print("\n[green]Server Status: Running[/green]")
            console.print(f"  Endpoint: http://{config.host}:{config.port}/metrics")
        else:
            console.print("\n[dim]Server Status: Not running[/dim]")
    except Exception:
        console.print("\n[dim]Server Status: Not running[/dim]")

    # Show alert rules count
    rules = store.get_rules()
    enabled = len([r for r in rules if r.enabled])
    console.print(f"\n[bold]Alert Rules:[/bold] {len(rules)} total, {enabled} enabled")


# -------------------------------------------------------------------------
# Network Leak Monitor commands
# -------------------------------------------------------------------------


@monitor_app.command(name="start")
def monitor_start(
    name: Annotated[str, typer.Argument(help="Container name to monitor")],
    privacy_type: Annotated[
        str, typer.Option("--type", "-t", help="Privacy type: tor, vpn, wireguard")
    ] = "tor",
    log_file: Annotated[
        str | None, typer.Option("--log", "-l", help="Log leak events to file")
    ] = None,
    run_in_background: Annotated[
        bool, typer.Option("--background", "-b", help="Run in background")
    ] = False,
) -> None:
    """Start network leak monitoring for a privacy container.

    Monitors the container for traffic that bypasses the expected privacy
    tunnel (Tor, VPN, or WireGuard).

    Examples:
        gaol monitor start tor-browser
        gaol monitor start vpn-gateway --type vpn
        gaol monitor start tor-browser --log /var/log/leaks.log -b
    """
    from pathlib import Path as PathLib

    from gaol.monitoring import (
        LeakMonitor,
        MonitorConfig,
        PrivacyType,
    )

    import subprocess

    try:
        ptype = PrivacyType(privacy_type)
    except ValueError:
        err_console.print(f"[red]Invalid privacy type: {privacy_type}[/red]")
        err_console.print("Valid types: tor, vpn, wireguard")
        raise typer.Exit(1)

    # Check if container is running using machinectl
    result = subprocess.run(
        ["sudo", "machinectl", "status", name],
        capture_output=True,
        text=True,
    )
    if result.returncode != 0:
        err_console.print(f"[red]Container '{name}' not found or not running[/red]")
        raise typer.Exit(1)

    config = MonitorConfig(
        privacy_type=ptype,
        log_file=PathLib(log_file) if log_file else None,
    )

    monitor = LeakMonitor(name, config)

    def on_leak(event):
        console.print(f"[red][LEAK][/red] {event.leak_type.value}: {event.details}")
        if event.dest_ip:
            console.print(f"  Destination: {event.dest_ip}:{event.dest_port}")

    config.alert_callback = on_leak

    console.print(f"[green]Starting leak monitor for {name}[/green]")
    console.print(f"  Privacy type: {privacy_type}")
    if log_file:
        console.print(f"  Log file: {log_file}")
    console.print("\n[dim]Press Ctrl+C to stop monitoring...[/dim]\n")

    monitor.start()

    try:
        import time
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        console.print("\n[yellow]Stopping monitor...[/yellow]")
        monitor.stop()

    summary = monitor.get_summary()
    console.print(f"\n[bold]Summary:[/bold]")
    console.print(f"  Total leaks detected: {summary['total_leaks']}")
    if summary['total_leaks'] > 0:
        console.print("  By type:")
        for leak_type, count in summary['leaks_by_type'].items():
            if count > 0:
                console.print(f"    {leak_type}: {count}")


@monitor_app.command(name="test")
def monitor_test(
    name: Annotated[str, typer.Argument(help="Container name to test")],
    privacy_type: Annotated[
        str, typer.Option("--type", "-t", help="Privacy type: tor, vpn, wireguard")
    ] = "tor",
) -> None:
    """Run active leak tests against a privacy container.

    Performs several tests including DNS leak test, direct IP connection test,
    IPv6 leak test, and tunnel verification.

    Examples:
        gaol monitor test tor-browser
        gaol monitor test vpn-gateway --type vpn
    """
    import subprocess

    from gaol.monitoring import (
        LeakMonitor,
        MonitorConfig,
        PrivacyType,
    )

    try:
        ptype = PrivacyType(privacy_type)
    except ValueError:
        err_console.print(f"[red]Invalid privacy type: {privacy_type}[/red]")
        raise typer.Exit(1)

    # Check if container is running using machinectl
    result = subprocess.run(
        ["sudo", "machinectl", "status", name],
        capture_output=True,
        text=True,
    )
    if result.returncode != 0:
        err_console.print(f"[red]Container '{name}' not found or not running[/red]")
        raise typer.Exit(1)

    config = MonitorConfig(privacy_type=ptype)
    monitor = LeakMonitor(name, config)

    console.print(f"[bold]Running leak tests for {name}[/bold]")
    console.print(f"Privacy type: {privacy_type}\n")

    # Run active tests
    console.print("[cyan]Running active leak tests...[/cyan]")
    events = monitor.run_leak_test()

    if events:
        console.print(f"\n[red]FAILED: {len(events)} leak(s) detected![/red]\n")
        for event in events:
            console.print(f"  [red]✗[/red] {event.leak_type.value}: {event.details}")
    else:
        console.print("\n[green]PASSED: No leaks detected in active tests[/green]")

    # Check iptables rules
    console.print("\n[cyan]Checking firewall rules...[/cyan]")
    rules = monitor.check_iptables_rules()

    if "error" in rules:
        console.print(f"  [yellow]Warning: {rules['error']}[/yellow]")
    else:
        checks = [
            ("Tor redirect", rules.get("has_tor_redirect", False)),
            ("DNS blocking", rules.get("has_dns_block", False)),
            ("Kill switch", rules.get("has_kill_switch", False)),
            ("Leak logging", rules.get("has_logging", False)),
        ]
        for check_name, passed in checks:
            if passed:
                console.print(f"  [green]✓[/green] {check_name}")
            else:
                console.print(f"  [yellow]○[/yellow] {check_name} (not configured)")

    # Final verdict
    if events:
        console.print("\n[red]Container has potential privacy leaks![/red]")
        raise typer.Exit(1)
    else:
        console.print("\n[green]Container appears secure.[/green]")


@monitor_app.command(name="rules")
def monitor_rules(
    name: Annotated[str, typer.Argument(help="Container name")],
    show_all: Annotated[
        bool, typer.Option("--all", "-a", help="Show all iptables rules")
    ] = False,
) -> None:
    """Show firewall rules for a privacy container.

    Displays iptables/nftables rules configured in the container
    and checks for proper privacy protection.

    Examples:
        gaol monitor rules tor-browser
        gaol monitor rules vpn-gateway --all
    """
    import subprocess

    from gaol.monitoring import LeakMonitor, MonitorConfig, PrivacyType

    # Check if container is running
    result = subprocess.run(
        ["sudo", "machinectl", "status", name],
        capture_output=True,
        text=True,
    )
    if result.returncode != 0:
        err_console.print(f"[red]Container '{name}' not found or not running[/red]")
        raise typer.Exit(1)

    config = MonitorConfig(privacy_type=PrivacyType.TOR)
    monitor = LeakMonitor(name, config)

    rules = monitor.check_iptables_rules()

    if "error" in rules:
        err_console.print(f"[red]Error: {rules['error']}[/red]")
        raise typer.Exit(1)

    console.print(f"[bold]Firewall Rules for {name}[/bold]\n")

    # Summary
    table = Table(title="Rule Checks")
    table.add_column("Check", style="cyan")
    table.add_column("Status")

    checks = [
        ("Tor Transparent Redirect", rules.get("has_tor_redirect", False)),
        ("DNS Block/Redirect", rules.get("has_dns_block", False)),
        ("Kill Switch (DROP/REJECT)", rules.get("has_kill_switch", False)),
        ("Leak Logging", rules.get("has_logging", False)),
    ]

    for check_name, passed in checks:
        status = "[green]✓ Configured[/green]" if passed else "[dim]○ Not found[/dim]"
        table.add_row(check_name, status)

    console.print(table)

    # Show all rules if requested
    if show_all and rules.get("rules"):
        console.print("\n[bold]All Rules:[/bold]")
        for rule in rules["rules"]:
            if rule.strip():
                console.print(f"  {rule}")


@monitor_app.command(name="install-service")
def monitor_install_service(
    name: Annotated[str, typer.Argument(help="Container name to monitor")],
    privacy_type: Annotated[
        str, typer.Option("--type", "-t", help="Privacy type: tor, vpn, wireguard")
    ] = "tor",
) -> None:
    """Install a systemd service for continuous leak monitoring.

    Creates and enables a systemd service that monitors the container
    for leaks whenever it is running.

    Examples:
        gaol monitor install-service tor-browser
        gaol monitor install-service vpn-gateway --type vpn
    """
    import subprocess
    from pathlib import Path as PathLib

    script_path = PathLib(__file__).parent.parent.parent.parent / "scripts" / "install-leak-monitor.sh"

    if not script_path.exists():
        # Try alternate location
        script_path = PathLib("/usr/local/share/gaol/scripts/install-leak-monitor.sh")

    if not script_path.exists():
        err_console.print("[red]Installation script not found[/red]")
        err_console.print("Please run scripts/install-leak-monitor.sh manually")
        raise typer.Exit(1)

    result = subprocess.run(
        ["sudo", "bash", str(script_path), name, "--type", privacy_type],
        capture_output=False,
    )

    if result.returncode != 0:
        raise typer.Exit(result.returncode)


# -------------------------------------------------------------------------
# Diff commands
# -------------------------------------------------------------------------


@diff_app.command(name="containers")
def diff_containers(
    source: Annotated[str, typer.Argument(help="Source container name")],
    target: Annotated[str, typer.Argument(help="Target container name")],
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
    output: Annotated[
        str, typer.Option("--output", "-o", help="Output format (console, json)")
    ] = "console",
    include: Annotated[
        list[str] | None, typer.Option("--include", "-i", help="Include path patterns")
    ] = None,
    exclude: Annotated[
        list[str] | None, typer.Option("--exclude", "-e", help="Exclude path patterns")
    ] = None,
    packages: Annotated[
        bool, typer.Option("--packages/--no-packages", help="Compare installed packages")
    ] = True,
    max_changes: Annotated[
        int | None, typer.Option("--max", help="Maximum changes to show")
    ] = None,
    no_color: Annotated[
        bool, typer.Option("--no-color", help="Disable colored output")
    ] = False,
) -> None:
    """Compare filesystems of two containers.

    Shows all file and package differences between the source and target containers.

    Examples:
        gaol diff containers web-dev web-prod
        gaol diff containers mycontainer backup --runtime docker
        gaol diff containers a b --include "/etc/*" --exclude "/var/log/*"
        gaol diff containers a b --output json
    """
    from gaol.diff import (
        DiffOptions,
        DiffSourceNotFoundError,
        compare_containers,
        get_formatter,
    )

    config = get_config()
    runtime_name = runtime or config.default_runtime

    # Build options
    options = DiffOptions()
    if include:
        options.include_paths = list(include)
    if exclude:
        options.exclude_paths.extend(list(exclude))
    options.compare_packages = packages

    try:
        result = compare_containers(
            source_id=source,
            target_id=target,
            runtime=runtime_name,
            options=options,
            include_packages=packages,
        )

        # Format and output
        formatter = get_formatter(
            output,
            color=not no_color,
            show_packages=packages,
            max_changes=max_changes,
        )
        console.print(formatter.format(result))

    except DiffSourceNotFoundError as e:
        err_console.print(f"[red]Container not found:[/red] {e}")
        raise typer.Exit(1) from None
    except Exception as e:
        err_console.print(f"[red]Diff failed:[/red] {e}")
        raise typer.Exit(1) from None


@diff_app.command(name="snapshots")
def diff_snapshots(
    container: Annotated[str, typer.Argument(help="Container name")],
    source_snap: Annotated[
        str, typer.Option("--from", "-f", help="Source snapshot name")
    ],
    target_snap: Annotated[
        str, typer.Option("--to", "-t", help="Target snapshot name")
    ],
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
    output: Annotated[
        str, typer.Option("--output", "-o", help="Output format (console, json)")
    ] = "console",
    include: Annotated[
        list[str] | None, typer.Option("--include", "-i", help="Include path patterns")
    ] = None,
    exclude: Annotated[
        list[str] | None, typer.Option("--exclude", "-e", help="Exclude path patterns")
    ] = None,
    max_changes: Annotated[
        int | None, typer.Option("--max", help="Maximum changes to show")
    ] = None,
    no_color: Annotated[
        bool, typer.Option("--no-color", help="Disable colored output")
    ] = False,
) -> None:
    """Compare two snapshots of a container.

    Shows all file differences between the source and target snapshots.

    Examples:
        gaol diff snapshots mycontainer --from baseline --to after-install
        gaol diff snapshots web --from v1.0 --to v1.1 --output json
    """
    from gaol.diff import (
        DiffOptions,
        DiffSnapshotNotFoundError,
        compare_snapshots,
        get_formatter,
    )

    config = get_config()
    runtime_name = runtime or config.default_runtime

    # Build options
    options = DiffOptions()
    if include:
        options.include_paths = list(include)
    if exclude:
        options.exclude_paths.extend(list(exclude))

    try:
        result = compare_snapshots(
            container_name=container,
            source_snapshot=source_snap,
            target_snapshot=target_snap,
            runtime=runtime_name,
            options=options,
        )

        formatter = get_formatter(
            output,
            color=not no_color,
            max_changes=max_changes,
        )
        console.print(formatter.format(result))

    except DiffSnapshotNotFoundError as e:
        err_console.print(f"[red]Snapshot not found:[/red] {e}")
        raise typer.Exit(1) from None
    except Exception as e:
        err_console.print(f"[red]Diff failed:[/red] {e}")
        raise typer.Exit(1) from None


@diff_app.command(name="baseline")
def diff_baseline(
    container: Annotated[str, typer.Argument(help="Container name")],
    image: Annotated[
        str | None, typer.Option("--image", "-i", help="Base image (auto-detect if omitted)")
    ] = None,
    output: Annotated[
        str, typer.Option("--output", "-o", help="Output format (console, json)")
    ] = "console",
    include: Annotated[
        list[str] | None, typer.Option("--include", help="Include path patterns")
    ] = None,
    exclude: Annotated[
        list[str] | None, typer.Option("--exclude", help="Exclude path patterns")
    ] = None,
    max_changes: Annotated[
        int | None, typer.Option("--max", help="Maximum changes to show")
    ] = None,
    no_color: Annotated[
        bool, typer.Option("--no-color", help="Disable colored output")
    ] = False,
) -> None:
    """Compare container to its base image.

    Shows all changes made to the container since it was created from its base image.
    Docker only.

    Examples:
        gaol diff baseline mycontainer --image debian:trixie
        gaol diff baseline web --output json
    """
    from gaol.diff import (
        DiffOptions,
        DiffUnsupportedRuntimeError,
        compare_to_image,
        get_formatter,
    )

    # Build options
    options = DiffOptions()
    if include:
        options.include_paths = list(include)
    if exclude:
        options.exclude_paths.extend(list(exclude))

    try:
        result = compare_to_image(
            container_id=container,
            image_name=image,
            runtime="docker",
            options=options,
        )

        formatter = get_formatter(
            output,
            color=not no_color,
            max_changes=max_changes,
        )
        console.print(formatter.format(result))

    except DiffUnsupportedRuntimeError:
        err_console.print("[red]Baseline comparison only supported for Docker[/red]")
        raise typer.Exit(1) from None
    except ValueError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None
    except Exception as e:
        err_console.print(f"[red]Diff failed:[/red] {e}")
        raise typer.Exit(1) from None


@app.command(rich_help_panel=PANEL_STORAGE)
def audit(
    container: Annotated[str, typer.Argument(help="Container name")],
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
    output: Annotated[
        str, typer.Option("--output", "-o", help="Output format (console, json)")
    ] = "console",
    packages: Annotated[
        bool, typer.Option("--packages/--no-packages", help="Show package changes")
    ] = True,
    include: Annotated[
        list[str] | None, typer.Option("--include", "-i", help="Include path patterns")
    ] = None,
    exclude: Annotated[
        list[str] | None, typer.Option("--exclude", "-e", help="Exclude path patterns")
    ] = None,
    max_changes: Annotated[
        int | None, typer.Option("--max", help="Maximum changes to show")
    ] = None,
    no_color: Annotated[
        bool, typer.Option("--no-color", help="Disable colored output")
    ] = False,
) -> None:
    """Audit container changes since last snapshot.

    Compares the current container state to its most recent snapshot,
    showing all changes made since the snapshot was taken.

    Examples:
        gaol audit mycontainer
        gaol audit web --packages --output json
        gaol audit db --include "/etc/*" --exclude "/var/log/*"
    """
    from gaol.diff import (
        DiffOptions,
        DiffSnapshotNotFoundError,
        audit_container,
        get_formatter,
    )

    config = get_config()
    runtime_name = runtime or config.default_runtime

    # Build options
    options = DiffOptions()
    if include:
        options.include_paths = list(include)
    if exclude:
        options.exclude_paths.extend(list(exclude))
    options.compare_packages = packages

    try:
        result = audit_container(
            container_name=container,
            runtime=runtime_name,
            options=options,
            include_packages=packages,
        )

        if not result.has_changes:
            console.print("[green]No changes detected since last snapshot[/green]")
            return

        formatter = get_formatter(
            output,
            color=not no_color,
            show_packages=packages,
            max_changes=max_changes,
        )
        console.print(formatter.format(result))

    except DiffSnapshotNotFoundError:
        err_console.print("[red]No snapshots found for this container[/red]")
        err_console.print("[dim]Create a snapshot first with: gaol snapshot create <name>[/dim]")
        raise typer.Exit(1) from None
    except Exception as e:
        err_console.print(f"[red]Audit failed:[/red] {e}")
        raise typer.Exit(1) from None


# =============================================================================
# Build commands
# =============================================================================


@build_app.command(name="dockerfile")
def build_dockerfile(
    container: Annotated[str, typer.Argument(help="Container name")],
    output: Annotated[
        str | None, typer.Option("--output", "-o", help="Output file (default: stdout)")
    ] = None,
    base_image: Annotated[
        str | None, typer.Option("--base-image", "-b", help="Override base image")
    ] = None,
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
    no_packages: Annotated[
        bool, typer.Option("--no-packages", help="Skip package detection")
    ] = False,
    no_environment: Annotated[
        bool, typer.Option("--no-env", help="Skip environment variables")
    ] = False,
) -> None:
    """Generate Dockerfile from container state.

    Analyzes the container and produces a Dockerfile that can recreate
    its current state including installed packages and configuration.

    Examples:
        gaol build dockerfile mycontainer
        gaol build dockerfile mycontainer -o Dockerfile
        gaol build dockerfile web --base-image debian:bookworm
    """
    from gaol.build import (
        BaseImageNotFoundError,
        ContainerNotFoundError,
        generate_dockerfile,
    )

    config = get_config()
    runtime_name = runtime or config.default_runtime

    try:
        dockerfile = generate_dockerfile(
            container,
            runtime=runtime_name,
            base_image=base_image,
            include_packages=not no_packages,
            include_environment=not no_environment,
        )

        content = dockerfile.render()

        if output:
            Path(output).write_text(content)
            console.print(f"[green]Dockerfile written to:[/green] {output}")
            console.print(f"[dim]Base image: {dockerfile.base_image}[/dim]")
            if dockerfile.packages:
                console.print(f"[dim]Packages: {len(dockerfile.packages)}[/dim]")
        else:
            console.print(content)

    except ContainerNotFoundError:
        err_console.print(f"[red]Container not found:[/red] {container}")
        raise typer.Exit(1) from None
    except BaseImageNotFoundError as e:
        err_console.print(f"[red]Cannot determine base image:[/red] {e}")
        err_console.print("[dim]Specify --base-image manually[/dim]")
        raise typer.Exit(1) from None
    except Exception as e:
        err_console.print(f"[red]Failed to generate Dockerfile:[/red] {e}")
        raise typer.Exit(1) from None


@build_app.command(name="image")
def build_image_cmd(
    container: Annotated[str, typer.Argument(help="Container name")],
    tag: Annotated[
        str, typer.Option("--tag", "-t", help="Image tag")
    ] = "latest",
    repository: Annotated[
        str | None, typer.Option("--repo", help="Repository name (default: gaol-build/<container>)")
    ] = None,
    method: Annotated[
        str, typer.Option("--method", "-m", help="Build method: commit (fast) or dockerfile (reproducible)")
    ] = "commit",
    runtime: Annotated[
        str | None, typer.Option("--runtime", "-r", help="Container runtime")
    ] = None,
    label: Annotated[
        list[str] | None, typer.Option("--label", "-l", help="Add label (KEY=VALUE)")
    ] = None,
    no_packages: Annotated[
        bool, typer.Option("--no-packages", help="Skip package detection (dockerfile method)")
    ] = False,
) -> None:
    """Build OCI image from container state.

    Creates a container image that captures the current container state.
    Two methods are available:

    - commit: Fast method using docker commit (default)
    - dockerfile: Slower but more reproducible, generates and builds Dockerfile

    Examples:
        gaol build image mycontainer --tag v1.0
        gaol build image web --method dockerfile --repo myapp
        gaol build image db -t backup --label version=1.0
    """
    from gaol.build import (
        ContainerNotFoundError,
        ImageBuildError,
        build_image,
    )

    config = get_config()
    runtime_name = runtime or config.default_runtime

    # Parse labels
    labels = {}
    if label:
        for lbl in label:
            if "=" in lbl:
                key, value = lbl.split("=", 1)
                labels[key] = value
            else:
                err_console.print(f"[yellow]Invalid label format (use KEY=VALUE):[/yellow] {lbl}")

    # Validate method
    if method not in ("commit", "dockerfile"):
        err_console.print(f"[red]Invalid method:[/red] {method}")
        err_console.print("[dim]Use 'commit' or 'dockerfile'[/dim]")
        raise typer.Exit(1)

    try:
        console.print(f"[dim]Building image from {container} using {method} method...[/dim]")

        result = build_image(
            container,
            tag=tag,
            repository=repository,
            method=method,
            labels=labels if labels else None,
            include_packages=not no_packages,
        )

        if result.success:
            console.print("[green]Image built successfully[/green]")
            console.print(f"  Image ID: {result.image_id[:12] if result.image_id else 'unknown'}")
            for img_tag in result.tags:
                console.print(f"  Tag: {img_tag}")
            console.print(f"  Duration: {result.duration_seconds:.1f}s")
        else:
            err_console.print(f"[red]Build failed:[/red] {result.error}")
            if result.build_log:
                err_console.print("[dim]Build log:[/dim]")
                for line in result.build_log[-10:]:
                    err_console.print(f"  {line}")
            raise typer.Exit(1)

    except ContainerNotFoundError:
        err_console.print(f"[red]Container not found:[/red] {container}")
        raise typer.Exit(1) from None
    except ImageBuildError as e:
        err_console.print(f"[red]Build failed:[/red] {e}")
        raise typer.Exit(1) from None
    except Exception as e:
        err_console.print(f"[red]Build failed:[/red] {e}")
        raise typer.Exit(1) from None


@build_app.command(name="push")
def build_push(
    image: Annotated[str, typer.Argument(help="Image reference (name:tag)")],
    registry: Annotated[
        str | None, typer.Option("--registry", "-r", help="Target registry URL")
    ] = None,
    progress: Annotated[
        bool, typer.Option("--progress/--no-progress", help="Show push progress")
    ] = True,
) -> None:
    """Push image to a container registry.

    Pushes a locally built image to a container registry like Docker Hub,
    GitHub Container Registry, or a private registry.

    Examples:
        gaol build push myapp:latest
        gaol build push myapp:v1.0 --registry ghcr.io/user
        gaol build push db:backup -r registry.example.com
    """
    from gaol.build import (
        ImageNotFoundError,
        RegistryPushError,
        push_image,
        push_image_with_progress,
    )

    try:
        if progress:
            console.print(f"[dim]Pushing {image}...[/dim]")
            last_layer = None
            for update in push_image_with_progress(image, registry):
                if update.layer_id and update.layer_id != last_layer:
                    last_layer = update.layer_id
                if update.progress:
                    console.print(f"  {update.layer_id or ''}: {update.status} {update.progress}", end="\r")
                else:
                    console.print(f"  {update.layer_id or ''}: {update.status}")
            console.print()
            console.print("[green]Push completed[/green]")
        else:
            result = push_image(image, registry)
            if result.success:
                console.print("[green]Push completed[/green]")
                if result.digest:
                    console.print(f"  Digest: {result.digest}")
                console.print(f"  Duration: {result.duration_seconds:.1f}s")
            else:
                err_console.print(f"[red]Push failed:[/red] {result.error}")
                raise typer.Exit(1)

    except ImageNotFoundError:
        err_console.print(f"[red]Image not found:[/red] {image}")
        raise typer.Exit(1) from None
    except RegistryPushError as e:
        err_console.print(f"[red]Push failed:[/red] {e}")
        raise typer.Exit(1) from None
    except Exception as e:
        err_console.print(f"[red]Push failed:[/red] {e}")
        raise typer.Exit(1) from None


@build_app.command(name="history")
def build_history(
    image: Annotated[str, typer.Argument(help="Image reference or container name")],
    output: Annotated[
        str, typer.Option("--output", "-o", help="Output format (table, json)")
    ] = "table",
) -> None:
    """Show image layer history.

    Displays the layers that make up an image, showing what commands
    created each layer and their sizes.

    Examples:
        gaol build history myapp:latest
        gaol build history gaol-build/web:v1 --output json
    """
    import json

    from gaol.build import ImageNotFoundError, get_image_info

    try:
        info = get_image_info(image)

        if output == "json":
            data = {
                "id": info.id,
                "tags": info.tags,
                "size_bytes": info.size_bytes,
                "architecture": info.architecture,
                "os": info.os,
                "layers": [
                    {
                        "id": layer.id,
                        "created_by": layer.created_by,
                        "size_bytes": layer.size_bytes,
                        "created_at": layer.created_at.isoformat() if layer.created_at else None,
                    }
                    for layer in info.layers
                ],
            }
            console.print(json.dumps(data, indent=2))
        else:
            # Table output
            console.print(f"[bold]Image:[/bold] {info.primary_tag or info.id[:12]}")
            console.print(f"[dim]ID: {info.id}[/dim]")
            console.print(f"[dim]Size: {info.size_bytes / 1024 / 1024:.1f} MB[/dim]")
            console.print(f"[dim]Arch: {info.architecture or 'unknown'}/{info.os}[/dim]")
            console.print()

            table = Table(title="Layers")
            table.add_column("ID", style="cyan")
            table.add_column("Size", justify="right")
            table.add_column("Created By", max_width=60)

            for layer in info.layers:
                size_str = f"{layer.size_bytes / 1024:.0f} KB" if layer.size_bytes > 0 else "-"
                created_by = layer.created_by[:60] if layer.created_by else "-"
                table.add_row(layer.id, size_str, created_by)

            console.print(table)

    except ImageNotFoundError:
        err_console.print(f"[red]Image not found:[/red] {image}")
        raise typer.Exit(1) from None
    except Exception as e:
        err_console.print(f"[red]Failed to get image history:[/red] {e}")
        raise typer.Exit(1) from None


@build_app.command(name="list")
def build_list(
    output: Annotated[
        str, typer.Option("--output", "-o", help="Output format (table, json)")
    ] = "table",
) -> None:
    """List images built by gaol.

    Shows all images that were built from containers using gaol.

    Examples:
        gaol build list
        gaol build list --output json
    """
    import json

    from gaol.build import ImageBuilder

    try:
        builder = ImageBuilder()
        images = builder.list_built_images()

        if not images:
            console.print("[dim]No gaol-built images found[/dim]")
            return

        if output == "json":
            data = [
                {
                    "id": img.id,
                    "tags": img.tags,
                    "size_bytes": img.size_bytes,
                    "created_at": img.created_at.isoformat() if img.created_at else None,
                    "labels": img.labels,
                }
                for img in images
            ]
            console.print(json.dumps(data, indent=2))
        else:
            table = Table(title="Gaol-Built Images")
            table.add_column("Tag", style="cyan")
            table.add_column("ID", style="dim")
            table.add_column("Size", justify="right")
            table.add_column("Built From")
            table.add_column("Method")

            for img in images:
                tag = img.primary_tag or "-"
                img_id = img.id[:12] if img.id else "-"
                size = f"{img.size_bytes / 1024 / 1024:.1f} MB"
                built_from = img.labels.get("dev.gaol.built-from", "-")
                method = img.labels.get("dev.gaol.build-method", "-")
                table.add_row(tag, img_id, size, built_from, method)

            console.print(table)

    except Exception as e:
        err_console.print(f"[red]Failed to list images:[/red] {e}")
        raise typer.Exit(1) from None


# ============================================================================
# Template Commands
# ============================================================================


@template_app.command(name="list")
def template_list(
    category: Annotated[
        str | None,
        typer.Option("-c", "--category", help="Filter by category"),
    ] = None,
    local_only: Annotated[
        bool,
        typer.Option("-l", "--local", help="Show only local templates"),
    ] = False,
    json_output: Annotated[
        bool,
        typer.Option("-j", "--json", help="Output as JSON"),
    ] = False,
) -> None:
    """List available templates."""
    from gaol.templates import get_template_loader

    loader = get_template_loader()

    # Collect templates
    templates_data: list[dict[str, Any]] = []

    # Local templates (builtin + user)
    for template in loader.list_local(category):
        templates_data.append({
            "id": template.metadata.id,
            "name": template.metadata.display_name,
            "description": template.metadata.description,
            "category": template.metadata.category,
            "author": template.metadata.author,
            "version": template.metadata.version,
            "source": "builtin" if loader.is_builtin(template.metadata.id) else "local",
        })

    # Remote templates (unless local-only)
    if not local_only:
        try:
            for entry in loader.list_remote():
                # Skip if already in local list
                if any(t["id"] == entry.id for t in templates_data):
                    continue
                if category and entry.category != category:
                    continue
                templates_data.append({
                    "id": entry.id,
                    "name": entry.display_name,
                    "description": entry.description,
                    "category": entry.category,
                    "author": entry.author,
                    "version": entry.version,
                    "source": "remote",
                })
        except Exception as e:
            if not local_only:
                err_console.print(f"[dim]Note: Could not fetch remote templates: {e}[/dim]")

    if not templates_data:
        console.print("[dim]No templates found[/dim]")
        return

    if json_output:
        console.print(json.dumps(templates_data, indent=2))
        return

    # Group by category
    by_category: dict[str, list[dict[str, Any]]] = {}
    for t in templates_data:
        cat = t["category"]
        if cat not in by_category:
            by_category[cat] = []
        by_category[cat].append(t)

    for cat_name in sorted(by_category.keys()):
        cat_templates = by_category[cat_name]
        table = Table(title=f"[bold]{cat_name}[/bold]", show_header=True)
        table.add_column("ID", style="cyan")
        table.add_column("Name")
        table.add_column("Description")
        table.add_column("Source", style="dim")

        for t in sorted(cat_templates, key=lambda x: x["id"]):
            desc = t["description"]
            if len(desc) > 50:
                desc = desc[:47] + "..."
            table.add_row(t["id"], t["name"], desc, t["source"])

        console.print(table)
        console.print()


@template_app.command(name="search")
def template_search(
    query: Annotated[str, typer.Argument(help="Search query")],
    category: Annotated[
        str | None,
        typer.Option("-c", "--category", help="Filter by category"),
    ] = None,
    json_output: Annotated[
        bool,
        typer.Option("-j", "--json", help="Output as JSON"),
    ] = False,
) -> None:
    """Search templates by query."""
    from gaol.templates import get_template_loader

    loader = get_template_loader()
    results = loader.search(query, category)

    if not results:
        console.print(f"[dim]No templates found matching '{query}'[/dim]")
        return

    if json_output:
        data = [
            {
                "id": r.id,
                "name": r.display_name,
                "description": r.description,
                "category": r.category,
                "tags": r.tags,
                "author": r.author,
            }
            for r in results
        ]
        console.print(json.dumps(data, indent=2))
        return

    table = Table(title=f"Search Results for '{query}'")
    table.add_column("ID", style="cyan")
    table.add_column("Name")
    table.add_column("Description")
    table.add_column("Tags", style="dim")

    for entry in results:
        desc = entry.description
        if len(desc) > 40:
            desc = desc[:37] + "..."
        tags = ", ".join(entry.tags[:3])
        if len(entry.tags) > 3:
            tags += "..."
        table.add_row(entry.id, entry.display_name, desc, tags)

    console.print(table)


@template_app.command(name="info")
def template_info(
    template_id: Annotated[str, typer.Argument(help="Template ID (e.g., web-dev/django)")],
    json_output: Annotated[
        bool,
        typer.Option("-j", "--json", help="Output as JSON"),
    ] = False,
) -> None:
    """Show template details."""
    from gaol.templates import TemplateNotFoundError, get_template_loader

    loader = get_template_loader()

    try:
        template = loader.get(template_id)
    except TemplateNotFoundError as e:
        err_console.print(f"[red]Template not found:[/red] {template_id}")
        err_console.print(f"[dim]Searched: {', '.join(e.searched_locations)}[/dim]")
        raise typer.Exit(1) from None

    if json_output:
        console.print(json.dumps(template.model_dump(mode="json"), indent=2))
        return

    meta = template.metadata
    icon = meta.icon or ""

    console.print(f"[bold cyan]{icon} {meta.display_name}[/bold cyan]")
    console.print(f"[dim]ID: {meta.id}[/dim]")
    console.print()
    console.print(f"[bold]Description:[/bold] {meta.description}")
    console.print(f"[bold]Category:[/bold] {meta.category}")
    console.print(f"[bold]Author:[/bold] {meta.author}")
    console.print(f"[bold]Version:[/bold] {meta.version}")
    console.print(f"[bold]License:[/bold] {meta.license}")

    if meta.tags:
        console.print(f"[bold]Tags:[/bold] {', '.join(meta.tags)}")

    console.print()
    console.print("[bold]Container Spec:[/bold]")
    spec = template.spec
    console.print(f"  Runtime: {spec.runtime}")
    console.print(f"  Distro: {spec.distro}")
    if spec.profiles:
        console.print(f"  Profiles: {', '.join(spec.profiles)}")
    if spec.network and spec.network.ports:
        console.print(f"  Ports: {', '.join(spec.network.ports)}")
    if spec.mounts:
        console.print(f"  Mounts: {len(spec.mounts)} configured")

    if template.post_create_commands:
        console.print()
        console.print("[bold]Post-create commands:[/bold]")
        for cmd in template.post_create_commands:
            console.print(f"  - {cmd}")

    if template.example_env:
        console.print()
        console.print("[bold]Example environment variables:[/bold]")
        for key, val in template.example_env.items():
            console.print(f"  {key}={val}")

    if template.readme:
        console.print()
        console.print("[bold]README:[/bold]")
        console.print(template.readme)


@template_app.command(name="use")
def template_use(
    template_id: Annotated[str, typer.Argument(help="Template ID (e.g., web-dev/django)")],
    name: Annotated[str, typer.Argument(help="Container name")],
    runtime: Annotated[
        str | None,
        typer.Option("-r", "--runtime", help="Override runtime (docker, podman, etc.)"),
    ] = None,
    start: Annotated[
        bool,
        typer.Option("-s", "--start", help="Start container after creation"),
    ] = False,
    env_file: Annotated[
        Path | None,
        typer.Option("-e", "--env-file", help="Environment file"),
    ] = None,
) -> None:
    """Create a container from a template."""
    from gaol.profiles import get_profile_applicator
    from gaol.runtimes import get_runtime
    from gaol.templates import TemplateNotFoundError, get_template_loader

    loader = get_template_loader()

    try:
        template = loader.get(template_id)
    except TemplateNotFoundError as e:
        err_console.print(f"[red]Template not found:[/red] {template_id}")
        err_console.print(f"[dim]Searched: {', '.join(e.searched_locations)}[/dim]")
        raise typer.Exit(1) from None

    console.print(f"[bold]Creating container from template:[/bold] {template.metadata.display_name}")

    # Build container config from template spec
    spec = template.spec.model_copy(deep=True)
    spec.name = name

    # Override runtime if specified
    if runtime:
        spec.runtime = runtime

    # Load environment from file if specified
    if env_file:
        if not env_file.exists():
            err_console.print(f"[red]Environment file not found:[/red] {env_file}")
            raise typer.Exit(1) from None
        env_vars = {}
        for line in env_file.read_text().splitlines():
            line = line.strip()
            if line and not line.startswith("#") and "=" in line:
                key, _, value = line.partition("=")
                env_vars[key.strip()] = value.strip()
        if spec.environment is None:
            spec.environment = {}
        spec.environment.update(env_vars)

    try:
        rt = get_runtime(spec.runtime)

        # Apply profiles
        if spec.profiles:
            applicator = get_profile_applicator()
            spec = applicator.apply_to_config(spec, spec.profiles)

        # Create container
        container_id = rt.create(spec)
        console.print(f"[green]Created container:[/green] {name} ({container_id[:12]})")

        # Run post-create commands
        if template.post_create_commands:
            console.print("[bold]Running post-create commands...[/bold]")
            # Start container temporarily for commands
            rt.start(container_id)
            for cmd in template.post_create_commands:
                console.print(f"  [dim]$ {cmd}[/dim]")
                try:
                    result = rt.exec(container_id, cmd.split())
                    if result.returncode != 0:
                        err_console.print(f"[yellow]Warning: Command failed with exit code {result.returncode}[/yellow]")
                except Exception as e:
                    err_console.print(f"[yellow]Warning: Failed to run command: {e}[/yellow]")

            if not start:
                rt.stop(container_id)

        # Start if requested (and not already started for post-create)
        if start and not template.post_create_commands:
            rt.start(container_id)
            console.print(f"[green]Started container:[/green] {name}")

        console.print()
        console.print(f"[dim]Use 'gaol exec {name} -- bash' to enter the container[/dim]")

    except Exception as e:
        err_console.print(f"[red]Failed to create container:[/red] {e}")
        raise typer.Exit(1) from None


@template_app.command(name="create")
def template_create(
    container: Annotated[str, typer.Argument(help="Container name to create template from")],
    template_id: Annotated[
        str,
        typer.Option("-i", "--id", help="Template ID (e.g., custom/my-template)"),
    ],
    description: Annotated[
        str | None,
        typer.Option("-d", "--description", help="Template description"),
    ] = None,
    category: Annotated[
        str,
        typer.Option("-c", "--category", help="Template category"),
    ] = "custom",
    output: Annotated[
        Path | None,
        typer.Option("-o", "--output", help="Output file (default: save to local store)"),
    ] = None,
) -> None:
    """Create a template from an existing container."""
    from gaol.models import ContainerSpec
    from gaol.runtimes import get_runtime
    from gaol.templates import (
        Template,
        TemplateExistsError,
        TemplateMetadata,
        get_template_store,
    )

    # Get container info
    rt = get_runtime()
    containers = rt.list()

    container_info = None
    for c in containers:
        if c.name == container or c.id.startswith(container):
            container_info = c
            break

    if not container_info:
        err_console.print(f"[red]Container not found:[/red] {container}")
        raise typer.Exit(1) from None

    # Get container config/spec
    try:
        config = rt.inspect(container_info.id)
    except Exception as e:
        err_console.print(f"[red]Failed to inspect container:[/red] {e}")
        raise typer.Exit(1) from None

    # Build spec from config
    spec_data: dict[str, Any] = {
        "name": "{{name}}",  # Placeholder
        "runtime": rt.name,
        "distro": config.get("distro", "trixie"),
    }

    # Extract relevant config
    if "environment" in config and config["environment"]:
        spec_data["environment"] = config["environment"]
    if "mounts" in config and config["mounts"]:
        spec_data["mounts"] = config["mounts"]
    if "network" in config and config["network"]:
        spec_data["network"] = config["network"]

    spec = ContainerSpec(**spec_data)

    # Create template
    display_name = template_id.split("/")[-1].replace("-", " ").replace("_", " ").title()
    metadata = TemplateMetadata(
        id=template_id,
        display_name=display_name,
        description=description or f"Template created from container {container}",
        category=category,
        author="user",
        version="1.0.0",
    )

    template = Template(metadata=metadata, spec=spec)

    if output:
        # Save to file
        import yaml
        output.write_text(yaml.safe_dump(template.model_dump(mode="json"), sort_keys=False))
        console.print(f"[green]Template saved to:[/green] {output}")
    else:
        # Save to local store
        store = get_template_store()
        try:
            path = store.save(template)
            console.print(f"[green]Template saved:[/green] {template_id}")
            console.print(f"[dim]Location: {path}[/dim]")
        except TemplateExistsError:
            err_console.print(f"[red]Template already exists:[/red] {template_id}")
            err_console.print("[dim]Use --output to save to a different location[/dim]")
            raise typer.Exit(1) from None


@template_app.command(name="categories")
def template_categories(
    json_output: Annotated[
        bool,
        typer.Option("-j", "--json", help="Output as JSON"),
    ] = False,
) -> None:
    """List available template categories."""
    from gaol.templates import get_template_loader

    loader = get_template_loader()
    categories = loader.get_categories()

    if not categories:
        console.print("[dim]No categories found[/dim]")
        return

    if json_output:
        data = [
            {
                "name": c.name,
                "display_name": c.display_name,
                "description": c.description,
                "icon": c.icon,
            }
            for c in categories
        ]
        console.print(json.dumps(data, indent=2))
        return

    table = Table(title="Template Categories")
    table.add_column("Name", style="cyan")
    table.add_column("Display Name")
    table.add_column("Description")

    for cat in categories:
        icon = cat.icon or ""
        display = f"{icon} {cat.display_name}" if icon else cat.display_name
        table.add_row(cat.name, display, cat.description or "-")

    console.print(table)


@template_app.command(name="refresh")
def template_refresh() -> None:
    """Refresh the remote gallery index."""
    from gaol.templates import GalleryOfflineError, get_template_loader

    loader = get_template_loader()

    console.print("[bold]Refreshing gallery index...[/bold]")

    try:
        entries = loader.list_remote(refresh=True)
        console.print(f"[green]Gallery index updated:[/green] {len(entries)} templates available")
    except GalleryOfflineError as e:
        err_console.print(f"[red]Failed to refresh gallery:[/red] {e}")
        raise typer.Exit(1) from None


@template_app.command(name="delete")
def template_delete(
    template_id: Annotated[str, typer.Argument(help="Template ID to delete")],
    force: Annotated[
        bool,
        typer.Option("-f", "--force", help="Skip confirmation"),
    ] = False,
) -> None:
    """Delete a user template."""
    from gaol.templates import get_template_loader, get_template_store

    loader = get_template_loader()
    store = get_template_store()

    # Check if it's a builtin template
    if loader.is_builtin(template_id):
        err_console.print(f"[red]Cannot delete built-in template:[/red] {template_id}")
        raise typer.Exit(1) from None

    # Check if it exists
    if not store.exists(template_id):
        err_console.print(f"[red]Template not found:[/red] {template_id}")
        raise typer.Exit(1) from None

    if not force:
        confirm = typer.confirm(f"Delete template '{template_id}'?")
        if not confirm:
            console.print("[dim]Aborted[/dim]")
            return

    if store.delete(template_id):
        console.print(f"[green]Deleted template:[/green] {template_id}")
    else:
        err_console.print(f"[red]Failed to delete template:[/red] {template_id}")
        raise typer.Exit(1) from None


# =============================================================================
# Nix Commands
# =============================================================================

nix_app = typer.Typer(help="Nix package manager integration")
app.add_typer(nix_app, name="nix", rich_help_panel=PANEL_DEVELOPMENT)

nix_flake_app = typer.Typer(help="Nix flake management")
nix_app.add_typer(nix_flake_app, name="flake")


@nix_app.command(name="init")
def nix_init(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    channel: Annotated[
        str,
        typer.Option("--channel", "-c", help="Nix channel to configure"),
    ] = "nixpkgs-unstable",
    daemon: Annotated[
        bool,
        typer.Option("--daemon", help="Use daemon (multi-user) mode"),
    ] = False,
    no_flakes: Annotated[
        bool,
        typer.Option("--no-flakes", help="Disable flakes experimental feature"),
    ] = False,
) -> None:
    """Initialize Nix in a container."""
    from gaol.nix import get_nix_installer
    from gaol.runtimes import get_runtime

    runtime = get_runtime(default_runtime)
    installer = get_nix_installer()

    # Check if already installed
    if installer.is_installed(runtime, container):
        console.print(f"[yellow]Nix is already installed in {container}[/yellow]")
        return

    console.print(f"[bold]Installing Nix in {container}...[/bold]")
    console.print(f"  Channel: {channel}")
    console.print(f"  Mode: {'daemon' if daemon else 'single-user'}")
    console.print(f"  Flakes: {'disabled' if no_flakes else 'enabled'}")

    try:
        installer.install(
            runtime,
            container,
            daemon=daemon,
            channel=channel,
            enable_flakes=not no_flakes,
        )
        console.print(f"[green]Nix installed successfully in {container}[/green]")
    except Exception as e:
        err_console.print(f"[red]Failed to install Nix:[/red] {e}")
        raise typer.Exit(1) from None


@nix_app.command(name="status")
def nix_status(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    json_output: Annotated[
        bool,
        typer.Option("--json", help="Output as JSON"),
    ] = False,
) -> None:
    """Show Nix installation status in a container."""
    from gaol.nix import get_nix_installer
    from gaol.runtimes import get_runtime

    runtime = get_runtime(default_runtime)
    installer = get_nix_installer()

    status = installer.get_status(runtime, container)

    if json_output:
        import json

        console.print(json.dumps(status.to_dict(), indent=2))
    else:
        if not status.installed:
            console.print(f"[yellow]Nix is not installed in {container}[/yellow]")
            console.print("Run 'gaol nix init' to install it.")
            return

        console.print(f"[bold]Nix Status for {container}[/bold]")
        console.print(f"  Version: {status.version or 'unknown'}")
        console.print(f"  Channel: {status.channel or 'none'}")
        console.print(f"  Mode: {'daemon' if status.daemon_mode else 'single-user'}")
        console.print(f"  Flakes: {'enabled' if status.flake_support else 'disabled'}")
        console.print(f"  Store size: {status.store_size / (1024*1024):.1f} MB")
        console.print(f"  Packages: {status.num_packages}")


@nix_app.command(name="shell")
def nix_shell(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    packages: Annotated[
        list[str],
        typer.Option("-p", "--package", help="Package to include (can be repeated)"),
    ] = None,
    pure: Annotated[
        bool,
        typer.Option("--pure", help="Use pure mode (no system packages)"),
    ] = False,
    command: Annotated[
        str | None,
        typer.Option("--command", "-c", help="Command to run"),
    ] = None,
) -> None:
    """Run a nix-shell with specified packages."""
    from gaol.nix import NixNotInstalledError, get_nix_shell
    from gaol.runtimes import get_runtime

    if not packages:
        err_console.print("[red]At least one package is required[/red]")
        err_console.print("Use: gaol nix shell CONTAINER -p python311 -p nodejs")
        raise typer.Exit(1) from None

    runtime = get_runtime(default_runtime)
    shell = get_nix_shell()

    try:
        exit_code, stdout, stderr = shell.run_shell(
            runtime,
            container,
            packages=packages,
            pure=pure,
            command=command,
        )

        if stdout:
            console.print(stdout, end="")
        if stderr:
            err_console.print(stderr, end="")

        if exit_code != 0:
            raise typer.Exit(exit_code) from None

    except NixNotInstalledError as e:
        err_console.print(f"[red]{e}[/red]")
        raise typer.Exit(1) from None


@nix_app.command(name="install")
def nix_install_packages(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    packages: Annotated[
        list[str],
        typer.Argument(help="Packages to install"),
    ],
) -> None:
    """Install packages permanently via nix-env."""
    from gaol.nix import NixNotInstalledError, NixPackageError, get_nix_shell
    from gaol.runtimes import get_runtime

    runtime = get_runtime(default_runtime)
    shell = get_nix_shell()

    console.print(f"[bold]Installing packages via Nix:[/bold] {', '.join(packages)}")

    try:
        exit_code, stdout, stderr = shell.install_packages(
            runtime,
            container,
            packages=packages,
        )

        if stdout:
            console.print(stdout, end="")

        console.print(f"[green]Installed {len(packages)} package(s)[/green]")

    except NixNotInstalledError as e:
        err_console.print(f"[red]{e}[/red]")
        raise typer.Exit(1) from None
    except NixPackageError as e:
        err_console.print(f"[red]Package installation failed:[/red] {e}")
        raise typer.Exit(1) from None


@nix_app.command(name="uninstall")
def nix_uninstall_packages(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    packages: Annotated[
        list[str],
        typer.Argument(help="Packages to uninstall"),
    ],
) -> None:
    """Uninstall packages via nix-env."""
    from gaol.nix import NixNotInstalledError, get_nix_shell
    from gaol.runtimes import get_runtime

    runtime = get_runtime(default_runtime)
    shell = get_nix_shell()

    console.print(f"[bold]Uninstalling packages:[/bold] {', '.join(packages)}")

    try:
        exit_code, stdout, stderr = shell.uninstall_packages(
            runtime,
            container,
            packages=packages,
        )

        if stdout:
            console.print(stdout, end="")

        console.print(f"[green]Uninstalled {len(packages)} package(s)[/green]")

    except NixNotInstalledError as e:
        err_console.print(f"[red]{e}[/red]")
        raise typer.Exit(1) from None


@nix_app.command(name="list")
def nix_list_packages(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
) -> None:
    """List installed Nix packages."""
    from gaol.nix import NixNotInstalledError, get_nix_shell
    from gaol.runtimes import get_runtime

    runtime = get_runtime(default_runtime)
    shell = get_nix_shell()

    try:
        packages = shell.list_installed(runtime, container)

        if not packages:
            console.print("[dim]No packages installed via nix-env[/dim]")
            return

        console.print(f"[bold]Installed packages ({len(packages)}):[/bold]")
        for pkg in packages:
            console.print(f"  {pkg}")

    except NixNotInstalledError as e:
        err_console.print(f"[red]{e}[/red]")
        raise typer.Exit(1) from None


@nix_app.command(name="search")
def nix_search(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    query: Annotated[str, typer.Argument(help="Search query")],
    limit: Annotated[
        int,
        typer.Option("--limit", "-n", help="Maximum results"),
    ] = 20,
) -> None:
    """Search for packages in nixpkgs."""
    from gaol.nix import NixNotInstalledError, get_nix_shell
    from gaol.runtimes import get_runtime

    runtime = get_runtime(default_runtime)
    shell = get_nix_shell()

    try:
        results = shell.search_packages(runtime, container, query, limit=limit)

        if not results:
            console.print(f"[dim]No packages found matching '{query}'[/dim]")
            return

        console.print(f"[bold]Search results for '{query}':[/bold]")
        for pkg in results:
            version = f" ({pkg['version']})" if pkg.get("version") else ""
            desc = f" - {pkg['description']}" if pkg.get("description") else ""
            console.print(f"  [cyan]{pkg['name']}[/cyan]{version}{desc}")

    except NixNotInstalledError as e:
        err_console.print(f"[red]{e}[/red]")
        raise typer.Exit(1) from None


@nix_app.command(name="gc")
def nix_gc(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    delete_older_than: Annotated[
        str | None,
        typer.Option("--delete-older-than", "-d", help="Delete generations older than (e.g., 30d)"),
    ] = None,
) -> None:
    """Run Nix garbage collection."""
    from gaol.nix import NixNotInstalledError, get_nix_installer
    from gaol.runtimes import get_runtime

    runtime = get_runtime(default_runtime)
    installer = get_nix_installer()

    if not installer.is_installed(runtime, container):
        err_console.print(f"[red]Nix is not installed in {container}[/red]")
        raise typer.Exit(1) from None

    console.print("[bold]Running Nix garbage collection...[/bold]")

    freed, output = installer.garbage_collect(
        runtime,
        container,
        delete_older_than=delete_older_than,
    )

    console.print(output)
    if freed > 0:
        console.print(f"[green]Freed {freed / (1024*1024):.1f} MB[/green]")


# Flake subcommands


@nix_flake_app.command(name="init")
def nix_flake_init(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    path: Annotated[
        str,
        typer.Option("--path", "-p", help="Path for the flake"),
    ] = "/app",
    packages: Annotated[
        list[str],
        typer.Option("--package", "-P", help="Package to include"),
    ] = None,
    description: Annotated[
        str,
        typer.Option("--description", "-d", help="Flake description"),
    ] = "Gaol development environment",
    nixpkgs_ref: Annotated[
        str,
        typer.Option("--nixpkgs", help="Nixpkgs reference"),
    ] = "github:NixOS/nixpkgs/nixos-24.11",
    force: Annotated[
        bool,
        typer.Option("--force", "-f", help="Overwrite existing flake"),
    ] = False,
) -> None:
    """Initialize a Nix flake in a container."""
    from gaol.nix import FlakeInitError, get_flake_manager
    from gaol.runtimes import get_runtime

    if not packages:
        packages = ["coreutils", "bash"]  # Minimal default

    runtime = get_runtime(default_runtime)
    flake_mgr = get_flake_manager()

    console.print(f"[bold]Initializing flake at {path}...[/bold]")
    console.print(f"  Packages: {', '.join(packages)}")

    try:
        flake_mgr.init_flake(
            runtime,
            container,
            path,
            packages=packages,
            description=description,
            nixpkgs_ref=nixpkgs_ref,
            force=force,
        )
        console.print(f"[green]Flake initialized at {path}[/green]")
        console.print("Run 'gaol nix flake develop' to enter the shell")

    except FlakeInitError as e:
        err_console.print(f"[red]{e}[/red]")
        raise typer.Exit(1) from None


@nix_flake_app.command(name="update")
def nix_flake_update(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    path: Annotated[
        str,
        typer.Option("--path", "-p", help="Path to the flake"),
    ] = "/app",
    input_name: Annotated[
        str | None,
        typer.Option("--input", "-i", help="Specific input to update"),
    ] = None,
) -> None:
    """Update flake.lock."""
    from gaol.nix import FlakeNotFoundError, FlakeUpdateError, get_flake_manager
    from gaol.runtimes import get_runtime

    runtime = get_runtime(default_runtime)
    flake_mgr = get_flake_manager()

    console.print(f"[bold]Updating flake at {path}...[/bold]")

    try:
        flake_mgr.update_flake(runtime, container, path, input_name=input_name)
        console.print("[green]Flake updated successfully[/green]")

    except FlakeNotFoundError as e:
        err_console.print(f"[red]{e}[/red]")
        raise typer.Exit(1) from None
    except FlakeUpdateError as e:
        err_console.print(f"[red]{e}[/red]")
        raise typer.Exit(1) from None


@nix_flake_app.command(name="develop")
def nix_flake_develop(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    path: Annotated[
        str,
        typer.Option("--path", "-p", help="Path to the flake"),
    ] = "/app",
    command: Annotated[
        str | None,
        typer.Option("--command", "-c", help="Command to run"),
    ] = None,
) -> None:
    """Enter flake development shell."""
    from gaol.nix import FlakeNotFoundError, get_flake_manager
    from gaol.runtimes import get_runtime

    runtime = get_runtime(default_runtime)
    flake_mgr = get_flake_manager()

    try:
        exit_code, stdout, stderr = flake_mgr.develop(
            runtime,
            container,
            path,
            command=command,
        )

        if stdout:
            console.print(stdout, end="")
        if stderr:
            err_console.print(stderr, end="")

        if exit_code != 0:
            raise typer.Exit(exit_code) from None

    except FlakeNotFoundError as e:
        err_console.print(f"[red]{e}[/red]")
        raise typer.Exit(1) from None


@nix_flake_app.command(name="check")
def nix_flake_check(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    path: Annotated[
        str,
        typer.Option("--path", "-p", help="Path to the flake"),
    ] = "/app",
) -> None:
    """Check flake status and validity."""
    from gaol.nix import get_flake_manager
    from gaol.runtimes import get_runtime

    runtime = get_runtime(default_runtime)
    flake_mgr = get_flake_manager()

    status = flake_mgr.check_flake(runtime, container, path)

    if not status["exists"]:
        console.print(f"[yellow]No flake.nix found at {path}[/yellow]")
        console.print("Run 'gaol nix flake init' to create one")
        return

    console.print(f"[bold]Flake status at {path}:[/bold]")
    console.print(f"  flake.nix: [green]exists[/green]")
    console.print(
        f"  flake.lock: {'[green]exists[/green]' if status['has_lock'] else '[yellow]missing[/yellow]'}"
    )
    console.print(
        f"  Valid: {'[green]yes[/green]' if status['valid'] else '[red]no[/red]'}"
    )

    if status.get("error"):
        console.print(f"  Error: {status['error']}")


@nix_flake_app.command(name="show")
def nix_flake_show(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    path: Annotated[
        str,
        typer.Option("--path", "-p", help="Path to the flake"),
    ] = "/app",
    json_output: Annotated[
        bool,
        typer.Option("--json", help="Output as JSON"),
    ] = False,
) -> None:
    """Show flake metadata."""
    from gaol.nix import get_flake_manager
    from gaol.runtimes import get_runtime

    runtime = get_runtime(default_runtime)
    flake_mgr = get_flake_manager()

    metadata = flake_mgr.show_flake(runtime, container, path)

    if not metadata:
        err_console.print(f"[red]No valid flake at {path}[/red]")
        raise typer.Exit(1) from None

    if json_output:
        import json

        console.print(json.dumps(metadata, indent=2))
    else:
        console.print(f"[bold]Flake at {path}:[/bold]")
        for key, value in metadata.items():
            if isinstance(value, dict):
                console.print(f"  {key}:")
                for k, v in value.items():
                    console.print(f"    {k}: {v}")
            else:
                console.print(f"  {key}: {value}")


# =============================================================================
# Service management commands
# =============================================================================


@service_app.command(name="add")
def service_add(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    name: Annotated[str, typer.Option("--name", "-n", help="Service name")],
    command: Annotated[str, typer.Option("--command", "-c", help="Command to run")],
    user: Annotated[
        str,
        typer.Option("--user", "-u", help="User to run the service as"),
    ] = "root",
    workdir: Annotated[
        str | None,
        typer.Option("--workdir", "-w", help="Working directory for the service"),
    ] = None,
    description: Annotated[
        str | None,
        typer.Option("--description", "-d", help="Service description"),
    ] = None,
    restart: Annotated[
        str,
        typer.Option("--restart", "-r", help="Restart policy (no, always, on-failure)"),
    ] = "on-failure",
    environment: Annotated[
        list[str] | None,
        typer.Option("--env", "-e", help="Environment variables (KEY=VALUE)"),
    ] = None,
    enable: Annotated[
        bool,
        typer.Option("--enable", help="Enable service to start on boot"),
    ] = True,
    start: Annotated[
        bool,
        typer.Option("--start", help="Start the service immediately"),
    ] = True,
    runtime: Annotated[
        str | None,
        typer.Option("--runtime", help="Container runtime to use"),
    ] = None,
) -> None:
    """Add a systemd service to a container.

    Creates a systemd service unit file in the container that runs the specified
    command. The service can be configured to start on boot and restart on failure.

    Examples:
        gaol service add mycontainer --name web --command "python -m http.server 8000"
        gaol service add mycontainer -n app -c "node server.js" -u node -w /app
        gaol service add mycontainer -n worker -c "./worker.sh" --restart always -e PORT=3000
    """
    rt = get_runtime(runtime)

    # Check if container exists and is running
    try:
        status = rt.status(container)
    except RuntimeError as e:
        err_console.print(f"[red]Error: {e}[/red]")
        raise typer.Exit(1) from e

    if status.status != ContainerStatus.RUNNING:
        err_console.print(f"[red]Container '{container}' is not running[/red]")
        err_console.print("Start it first with: gaol start {container}")
        raise typer.Exit(1)

    # Build the service unit file content
    service_content = f"""[Unit]
Description={description or f'{name} service'}
After=network.target

[Service]
Type=simple
User={user}
"""
    if workdir:
        service_content += f"WorkingDirectory={workdir}\n"

    # Add environment variables
    if environment:
        for env_var in environment:
            service_content += f"Environment={env_var}\n"

    service_content += f"""ExecStart={command}
Restart={restart}
RestartSec=5

[Install]
WantedBy=multi-user.target
"""

    # Write service file and manage service in container
    service_path = f"/etc/systemd/system/{name}.service"

    # Create a script to set up the service
    setup_script = f"""cat > {service_path} << 'SERVICEEOF'
{service_content}
SERVICEEOF
systemctl daemon-reload
"""
    if enable:
        setup_script += f"systemctl enable {name}.service\n"

    if start:
        setup_script += f"systemctl start {name}.service\n"

    # Execute the setup script in the container
    exit_code, stdout, stderr = rt.exec(container, ["/bin/bash", "-c", setup_script])

    if exit_code != 0:
        err_console.print(f"[red]Failed to create service:[/red]")
        if stderr:
            err_console.print(stderr)
        raise typer.Exit(1)

    console.print(f"[green]✓[/green] Service '{name}' created in container '{container}'")

    if enable:
        console.print(f"[green]✓[/green] Service enabled to start on boot")

    if start:
        # Check if service started successfully
        exit_code, stdout, _ = rt.exec(
            container, ["systemctl", "is-active", f"{name}.service"]
        )
        if exit_code == 0 and "active" in stdout:
            console.print(f"[green]✓[/green] Service started successfully")
        else:
            console.print(f"[yellow]![/yellow] Service may not have started - check with: gaol service status {container} {name}")


@service_app.command(name="remove")
def service_remove(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    name: Annotated[str, typer.Argument(help="Service name")],
    force: Annotated[
        bool,
        typer.Option("--force", "-f", help="Remove even if service is running"),
    ] = False,
    runtime: Annotated[
        str | None,
        typer.Option("--runtime", help="Container runtime to use"),
    ] = None,
) -> None:
    """Remove a systemd service from a container."""
    rt = get_runtime(runtime)

    try:
        status = rt.status(container)
    except RuntimeError as e:
        err_console.print(f"[red]Error: {e}[/red]")
        raise typer.Exit(1) from e

    if status.status != ContainerStatus.RUNNING:
        err_console.print(f"[red]Container '{container}' is not running[/red]")
        raise typer.Exit(1)

    # Stop and disable service, then remove the unit file
    commands = []
    if force:
        commands.append(f"systemctl stop {name}.service 2>/dev/null || true")

    commands.extend([
        f"systemctl disable {name}.service 2>/dev/null || true",
        f"rm -f /etc/systemd/system/{name}.service",
        "systemctl daemon-reload",
    ])

    script = " && ".join(commands)
    exit_code, _, stderr = rt.exec(container, ["/bin/bash", "-c", script])

    if exit_code != 0:
        err_console.print(f"[red]Failed to remove service:[/red]")
        if stderr:
            err_console.print(stderr)
        raise typer.Exit(1)

    console.print(f"[green]✓[/green] Service '{name}' removed from container '{container}'")


@service_app.command(name="list")
def service_list(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    all_services: Annotated[
        bool,
        typer.Option("--all", "-a", help="Show all services, not just user-defined"),
    ] = False,
    runtime: Annotated[
        str | None,
        typer.Option("--runtime", help="Container runtime to use"),
    ] = None,
) -> None:
    """List systemd services in a container."""
    rt = get_runtime(runtime)

    try:
        status = rt.status(container)
    except RuntimeError as e:
        err_console.print(f"[red]Error: {e}[/red]")
        raise typer.Exit(1) from e

    if status.status != ContainerStatus.RUNNING:
        err_console.print(f"[red]Container '{container}' is not running[/red]")
        raise typer.Exit(1)

    # List services
    if all_services:
        cmd = ["systemctl", "list-units", "--type=service", "--no-pager", "--plain"]
    else:
        # Only show services in /etc/systemd/system (user-defined)
        # Use absolute path for bash as required by machinectl shell
        cmd = ["/bin/bash", "-c", "ls /etc/systemd/system/*.service 2>/dev/null | xargs -I {} basename {} .service"]

    exit_code, stdout, _ = rt.exec(container, cmd)

    if all_services:
        console.print(stdout)
    else:
        # Filter out machinectl connection messages from nspawn runtime
        services = [
            s.strip() for s in stdout.strip().split("\n")
            if s.strip()
            and not s.startswith("Connected to machine")
            and not s.startswith("Connection to machine")
        ]
        if not services:
            console.print("No user-defined services found")
            return

        table = Table(title=f"Services in '{container}'")
        table.add_column("Name", style="cyan")
        table.add_column("Status", style="green")
        table.add_column("Enabled", style="yellow")

        for svc in services:
            # Get status (is-active returns non-zero for inactive, so check stdout)
            _, active_stdout, _ = rt.exec(
                container, ["/usr/bin/systemctl", "is-active", f"{svc}.service"]
            )
            active = active_stdout.strip().split("\n")[0] if active_stdout.strip() else "unknown"
            # Filter out machinectl connection messages
            if active.startswith("Connected to machine") or active.startswith("Connection to machine"):
                active = "unknown"

            # Get enabled status (is-enabled returns non-zero for disabled, so check stdout)
            _, enabled_stdout, _ = rt.exec(
                container, ["/usr/bin/systemctl", "is-enabled", f"{svc}.service"]
            )
            enabled = enabled_stdout.strip().split("\n")[0] if enabled_stdout.strip() else "unknown"
            # Filter out machinectl connection messages
            if enabled.startswith("Connected to machine") or enabled.startswith("Connection to machine"):
                enabled = "unknown"

            status_style = "green" if active == "active" else "red" if active == "inactive" else "yellow"
            enabled_style = "green" if enabled == "enabled" else "dim"

            table.add_row(svc, f"[{status_style}]{active}[/]", f"[{enabled_style}]{enabled}[/]")

        console.print(table)


@service_app.command(name="status")
def service_status(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    name: Annotated[str, typer.Argument(help="Service name")],
    runtime: Annotated[
        str | None,
        typer.Option("--runtime", help="Container runtime to use"),
    ] = None,
) -> None:
    """Get the status of a service in a container."""
    rt = get_runtime(runtime)

    try:
        status = rt.status(container)
    except RuntimeError as e:
        err_console.print(f"[red]Error: {e}[/red]")
        raise typer.Exit(1) from e

    if status.status != ContainerStatus.RUNNING:
        err_console.print(f"[red]Container '{container}' is not running[/red]")
        raise typer.Exit(1)

    exit_code, stdout, stderr = rt.exec(
        container, ["systemctl", "status", f"{name}.service", "--no-pager"]
    )

    console.print(stdout)
    if stderr:
        err_console.print(stderr)


@service_app.command(name="start")
def service_start(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    name: Annotated[str, typer.Argument(help="Service name")],
    runtime: Annotated[
        str | None,
        typer.Option("--runtime", help="Container runtime to use"),
    ] = None,
) -> None:
    """Start a service in a container."""
    rt = get_runtime(runtime)

    try:
        status = rt.status(container)
    except RuntimeError as e:
        err_console.print(f"[red]Error: {e}[/red]")
        raise typer.Exit(1) from e

    if status.status != ContainerStatus.RUNNING:
        err_console.print(f"[red]Container '{container}' is not running[/red]")
        raise typer.Exit(1)

    exit_code, _, stderr = rt.exec(container, ["systemctl", "start", f"{name}.service"])

    if exit_code != 0:
        err_console.print(f"[red]Failed to start service:[/red]")
        if stderr:
            err_console.print(stderr)
        raise typer.Exit(1)

    console.print(f"[green]✓[/green] Service '{name}' started")


@service_app.command(name="stop")
def service_stop(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    name: Annotated[str, typer.Argument(help="Service name")],
    runtime: Annotated[
        str | None,
        typer.Option("--runtime", help="Container runtime to use"),
    ] = None,
) -> None:
    """Stop a service in a container."""
    rt = get_runtime(runtime)

    try:
        status = rt.status(container)
    except RuntimeError as e:
        err_console.print(f"[red]Error: {e}[/red]")
        raise typer.Exit(1) from e

    if status.status != ContainerStatus.RUNNING:
        err_console.print(f"[red]Container '{container}' is not running[/red]")
        raise typer.Exit(1)

    exit_code, _, stderr = rt.exec(container, ["systemctl", "stop", f"{name}.service"])

    if exit_code != 0:
        err_console.print(f"[red]Failed to stop service:[/red]")
        if stderr:
            err_console.print(stderr)
        raise typer.Exit(1)

    console.print(f"[green]✓[/green] Service '{name}' stopped")


@service_app.command(name="restart")
def service_restart(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    name: Annotated[str, typer.Argument(help="Service name")],
    runtime: Annotated[
        str | None,
        typer.Option("--runtime", help="Container runtime to use"),
    ] = None,
) -> None:
    """Restart a service in a container."""
    rt = get_runtime(runtime)

    try:
        status = rt.status(container)
    except RuntimeError as e:
        err_console.print(f"[red]Error: {e}[/red]")
        raise typer.Exit(1) from e

    if status.status != ContainerStatus.RUNNING:
        err_console.print(f"[red]Container '{container}' is not running[/red]")
        raise typer.Exit(1)

    exit_code, _, stderr = rt.exec(container, ["systemctl", "restart", f"{name}.service"])

    if exit_code != 0:
        err_console.print(f"[red]Failed to restart service:[/red]")
        if stderr:
            err_console.print(stderr)
        raise typer.Exit(1)

    console.print(f"[green]✓[/green] Service '{name}' restarted")


@service_app.command(name="enable")
def service_enable(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    name: Annotated[str, typer.Argument(help="Service name")],
    runtime: Annotated[
        str | None,
        typer.Option("--runtime", help="Container runtime to use"),
    ] = None,
) -> None:
    """Enable a service to start on boot in a container."""
    rt = get_runtime(runtime)

    try:
        status = rt.status(container)
    except RuntimeError as e:
        err_console.print(f"[red]Error: {e}[/red]")
        raise typer.Exit(1) from e

    if status.status != ContainerStatus.RUNNING:
        err_console.print(f"[red]Container '{container}' is not running[/red]")
        raise typer.Exit(1)

    exit_code, _, stderr = rt.exec(container, ["systemctl", "enable", f"{name}.service"])

    if exit_code != 0:
        err_console.print(f"[red]Failed to enable service:[/red]")
        if stderr:
            err_console.print(stderr)
        raise typer.Exit(1)

    console.print(f"[green]✓[/green] Service '{name}' enabled")


@service_app.command(name="disable")
def service_disable(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    name: Annotated[str, typer.Argument(help="Service name")],
    runtime: Annotated[
        str | None,
        typer.Option("--runtime", help="Container runtime to use"),
    ] = None,
) -> None:
    """Disable a service from starting on boot in a container."""
    rt = get_runtime(runtime)

    try:
        status = rt.status(container)
    except RuntimeError as e:
        err_console.print(f"[red]Error: {e}[/red]")
        raise typer.Exit(1) from e

    if status.status != ContainerStatus.RUNNING:
        err_console.print(f"[red]Container '{container}' is not running[/red]")
        raise typer.Exit(1)

    exit_code, _, stderr = rt.exec(container, ["systemctl", "disable", f"{name}.service"])

    if exit_code != 0:
        err_console.print(f"[red]Failed to disable service:[/red]")
        if stderr:
            err_console.print(stderr)
        raise typer.Exit(1)

    console.print(f"[green]✓[/green] Service '{name}' disabled")


@service_app.command(name="logs")
def service_logs(
    container: Annotated[str, typer.Argument(help="Container name or ID")],
    name: Annotated[str, typer.Argument(help="Service name")],
    follow: Annotated[
        bool,
        typer.Option("--follow", "-f", help="Follow log output"),
    ] = False,
    lines: Annotated[
        int,
        typer.Option("--lines", "-n", help="Number of lines to show"),
    ] = 50,
    runtime: Annotated[
        str | None,
        typer.Option("--runtime", help="Container runtime to use"),
    ] = None,
) -> None:
    """View logs for a service in a container."""
    rt = get_runtime(runtime)

    try:
        status = rt.status(container)
    except RuntimeError as e:
        err_console.print(f"[red]Error: {e}[/red]")
        raise typer.Exit(1) from e

    if status.status != ContainerStatus.RUNNING:
        err_console.print(f"[red]Container '{container}' is not running[/red]")
        raise typer.Exit(1)

    cmd = ["journalctl", "-u", f"{name}.service", "-n", str(lines), "--no-pager"]
    if follow:
        cmd.append("-f")

    exit_code, stdout, stderr = rt.exec(container, cmd)

    console.print(stdout)
    if stderr:
        err_console.print(stderr)


@service_app.command(name="migrate")
def service_migrate(
    service_name: Annotated[str, typer.Argument(help="Name of the systemd service to migrate")],
    name: Annotated[
        str | None,
        typer.Option("--name", "-n", help="Container name (default: service name)"),
    ] = None,
    runtime: Annotated[
        str | None,
        typer.Option("--runtime", "-r", help="Target container runtime"),
    ] = None,
    distro: Annotated[
        str | None,
        typer.Option("--distro", "-d", help="Container distribution (default: trixie)"),
    ] = None,
    output: Annotated[
        Path | None,
        typer.Option("--output", "-o", help="Write spec to file instead of creating container"),
    ] = None,
    dry_run: Annotated[
        bool,
        typer.Option("--dry-run", help="Show analysis without creating anything"),
    ] = False,
    interactive: Annotated[
        bool,
        typer.Option("--interactive", "-i", help="Confirm discovered dependencies"),
    ] = False,
    copy_config: Annotated[
        bool,
        typer.Option("--copy-config/--no-copy-config", help="Copy config files into container"),
    ] = True,
    copy_data: Annotated[
        bool,
        typer.Option("--copy-data/--no-copy-data", help="Copy state directories into container"),
    ] = False,
    json_output: Annotated[
        bool,
        typer.Option("--json", "-j", help="Output as JSON"),
    ] = False,
) -> None:
    """Migrate a host systemd service into a container.

    Analyzes a systemd service, discovers its dependencies, and generates
    a container specification or creates a container to run the service.

    Examples:
        gaol service migrate nginx --dry-run
        gaol service migrate postgres --output postgres.yaml
        gaol service migrate redis -i
        gaol service migrate myapp --copy-config --copy-data
    """
    import json as json_lib

    from rich.panel import Panel
    from rich.table import Table

    from gaol.service_migration import (
        MigrationWarningSeverity,
        ServiceMigrator,
        ServiceNotFoundError,
        ServiceParseError,
        UnsupportedServiceError,
    )

    try:
        migrator = ServiceMigrator()

        # Analyze the service
        with console.status(f"Analyzing service '{service_name}'..."):
            analysis = migrator.analyze(service_name)

        if json_output:
            # JSON output mode
            data = {
                "service": analysis.service.name,
                "file": str(analysis.service.file_path),
                "description": analysis.service.unit.description,
                "can_migrate": analysis.can_migrate,
                "packages": analysis.dependencies.packages,
                "config_files": analysis.dependencies.config_files,
                "config_directories": analysis.dependencies.config_directories,
                "state_directories": analysis.dependencies.state_directories,
                "ports": analysis.dependencies.listening_ports,
                "warnings": [
                    {
                        "category": w.category,
                        "message": w.message,
                        "severity": w.severity.value,
                        "suggestion": w.suggestion,
                    }
                    for w in analysis.warnings
                ],
                "blockers": analysis.migration_blockers,
            }
            console.print(json_lib.dumps(data, indent=2))
            return

        # Display analysis
        console.print()
        console.print(Panel(f"[bold]Service Migration Analysis: {analysis.service.name}[/bold]"))
        console.print()

        # Basic info
        console.print(f"[bold]Service:[/bold] {analysis.service.name}")
        console.print(f"[bold]File:[/bold] {analysis.service.file_path}")
        if analysis.service.unit.description:
            console.print(f"[bold]Description:[/bold] {analysis.service.unit.description}")

        binary = analysis.service.get_binary_path()
        if binary:
            console.print(f"[bold]Binary:[/bold] {binary}")
        if analysis.service.service.exec_start:
            console.print(f"[bold]Command:[/bold] {analysis.service.service.exec_start}")

        console.print()

        # Dependencies table
        deps = analysis.dependencies
        if deps.packages or deps.config_files or deps.config_directories or deps.listening_ports:
            table = Table(title="Discovered Dependencies", show_header=True)
            table.add_column("Type", style="cyan")
            table.add_column("Value")

            for pkg in deps.packages:
                table.add_row("Package", pkg)
            for cf in deps.config_files:
                table.add_row("Config File", cf)
            for cd in deps.config_directories:
                table.add_row("Config Dir", cd)
            for sd in deps.state_directories:
                table.add_row("State Dir", sd)
            for port in deps.listening_ports:
                table.add_row("Port", str(port))

            console.print(table)
            console.print()

        # Warnings
        if analysis.warnings:
            console.print("[bold]Warnings:[/bold]")
            for warning in analysis.warnings:
                if warning.severity == MigrationWarningSeverity.CRITICAL:
                    style = "red bold"
                    prefix = "CRITICAL"
                elif warning.severity == MigrationWarningSeverity.WARNING:
                    style = "yellow"
                    prefix = "WARN"
                else:
                    style = "blue"
                    prefix = "INFO"

                console.print(f"  [{style}][{prefix}][/{style}] {warning.category}: {warning.message}")
                if warning.suggestion:
                    console.print(f"          [dim]→ {warning.suggestion}[/dim]")
            console.print()

        # Migration status
        if analysis.can_migrate:
            console.print("[green]✓ Service can be migrated[/green]")
        else:
            console.print("[red]✗ Service cannot be migrated[/red]")
            for blocker in analysis.migration_blockers:
                console.print(f"  [red]• {blocker}[/red]")
            raise typer.Exit(1)

        # Interactive confirmation
        if interactive and not dry_run:
            console.print()
            if not typer.confirm("Proceed with migration?"):
                console.print("[yellow]Migration cancelled[/yellow]")
                raise typer.Exit(0)

        # Dry run - stop here
        if dry_run:
            console.print()
            console.print("[dim]Dry run complete. Use --output to generate a spec file.[/dim]")
            return

        # Generate spec
        used_runtime = runtime or "nspawn"
        rt = get_runtime(used_runtime)
        migrator.runtime = rt

        spec = migrator.generate_spec(
            analysis=analysis,
            container_name=name,
            runtime=used_runtime,
            distro=distro,
            copy_config=copy_config,
            copy_data=copy_data,
        )

        # Output to file
        if output:
            output_format = "yaml"
            if str(output).endswith(".toml"):
                output_format = "toml"

            migrator.write_spec(spec, output, format=output_format)
            console.print()
            console.print(f"[green]✓ Spec written to {output}[/green]")
            console.print(f"[dim]Review and create with: gaol create -f {output}[/dim]")
            return

        # Execute migration
        console.print()

        def progress_callback(msg: str) -> None:
            console.print(f"  {msg}")

        container_id = migrator.execute(
            spec=spec,
            copy_config=copy_config,
            copy_data=copy_data,
            progress_callback=progress_callback,
        )

        console.print()
        console.print(f"[green]✓ Migration complete![/green]")
        console.print(f"  Container: {spec.name}")
        console.print(f"  ID: {container_id}")
        console.print()
        console.print("[dim]Useful commands:[/dim]")
        console.print(f"  gaol status {spec.name}")
        console.print(f"  gaol service list {spec.name}")
        console.print(f"  gaol logs {spec.name}")

    except ServiceNotFoundError as e:
        err_console.print(f"[red]Error: {e}[/red]")
        err_console.print("[dim]Searched locations:[/dim]")
        for path in e.searched_paths:
            err_console.print(f"  • {path}")
        raise typer.Exit(1) from e
    except ServiceParseError as e:
        err_console.print(f"[red]Error parsing service: {e}[/red]")
        raise typer.Exit(1) from e
    except UnsupportedServiceError as e:
        err_console.print(f"[red]Error: {e}[/red]")
        raise typer.Exit(1) from e
    except Exception as e:
        err_console.print(f"[red]Error: {e}[/red]")
        raise typer.Exit(1) from e


# =============================================================================
# security domain-inspired Template Container Commands
# =============================================================================


@template_container_app.command(name="create")
def template_container_create(
    name: Annotated[str, typer.Argument(help="Template name")],
    distro: Annotated[
        str, typer.Option("-d", "--distro", help="Base distribution")
    ] = "trixie",
    profiles: Annotated[
        list[str] | None,
        typer.Option("-p", "--profile", help="Profiles to apply (can specify multiple)"),
    ] = None,
    packages: Annotated[
        list[str] | None,
        typer.Option("--package", help="Additional packages to install"),
    ] = None,
    description: Annotated[
        str | None,
        typer.Option("--desc", help="Template description"),
    ] = None,
) -> None:
    """Create a new template container.

    Templates are read-only base containers that can spawn multiple
    instances using OverlayFS.
    """
    from gaol.domains import TemplateConfig, get_template_manager
    from gaol.domains.exceptions import TemplateExistsError

    manager = get_template_manager()

    config = TemplateConfig(
        name=name,
        distro=distro,
        profiles=profiles or [],
        packages=packages or [],
        description=description,
    )

    console.print(f"[bold]Creating template:[/bold] {name}")
    console.print(f"  Distro: {distro}")
    if profiles:
        console.print(f"  Profiles: {', '.join(profiles)}")
    console.print()

    try:
        with console.status("Bootstrapping template..."):
            template = manager.create_template(config)

        console.print(f"[green]✓ Template created:[/green] {template.name}")
        console.print(f"  Path: {template.root_path}")
        console.print(f"  Size: {template.size_bytes / 1024 / 1024:.1f} MB")
        console.print()
        console.print("[dim]Spawn an instance with:[/dim]")
        console.print(f"  gaol template-container spawn {name} my-instance")

    except TemplateExistsError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@template_container_app.command(name="list")
def template_container_list(
    json_output: Annotated[
        bool, typer.Option("-j", "--json", help="Output as JSON")
    ] = False,
) -> None:
    """List all template containers."""
    from gaol.domains import get_template_manager

    manager = get_template_manager()
    templates = manager.list_templates()

    if not templates:
        console.print("[dim]No templates found[/dim]")
        console.print("[dim]Create one with: gaol template-container create <name>[/dim]")
        return

    if json_output:
        import json
        data = [t.model_dump(mode="json") for t in templates]
        console.print_json(json.dumps(data))
        return

    table = Table(title="Template Containers")
    table.add_column("Name", style="cyan")
    table.add_column("Distro")
    table.add_column("Type")
    table.add_column("Version")
    table.add_column("Instances")
    table.add_column("Size")

    for t in templates:
        size_mb = t.size_bytes / 1024 / 1024
        table.add_row(
            t.name,
            t.distro,
            t.template_type.value,
            str(t.version),
            str(len(t.instances)),
            f"{size_mb:.1f} MB",
        )

    console.print(table)


@template_container_app.command(name="spawn")
def template_container_spawn(
    template_name: Annotated[str, typer.Argument(help="Template to spawn from")],
    instance_name: Annotated[str, typer.Argument(help="Name for the new instance")],
    ephemeral: Annotated[
        bool, typer.Option("-e", "--ephemeral", help="Destroy instance on stop")
    ] = False,
    start: Annotated[
        bool, typer.Option("-s", "--start", help="Start instance after creation")
    ] = False,
) -> None:
    """Spawn a new instance from a template.

    Instances use OverlayFS to layer changes on top of the template.
    Ephemeral instances are destroyed when stopped.
    """
    from gaol.domains import get_template_manager
    from gaol.domains.exceptions import TemplateNotFoundError, TemplateSpawnError

    manager = get_template_manager()

    try:
        console.print(f"[bold]Spawning instance:[/bold] {instance_name}")
        console.print(f"  From template: {template_name}")
        console.print(f"  Ephemeral: {ephemeral}")
        console.print()

        instance = manager.spawn(
            template_name,
            instance_name,
            ephemeral=ephemeral,
        )

        console.print(f"[green]✓ Instance created:[/green] {instance.name}")

        if start:
            console.print("Starting instance...")
            manager.start_instance(instance_name)
            console.print("[green]✓ Instance started[/green]")

        console.print()
        console.print("[dim]Access with:[/dim]")
        console.print(f"  sudo machinectl shell {instance_name}")

    except TemplateNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None
    except TemplateSpawnError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@template_container_app.command(name="update")
def template_container_update(
    name: Annotated[str, typer.Argument(help="Template name")],
) -> None:
    """Update a template (run apt upgrade)."""
    from gaol.domains import get_template_manager
    from gaol.domains.exceptions import TemplateNotFoundError

    manager = get_template_manager()

    try:
        console.print(f"[bold]Updating template:[/bold] {name}")

        with console.status("Running updates..."):
            template = manager.update_template(name)

        console.print(f"[green]✓ Template updated to version {template.version}[/green]")

    except TemplateNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@template_container_app.command(name="delete")
def template_container_delete(
    name: Annotated[str, typer.Argument(help="Template name")],
    force: Annotated[
        bool, typer.Option("-f", "--force", help="Delete even if instances exist")
    ] = False,
) -> None:
    """Delete a template container."""
    from gaol.domains import get_template_manager
    from gaol.domains.exceptions import TemplateInUseError, TemplateNotFoundError

    manager = get_template_manager()

    try:
        manager.delete_template(name, force=force)
        console.print(f"[green]✓ Template deleted:[/green] {name}")

    except TemplateNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None
    except TemplateInUseError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        err_console.print("[dim]Use --force to delete anyway[/dim]")
        raise typer.Exit(1) from None


@template_container_app.command(name="instances")
def template_container_instances(
    template_name: Annotated[
        str | None, typer.Argument(help="Template name (optional, show all if omitted)")
    ] = None,
) -> None:
    """List instances spawned from templates."""
    from gaol.domains import get_template_manager

    manager = get_template_manager()
    instances = manager.list_instances(template_name)

    if not instances:
        if template_name:
            console.print(f"[dim]No instances for template '{template_name}'[/dim]")
        else:
            console.print("[dim]No instances found[/dim]")
        return

    table = Table(title="Template Instances")
    table.add_column("Instance", style="cyan")
    table.add_column("Template")
    table.add_column("Status")

    for instance_name in instances:
        # Check status
        result = subprocess.run(
            ["sudo", "machinectl", "show", instance_name, "--property=State"],
            capture_output=True,
            text=True,
        )
        if result.returncode == 0 and "running" in result.stdout.lower():
            status = "[green]running[/green]"
        else:
            status = "[dim]stopped[/dim]"

        # Find which template
        template = None
        for t in manager.list_templates():
            if instance_name in t.instances:
                template = t.name
                break

        table.add_row(instance_name, template or "?", status)

    console.print(table)


# =============================================================================
# security domain-inspired Disposable Container Commands
# =============================================================================


@disposable_app.command(name="create")
def disposable_create(
    name: Annotated[str, typer.Argument(help="Disposable name")],
    template_name: Annotated[str, typer.Option("-t", "--template", help="Template to use")],
    application: Annotated[
        str | None,
        typer.Option("-a", "--app", help="Application to run (e.g., /usr/bin/firefox)"),
    ] = None,
    icon: Annotated[
        str | None, typer.Option("-i", "--icon", help="Icon name")
    ] = None,
    icon_color: Annotated[
        str | None, typer.Option("--color", help="Hex color to tint icon (e.g., #CC0000)")
    ] = None,
    desktop_name: Annotated[
        str | None, typer.Option("--desktop-name", help="Name for .desktop file")
    ] = None,
    preserve_downloads: Annotated[
        bool, typer.Option("--keep-downloads", help="Preserve ~/Downloads between launches")
    ] = False,
) -> None:
    """Create a disposable container configuration.

    Disposables are ephemeral containers spawned from templates.
    Each launch creates a fresh instance destroyed after use.
    """
    from gaol.domains import DisposableConfig, DisposableType, get_disposable_manager
    from gaol.domains.exceptions import DisposableExistsError

    manager = get_disposable_manager()

    # Determine type
    disp_type = DisposableType.CUSTOM
    if application and "firefox" in application.lower():
        disp_type = DisposableType.BROWSER
    elif application and "terminal" in application.lower():
        disp_type = DisposableType.TERMINAL

    config = DisposableConfig(
        name=name,
        template_name=template_name,
        disposable_type=disp_type,
        application=application,
        icon=icon,
        icon_color=icon_color,
        desktop_name=desktop_name,
        preserve_downloads=preserve_downloads,
    )

    try:
        manager.create(config)
        console.print(f"[green]✓ Disposable created:[/green] {name}")
        console.print(f"  Template: {template_name}")
        if application:
            console.print(f"  Application: {application}")
        console.print()
        console.print("[dim]Launch with:[/dim]")
        console.print(f"  gaol disposable launch {name}")
        console.print()
        console.print("[dim]Create desktop launcher with:[/dim]")
        console.print(f"  gaol disposable desktop {name}")

    except DisposableExistsError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@disposable_app.command(name="list")
def disposable_list(
    json_output: Annotated[
        bool, typer.Option("-j", "--json", help="Output as JSON")
    ] = False,
) -> None:
    """List configured disposables."""
    from gaol.domains import get_disposable_manager

    manager = get_disposable_manager()
    disposables = manager.list_disposables()

    if not disposables:
        console.print("[dim]No disposables configured[/dim]")
        console.print("[dim]Create one with: gaol disposable create <name> -t <template>[/dim]")
        return

    if json_output:
        import json
        data = [d.model_dump(mode="json") for d in disposables]
        console.print_json(json.dumps(data))
        return

    table = Table(title="Disposable Containers")
    table.add_column("Name", style="cyan")
    table.add_column("Template")
    table.add_column("Type")
    table.add_column("Application")

    for d in disposables:
        app = d.application or "-"
        if len(app) > 30:
            app = "..." + app[-27:]
        table.add_row(d.name, d.template_name, d.disposable_type.value, app)

    console.print(table)


@disposable_app.command(name="launch")
def disposable_launch(
    name: Annotated[str, typer.Argument(help="Disposable name")],
    arguments: Annotated[
        list[str] | None,
        typer.Argument(help="Additional arguments for the application"),
    ] = None,
    no_wait: Annotated[
        bool, typer.Option("--no-wait", help="Don't wait for application to exit")
    ] = False,
) -> None:
    """Launch a disposable container instance.

    Creates a fresh instance from the template, runs the application,
    and destroys the container when the application exits.
    """
    from gaol.domains import get_disposable_manager
    from gaol.domains.exceptions import DisposableLaunchError, DisposableNotFoundError

    manager = get_disposable_manager()

    try:
        console.print(f"[bold]Launching disposable:[/bold] {name}")

        instance = manager.launch(
            name,
            arguments=arguments,
            wait=not no_wait,
        )

        if no_wait:
            console.print(f"[green]✓ Instance started:[/green] {instance.instance_id}")
            console.print(f"  Container: {instance.container_name}")
        else:
            console.print("[green]✓ Application exited, container destroyed[/green]")

    except DisposableNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None
    except DisposableLaunchError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@disposable_app.command(name="instances")
def disposable_instances(
    name: Annotated[
        str | None, typer.Argument(help="Disposable name (optional)")
    ] = None,
) -> None:
    """List running disposable instances."""
    from gaol.domains import get_disposable_manager

    manager = get_disposable_manager()
    instances = manager.list_instances(name)

    if not instances:
        console.print("[dim]No running instances[/dim]")
        return

    table = Table(title="Running Disposable Instances")
    table.add_column("ID", style="cyan")
    table.add_column("Disposable")
    table.add_column("Container")
    table.add_column("Started")

    for inst in instances:
        started = inst.started_at.strftime("%H:%M:%S")
        table.add_row(inst.instance_id, inst.disposable_name, inst.container_name, started)

    console.print(table)


@disposable_app.command(name="terminate")
def disposable_terminate(
    instance_id: Annotated[str, typer.Argument(help="Instance ID to terminate")],
) -> None:
    """Terminate a running disposable instance."""
    from gaol.domains import get_disposable_manager

    manager = get_disposable_manager()
    manager.terminate(instance_id)
    console.print(f"[green]✓ Instance terminated:[/green] {instance_id}")


@disposable_app.command(name="delete")
def disposable_delete(
    name: Annotated[str, typer.Argument(help="Disposable name")],
) -> None:
    """Delete a disposable configuration."""
    from gaol.domains import get_disposable_manager
    from gaol.domains.exceptions import DisposableNotFoundError

    manager = get_disposable_manager()

    try:
        manager.delete(name)
        console.print(f"[green]✓ Disposable deleted:[/green] {name}")

    except DisposableNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@disposable_app.command(name="desktop")
def disposable_desktop(
    name: Annotated[str, typer.Argument(help="Disposable name")],
    uninstall: Annotated[
        bool, typer.Option("-u", "--uninstall", help="Remove desktop entry")
    ] = False,
) -> None:
    """Create or remove a .desktop launcher for a disposable.

    The launcher will spawn a fresh disposable instance each time.
    """
    from gaol.domains import get_disposable_manager, install_disposable_desktop, uninstall_disposable_desktop
    from gaol.domains.exceptions import DisposableNotFoundError

    if uninstall:
        if uninstall_disposable_desktop(name):
            console.print(f"[green]✓ Desktop entry removed:[/green] {name}")
        else:
            console.print(f"[yellow]Desktop entry not found:[/yellow] {name}")
        return

    manager = get_disposable_manager()

    try:
        config = manager.get(name)
        path = install_disposable_desktop(config)
        console.print(f"[green]✓ Desktop entry installed:[/green] {path}")
        console.print()
        console.print("[dim]The launcher should appear in your application menu.[/dim]")

    except DisposableNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@disposable_app.command(name="open-file")
def disposable_open_file(
    file_path: Annotated[str, typer.Argument(help="File to open")],
    disposable_name: Annotated[
        str | None,
        typer.Option("-d", "--disposable", help="Disposable to use (auto-select by MIME if omitted)"),
    ] = None,
) -> None:
    """Open a file in a disposable container.

    The file is copied into a fresh disposable instance and opened.
    The container is destroyed after the viewer exits.
    """
    from pathlib import Path

    from gaol.domains import get_disposable_manager
    from gaol.domains.exceptions import DisposableLaunchError, DisposableNotFoundError

    manager = get_disposable_manager()
    path = Path(file_path).resolve()

    if not path.exists():
        err_console.print(f"[red]File not found:[/red] {file_path}")
        raise typer.Exit(1)

    try:
        console.print(f"[bold]Opening file in disposable:[/bold] {path.name}")

        manager.open_file(str(path), disposable_name)

        console.print("[green]✓ Viewer closed, container destroyed[/green]")

    except DisposableNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None
    except DisposableLaunchError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


# =============================================================================
# Security Domain Commands
# =============================================================================


@domain_app.command(name="list")
def domain_list() -> None:
    """List security domains and their policies."""
    from gaol.domains import DOMAIN_COLORS, DEFAULT_DOMAIN_POLICIES, SecurityDomain, get_domain_store

    store = get_domain_store()

    table = Table(title="Security Domains")
    table.add_column("Domain", style="bold")
    table.add_column("Color")
    table.add_column("Network")
    table.add_column("Ephemeral")
    table.add_column("Containers")

    for domain in SecurityDomain:
        policy = store.get_domain_policy(domain)
        color = policy.color
        containers = store.list_containers_in_domain(domain)

        # Network status
        if not policy.allow_network:
            net = "[red]blocked[/red]"
        elif not policy.allow_internet:
            net = "[yellow]local only[/yellow]"
        else:
            net = "[green]allowed[/green]"

        # Color display
        color_display = f"[{color}]●[/{color}] {color}"

        table.add_row(
            domain.value,
            color_display,
            net,
            "yes" if policy.ephemeral_only else "no",
            str(len(containers)),
        )

    console.print(table)


@domain_app.command(name="assign")
def domain_assign(
    container: Annotated[str, typer.Argument(help="Container name")],
    domain: Annotated[str, typer.Argument(help="Domain (vault, work, personal, self-hosted, untrusted, disposable)")],
) -> None:
    """Assign a container to a security domain."""
    from gaol.domains import SecurityDomain, get_domain_store

    store = get_domain_store()

    try:
        sec_domain = SecurityDomain(domain)
    except ValueError:
        valid = ", ".join(d.value for d in SecurityDomain)
        err_console.print(f"[red]Invalid domain:[/red] {domain}")
        err_console.print(f"[dim]Valid domains: {valid}[/dim]")
        raise typer.Exit(1) from None

    store.assign_container_domain(container, sec_domain)
    console.print(f"[green]✓ Container '{container}' assigned to domain '{domain}'[/green]")


@domain_app.command(name="unassign")
def domain_unassign(
    container: Annotated[str, typer.Argument(help="Container name")],
) -> None:
    """Remove a container from its security domain."""
    from gaol.domains import get_domain_store

    store = get_domain_store()

    if store.unassign_container_domain(container):
        console.print(f"[green]✓ Container '{container}' unassigned from domain[/green]")
    else:
        console.print(f"[yellow]Container '{container}' was not assigned to any domain[/yellow]")


@domain_app.command(name="containers")
def domain_containers(
    domain: Annotated[str, typer.Argument(help="Domain to list containers for")],
) -> None:
    """List containers in a security domain."""
    from gaol.domains import SecurityDomain, get_domain_store

    store = get_domain_store()

    try:
        sec_domain = SecurityDomain(domain)
    except ValueError:
        valid = ", ".join(d.value for d in SecurityDomain)
        err_console.print(f"[red]Invalid domain:[/red] {domain}")
        err_console.print(f"[dim]Valid domains: {valid}[/dim]")
        raise typer.Exit(1) from None

    containers = store.list_containers_in_domain(sec_domain)

    if not containers:
        console.print(f"[dim]No containers in domain '{domain}'[/dim]")
        return

    policy = store.get_domain_policy(sec_domain)
    console.print(f"[bold]Containers in '{domain}' domain:[/bold]")
    console.print(f"  Color: {policy.color}")
    console.print()

    for c in containers:
        console.print(f"  • {c}")


# =============================================================================
# security domain-inspired Service Container Commands
# =============================================================================


@service_container_app.command(name="create")
def service_container_create(
    name: Annotated[str, typer.Argument(help="Service container name")],
    service_type: Annotated[
        str,
        typer.Option("-t", "--type", help="Service type: tor_gateway, vpn_gateway, firewall"),
    ],
    distro: Annotated[
        str, typer.Option("-d", "--distro", help="Base distribution")
    ] = "trixie",
    vpn_config: Annotated[
        str | None,
        typer.Option("--vpn-config", help="Path to VPN config file (WireGuard or OpenVPN)"),
    ] = None,
    vpn_provider: Annotated[
        str,
        typer.Option("--vpn-provider", help="VPN provider: wireguard or openvpn"),
    ] = "wireguard",
    tor_exit_nodes: Annotated[
        list[str] | None,
        typer.Option("--tor-exit", help="Tor exit node countries (can specify multiple)"),
    ] = None,
    tor_bridges: Annotated[
        list[str] | None,
        typer.Option("--tor-bridge", help="Tor bridges (can specify multiple)"),
    ] = None,
    kill_switch: Annotated[
        bool, typer.Option("--kill-switch/--no-kill-switch", help="Enable kill switch")
    ] = True,
) -> None:
    """Create a new service container (sys-tor, sys-vpn, etc).

    Service containers provide network services to other containers,
    similar to security domain sys-net, sys-firewall, sys-whonix.

    Examples:
        gaol service-container create sys-tor -t tor_gateway
        gaol service-container create sys-vpn -t vpn_gateway --vpn-config ~/wg0.conf
        gaol service-container create sys-fw -t firewall
    """
    from gaol.domains import ServiceContainerConfig, ServiceContainerType, get_service_container_manager
    from gaol.domains.service_manager import ServiceExistsError

    manager = get_service_container_manager()

    # Parse service type
    try:
        svc_type = ServiceContainerType(service_type)
    except ValueError:
        valid = ", ".join(t.value for t in ServiceContainerType)
        err_console.print(f"[red]Invalid service type:[/red] {service_type}")
        err_console.print(f"[dim]Valid types: {valid}[/dim]")
        raise typer.Exit(1) from None

    # Validate VPN config
    if svc_type == ServiceContainerType.VPN_GATEWAY and not vpn_config:
        err_console.print("[red]VPN gateway requires --vpn-config[/red]")
        raise typer.Exit(1)

    config = ServiceContainerConfig(
        name=name,
        service_type=svc_type,
        distro=distro,
        vpn_config_file=vpn_config,
        vpn_provider=vpn_provider,
        tor_exit_nodes=tor_exit_nodes or [],
        tor_bridges=tor_bridges or [],
        kill_switch=kill_switch,
    )

    console.print(f"[bold]Creating service container:[/bold] {name}")
    console.print(f"  Type: {svc_type.value}")
    console.print(f"  Distro: {distro}")
    if vpn_config:
        console.print(f"  VPN Config: {vpn_config}")
    console.print()

    try:
        with console.status("Creating service container (this may take a while)..."):
            state = manager.create(config)

        console.print(f"[green]✓ Service container created:[/green] {state.name}")
        console.print(f"  Container: {state.container_name}")
        console.print()
        console.print("[dim]Start with:[/dim]")
        console.print(f"  gaol service-container start {name}")

    except ServiceExistsError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@service_container_app.command(name="list")
def service_container_list(
    service_type: Annotated[
        str | None,
        typer.Option("-t", "--type", help="Filter by service type"),
    ] = None,
    json_output: Annotated[
        bool, typer.Option("-j", "--json", help="Output as JSON")
    ] = False,
) -> None:
    """List all service containers."""
    from gaol.domains import ServiceContainerType, get_service_container_manager

    manager = get_service_container_manager()

    # Parse filter
    svc_type = None
    if service_type:
        try:
            svc_type = ServiceContainerType(service_type)
        except ValueError:
            valid = ", ".join(t.value for t in ServiceContainerType)
            err_console.print(f"[red]Invalid service type:[/red] {service_type}")
            err_console.print(f"[dim]Valid types: {valid}[/dim]")
            raise typer.Exit(1) from None

    services = manager.list_services(svc_type)

    if not services:
        console.print("[dim]No service containers found[/dim]")
        console.print("[dim]Create one with: gaol service-container create <name> -t <type>[/dim]")
        return

    if json_output:
        import json
        data = [s.model_dump(mode="json") for s in services]
        console.print_json(json.dumps(data))
        return

    table = Table(title="Service Containers")
    table.add_column("Name", style="cyan")
    table.add_column("Type")
    table.add_column("Status")
    table.add_column("Public IP")
    table.add_column("Clients")

    for s in services:
        # Status display
        if s.status == "running":
            status = "[green]running[/green]"
        elif s.status == "error":
            status = "[red]error[/red]"
        else:
            status = "[dim]stopped[/dim]"

        table.add_row(
            s.name,
            s.service_type.value,
            status,
            s.public_ip or "-",
            str(len(s.attached_clients)),
        )

    console.print(table)


@service_container_app.command(name="start")
def service_container_start(
    name: Annotated[str, typer.Argument(help="Service container name")],
) -> None:
    """Start a service container."""
    from gaol.domains import get_service_container_manager
    from gaol.domains.service_manager import ServiceNotFoundError, ServiceStartError

    manager = get_service_container_manager()

    try:
        console.print(f"[bold]Starting service:[/bold] {name}")

        with console.status("Starting service container..."):
            state = manager.start(name)

        console.print(f"[green]✓ Service started:[/green] {name}")
        if state.public_ip:
            console.print(f"  Public IP: {state.public_ip}")
        if state.tor_bootstrap_progress:
            console.print(f"  Tor Bootstrap: {state.tor_bootstrap_progress}%")
        console.print()
        console.print("[dim]Attach a client with:[/dim]")
        console.print(f"  gaol service-container attach <client> {name}")

    except ServiceNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None
    except ServiceStartError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@service_container_app.command(name="stop")
def service_container_stop(
    name: Annotated[str, typer.Argument(help="Service container name")],
) -> None:
    """Stop a service container."""
    from gaol.domains import get_service_container_manager
    from gaol.domains.service_manager import ServiceNotFoundError

    manager = get_service_container_manager()

    try:
        manager.stop(name)
        console.print(f"[green]✓ Service stopped:[/green] {name}")

    except ServiceNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@service_container_app.command(name="delete")
def service_container_delete(
    name: Annotated[str, typer.Argument(help="Service container name")],
    force: Annotated[
        bool, typer.Option("-f", "--force", help="Force deletion even if clients are attached")
    ] = False,
) -> None:
    """Delete a service container."""
    from gaol.domains import get_service_container_manager
    from gaol.domains.service_manager import ServiceNotFoundError, ServiceContainerError

    manager = get_service_container_manager()

    try:
        manager.delete(name, force=force)
        console.print(f"[green]✓ Service deleted:[/green] {name}")

    except ServiceNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None
    except ServiceContainerError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        err_console.print("[dim]Use --force to delete anyway[/dim]")
        raise typer.Exit(1) from None


@service_container_app.command(name="status")
def service_container_status(
    name: Annotated[str, typer.Argument(help="Service container name")],
) -> None:
    """Show status of a service container."""
    from gaol.domains import get_service_container_manager, get_domain_store
    from gaol.domains.service_manager import ServiceNotFoundError

    manager = get_service_container_manager()
    store = get_domain_store()

    try:
        state = manager.status(name)

        # Status display
        if state.status == "running":
            status_str = "[green]running[/green]"
        elif state.status == "error":
            status_str = f"[red]error: {state.error_message}[/red]"
        else:
            status_str = "[dim]stopped[/dim]"

        console.print(f"[bold]Service:[/bold] {state.name}")
        console.print(f"  Type: {state.service_type.value}")
        console.print(f"  Status: {status_str}")
        console.print(f"  Container: {state.container_name}")

        if state.public_ip:
            console.print(f"  Public IP: {state.public_ip}")
        if state.tor_bootstrap_progress is not None:
            console.print(f"  Tor Bootstrap: {state.tor_bootstrap_progress}%")
        if state.created_at:
            console.print(f"  Created: {state.created_at.strftime('%Y-%m-%d %H:%M')}")
        if state.started_at:
            console.print(f"  Started: {state.started_at.strftime('%Y-%m-%d %H:%M')}")

        # Show attached clients
        clients = store.get_service_clients(name)
        if clients:
            console.print()
            console.print("[bold]Attached Clients:[/bold]")
            for c in clients:
                console.print(f"  • {c.client_name} (routing: {c.routing_mode})")

    except ServiceNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@service_container_app.command(name="attach")
def service_container_attach(
    client: Annotated[str, typer.Argument(help="Client container name")],
    service: Annotated[str, typer.Argument(help="Service container name")],
    routing: Annotated[
        str, typer.Option("-r", "--routing", help="Routing mode: all or selected")
    ] = "all",
) -> None:
    """Attach a client container to route through a service.

    This configures the client container to route traffic through
    the service container (e.g., through Tor or VPN).

    Examples:
        gaol service-container attach my-browser sys-tor
        gaol service-container attach work-browser sys-vpn
    """
    from gaol.domains import get_service_container_manager
    from gaol.domains.service_manager import ServiceNotFoundError, AttachmentError

    manager = get_service_container_manager()

    try:
        console.print(f"[bold]Attaching:[/bold] {client} → {service}")

        with console.status("Configuring client routing..."):
            attachment = manager.attach(client, service, routing_mode=routing)

        console.print(f"[green]✓ Client attached:[/green] {client} → {service}")
        console.print(f"  Routing: {attachment.routing_mode}")
        console.print()
        console.print("[dim]Detach with:[/dim]")
        console.print(f"  gaol service-container detach {client} {service}")

    except ServiceNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None
    except AttachmentError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@service_container_app.command(name="detach")
def service_container_detach(
    client: Annotated[str, typer.Argument(help="Client container name")],
    service: Annotated[str, typer.Argument(help="Service container name")],
) -> None:
    """Detach a client container from a service."""
    from gaol.domains import get_service_container_manager

    manager = get_service_container_manager()

    if manager.detach(client, service):
        console.print(f"[green]✓ Client detached:[/green] {client} ✕ {service}")
    else:
        console.print(f"[yellow]Client '{client}' was not attached to '{service}'[/yellow]")


@service_container_app.command(name="clients")
def service_container_clients(
    service: Annotated[str, typer.Argument(help="Service container name")],
) -> None:
    """List clients attached to a service container."""
    from gaol.domains import get_domain_store
    from gaol.domains.service_manager import ServiceNotFoundError

    store = get_domain_store()

    # Check service exists
    state = store.get_service(service)
    if not state:
        err_console.print(f"[red]Service not found:[/red] {service}")
        raise typer.Exit(1)

    clients = store.get_service_clients(service)

    if not clients:
        console.print(f"[dim]No clients attached to '{service}'[/dim]")
        console.print("[dim]Attach with: gaol service-container attach <client> {service}[/dim]")
        return

    console.print(f"[bold]Clients attached to '{service}':[/bold]")
    for c in clients:
        console.print(f"  • {c.client_name} (routing: {c.routing_mode}, since: {c.attached_at.strftime('%Y-%m-%d %H:%M')})")


# =============================================================================
# security domain-inspired Vault Commands
# =============================================================================


@vault_app.command(name="create")
def vault_create(
    name: Annotated[str, typer.Argument(help="Vault name")],
    vault_type: Annotated[
        str,
        typer.Option("-t", "--type", help="Vault type: gpg, ssh, or combined"),
    ] = "gpg",
    distro: Annotated[
        str, typer.Option("-d", "--distro", help="Base distribution")
    ] = "trixie",
    description: Annotated[
        str | None,
        typer.Option("--desc", help="Vault description"),
    ] = None,
) -> None:
    """Create a new vault container for storing private keys.

    Vaults have NO network access and store private keys securely.
    Other containers can request cryptographic operations via agent
    socket forwarding without ever seeing the keys.

    Examples:
        gaol vault create gpg-vault -t gpg
        gaol vault create ssh-vault -t ssh
        gaol vault create secrets -t combined
    """
    from gaol.domains import VaultConfig, VaultType, get_vault_manager
    from gaol.domains.exceptions import VaultExistsError

    manager = get_vault_manager()

    # Parse vault type
    try:
        v_type = VaultType(vault_type)
    except ValueError:
        valid = ", ".join(t.value for t in VaultType)
        err_console.print(f"[red]Invalid vault type:[/red] {vault_type}")
        err_console.print(f"[dim]Valid types: {valid}[/dim]")
        raise typer.Exit(1) from None

    config = VaultConfig(
        name=name,
        vault_type=v_type,
        distro=distro,
        description=description,
    )

    console.print(f"[bold]Creating vault:[/bold] {name}")
    console.print(f"  Type: {v_type.value}")
    console.print(f"  [yellow]Network: DISABLED[/yellow] (security feature)")
    console.print()

    try:
        with console.status("Creating vault container (this may take a while)..."):
            state = manager.create(config)

        console.print(f"[green]✓ Vault created:[/green] {state.name}")
        console.print(f"  Container: {state.container_name}")
        console.print()
        console.print("[dim]Start with:[/dim]")
        console.print(f"  gaol vault start {name}")
        console.print("[dim]Import keys with:[/dim]")
        console.print(f"  gaol vault import-key {name} /path/to/private.key")

    except VaultExistsError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@vault_app.command(name="list")
def vault_list(
    vault_type: Annotated[
        str | None,
        typer.Option("-t", "--type", help="Filter by vault type"),
    ] = None,
    json_output: Annotated[
        bool, typer.Option("-j", "--json", help="Output as JSON")
    ] = False,
) -> None:
    """List all vaults."""
    from gaol.domains import VaultType, get_vault_manager

    manager = get_vault_manager()

    # Parse filter
    v_type = None
    if vault_type:
        try:
            v_type = VaultType(vault_type)
        except ValueError:
            valid = ", ".join(t.value for t in VaultType)
            err_console.print(f"[red]Invalid vault type:[/red] {vault_type}")
            err_console.print(f"[dim]Valid types: {valid}[/dim]")
            raise typer.Exit(1) from None

    vaults = manager.list_vaults(v_type)

    if not vaults:
        console.print("[dim]No vaults found[/dim]")
        console.print("[dim]Create one with: gaol vault create <name> -t <type>[/dim]")
        return

    if json_output:
        import json
        data = [v.model_dump(mode="json") for v in vaults]
        console.print_json(json.dumps(data))
        return

    table = Table(title="Secret Vaults")
    table.add_column("Name", style="cyan")
    table.add_column("Type")
    table.add_column("Status")
    table.add_column("Keys")
    table.add_column("Attached")

    for v in vaults:
        # Status display
        if v.status == "running":
            status = "[green]running[/green]"
        elif v.status == "locked":
            status = "[yellow]locked[/yellow]"
        elif v.status == "error":
            status = "[red]error[/red]"
        else:
            status = "[dim]stopped[/dim]"

        table.add_row(
            v.name,
            v.vault_type.value,
            status,
            str(len(v.keys)),
            str(len(v.attached_containers)),
        )

    console.print(table)


@vault_app.command(name="start")
def vault_start(
    name: Annotated[str, typer.Argument(help="Vault name")],
) -> None:
    """Start a vault container."""
    from gaol.domains import get_vault_manager
    from gaol.domains.exceptions import VaultNotFoundError

    manager = get_vault_manager()

    try:
        console.print(f"[bold]Starting vault:[/bold] {name}")

        with console.status("Starting vault container..."):
            state = manager.start(name)

        console.print(f"[green]✓ Vault started:[/green] {name}")
        if state.gpg_agent_socket:
            console.print(f"  GPG Agent: {state.gpg_agent_socket}")
        if state.ssh_agent_socket:
            console.print(f"  SSH Agent: {state.ssh_agent_socket}")

    except VaultNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@vault_app.command(name="stop")
def vault_stop(
    name: Annotated[str, typer.Argument(help="Vault name")],
) -> None:
    """Stop a vault container."""
    from gaol.domains import get_vault_manager
    from gaol.domains.exceptions import VaultNotFoundError

    manager = get_vault_manager()

    try:
        manager.stop(name)
        console.print(f"[green]✓ Vault stopped:[/green] {name}")

    except VaultNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@vault_app.command(name="lock")
def vault_lock(
    name: Annotated[str, typer.Argument(help="Vault name")],
) -> None:
    """Lock a vault (clear cached passphrases)."""
    from gaol.domains import get_vault_manager
    from gaol.domains.exceptions import VaultNotFoundError

    manager = get_vault_manager()

    try:
        manager.lock(name)
        console.print(f"[yellow]🔒 Vault locked:[/yellow] {name}")
        console.print("[dim]All cached passphrases cleared[/dim]")

    except VaultNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@vault_app.command(name="unlock")
def vault_unlock(
    name: Annotated[str, typer.Argument(help="Vault name")],
) -> None:
    """Unlock a vault."""
    from gaol.domains import get_vault_manager
    from gaol.domains.exceptions import VaultNotFoundError

    manager = get_vault_manager()

    try:
        manager.unlock(name)
        console.print(f"[green]🔓 Vault unlocked:[/green] {name}")

    except VaultNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@vault_app.command(name="delete")
def vault_delete(
    name: Annotated[str, typer.Argument(help="Vault name")],
    force: Annotated[
        bool, typer.Option("-f", "--force", help="Force deletion even if containers attached")
    ] = False,
) -> None:
    """Delete a vault container.

    WARNING: This will destroy all keys stored in the vault!
    """
    from gaol.domains import get_vault_manager
    from gaol.domains.exceptions import VaultNotFoundError

    manager = get_vault_manager()

    try:
        manager.delete(name, force=force)
        console.print(f"[green]✓ Vault deleted:[/green] {name}")

    except VaultNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None
    except RuntimeError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        err_console.print("[dim]Use --force to delete anyway[/dim]")
        raise typer.Exit(1) from None


@vault_app.command(name="status")
def vault_status(
    name: Annotated[str, typer.Argument(help="Vault name")],
) -> None:
    """Show vault status and stored keys."""
    from gaol.domains import get_vault_manager, get_domain_store
    from gaol.domains.exceptions import VaultNotFoundError

    manager = get_vault_manager()
    store = get_domain_store()

    try:
        state = manager.status(name)

        # Status display
        if state.status == "running":
            status_str = "[green]running[/green]"
        elif state.status == "locked":
            status_str = "[yellow]locked[/yellow]"
        elif state.status == "error":
            status_str = f"[red]error: {state.error_message}[/red]"
        else:
            status_str = "[dim]stopped[/dim]"

        console.print(f"[bold]Vault:[/bold] {state.name}")
        console.print(f"  Type: {state.vault_type.value}")
        console.print(f"  Status: {status_str}")
        console.print(f"  Container: {state.container_name}")
        console.print(f"  [yellow]Network: DISABLED[/yellow]")

        if state.gpg_agent_socket:
            console.print(f"  GPG Agent: {state.gpg_agent_socket}")
        if state.ssh_agent_socket:
            console.print(f"  SSH Agent: {state.ssh_agent_socket}")

        if state.created_at:
            console.print(f"  Created: {state.created_at.strftime('%Y-%m-%d %H:%M')}")

        # Show keys
        if state.keys:
            console.print()
            console.print("[bold]Stored Keys:[/bold]")
            for key in state.keys:
                key_str = f"  • {key.key_id} ({key.key_type})"
                if key.user_id:
                    key_str += f" - {key.user_id}"
                console.print(key_str)

        # Show policies
        policies = store.list_vault_policies(name)
        if policies:
            console.print()
            console.print("[bold]Access Policies:[/bold]")
            for p in policies:
                ops = ", ".join(o.value for o in p.allowed_operations)
                console.print(f"  • {p.container_name}: {ops}")

        # Show attached containers
        attachments = store.list_vault_attachments(name)
        if attachments:
            console.print()
            console.print("[bold]Attached Containers:[/bold]")
            for a in attachments:
                console.print(f"  • {a.container_name} (since {a.attached_at.strftime('%Y-%m-%d %H:%M')})")

    except VaultNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@vault_app.command(name="import-key")
def vault_import_key(
    vault_name: Annotated[str, typer.Argument(help="Vault name")],
    key_path: Annotated[str, typer.Argument(help="Path to private key file")],
    key_type: Annotated[
        str | None,
        typer.Option("-t", "--type", help="Key type: gpg or ssh (auto-detected if not specified)"),
    ] = None,
) -> None:
    """Import a private key into a vault.

    The vault must be running. Keys can be GPG private keys or SSH private keys.

    Examples:
        gaol vault import-key gpg-vault ~/.gnupg/private.key
        gaol vault import-key ssh-vault ~/.ssh/id_ed25519
    """
    from gaol.domains import VaultType, get_vault_manager
    from gaol.domains.exceptions import VaultKeyImportError, VaultNotFoundError

    manager = get_vault_manager()

    # Parse key type if specified
    v_type = None
    if key_type:
        try:
            v_type = VaultType(key_type)
        except ValueError:
            err_console.print(f"[red]Invalid key type:[/red] {key_type}")
            err_console.print("[dim]Valid types: gpg, ssh[/dim]")
            raise typer.Exit(1) from None

    try:
        console.print(f"[bold]Importing key into vault:[/bold] {vault_name}")
        console.print(f"  Key file: {key_path}")

        with console.status("Importing key..."):
            key_info = manager.import_key(vault_name, key_path, v_type)

        console.print(f"[green]✓ Key imported:[/green] {key_info.key_id}")
        console.print(f"  Type: {key_info.key_type}")
        if key_info.user_id:
            console.print(f"  User: {key_info.user_id}")

    except VaultNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None
    except VaultKeyImportError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@vault_app.command(name="keys")
def vault_keys(
    vault_name: Annotated[str, typer.Argument(help="Vault name")],
) -> None:
    """List keys stored in a vault."""
    from gaol.domains import get_vault_manager
    from gaol.domains.exceptions import VaultNotFoundError

    manager = get_vault_manager()

    try:
        keys = manager.list_keys(vault_name)

        if not keys:
            console.print(f"[dim]No keys in vault '{vault_name}'[/dim]")
            console.print("[dim]Import with: gaol vault import-key <vault> <key-path>[/dim]")
            return

        table = Table(title=f"Keys in '{vault_name}'")
        table.add_column("Key ID", style="cyan")
        table.add_column("Type")
        table.add_column("Vault Type")
        table.add_column("User/Comment")
        table.add_column("Imported")

        for k in keys:
            table.add_row(
                k.key_id,
                k.key_type,
                k.vault_type.value,
                k.user_id or k.comment or "-",
                k.imported_at.strftime("%Y-%m-%d %H:%M"),
            )

        console.print(table)

    except VaultNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@vault_app.command(name="allow")
def vault_allow(
    vault_name: Annotated[str, typer.Argument(help="Vault name")],
    container: Annotated[str, typer.Option("--container", "-c", help="Container to allow")],
    operations: Annotated[
        list[str],
        typer.Option("--operations", "-o", help="Operations to allow (can specify multiple)"),
    ],
    key_filter: Annotated[
        list[str] | None,
        typer.Option("--key", "-k", help="Restrict to specific key IDs"),
    ] = None,
) -> None:
    """Allow a container to perform operations on the vault.

    Operations: sign, decrypt, encrypt, ssh_auth, ssh_sign, export_public, list_keys

    Examples:
        gaol vault allow gpg-vault -c work-email -o sign
        gaol vault allow ssh-vault -c dev -o ssh_auth -o ssh_sign
        gaol vault allow gpg-vault -c work -o sign -k 0x1234ABCD
    """
    from gaol.domains import VaultOperation, get_vault_manager
    from gaol.domains.exceptions import VaultNotFoundError

    manager = get_vault_manager()

    # Parse operations
    ops: list[VaultOperation] = []
    for op in operations:
        try:
            ops.append(VaultOperation(op))
        except ValueError:
            valid = ", ".join(o.value for o in VaultOperation)
            err_console.print(f"[red]Invalid operation:[/red] {op}")
            err_console.print(f"[dim]Valid operations: {valid}[/dim]")
            raise typer.Exit(1) from None

    try:
        policy = manager.allow(vault_name, container, ops, key_filter)

        console.print(f"[green]✓ Policy created:[/green]")
        console.print(f"  Container: {policy.container_name}")
        console.print(f"  Vault: {policy.vault_name}")
        console.print(f"  Operations: {', '.join(o.value for o in policy.allowed_operations)}")
        if policy.key_filter:
            console.print(f"  Keys: {', '.join(policy.key_filter)}")
        console.print()
        console.print("[dim]Attach with:[/dim]")
        console.print(f"  gaol vault attach {container} {vault_name}")

    except VaultNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@vault_app.command(name="revoke")
def vault_revoke(
    vault_name: Annotated[str, typer.Argument(help="Vault name")],
    container: Annotated[str, typer.Option("--container", "-c", help="Container to revoke")],
) -> None:
    """Revoke a container's access to a vault."""
    from gaol.domains import get_vault_manager

    manager = get_vault_manager()

    if manager.revoke(vault_name, container):
        console.print(f"[green]✓ Access revoked:[/green] {container} ✕ {vault_name}")
    else:
        console.print(f"[yellow]No policy found for '{container}' on vault '{vault_name}'[/yellow]")


@vault_app.command(name="attach")
def vault_attach(
    container: Annotated[str, typer.Argument(help="Container to attach")],
    vault_name: Annotated[str, typer.Argument(help="Vault name")],
) -> None:
    """Attach a container to a vault for agent forwarding.

    The container must have a policy allowing it to use the vault.
    After attaching, the container can use GPG/SSH operations through
    the forwarded agent socket.

    Examples:
        gaol vault attach work-email gpg-vault
        gaol vault attach dev ssh-vault
    """
    from gaol.domains import get_vault_manager
    from gaol.domains.exceptions import (
        VaultAttachmentError,
        VaultLockedError,
        VaultNotFoundError,
        VaultOperationDeniedError,
    )

    manager = get_vault_manager()

    try:
        console.print(f"[bold]Attaching:[/bold] {container} → {vault_name}")

        with console.status("Setting up agent forwarding..."):
            attachment = manager.attach(container, vault_name)

        console.print(f"[green]✓ Container attached:[/green] {container} → {vault_name}")
        if attachment.gpg_socket_path:
            console.print(f"  GPG Socket: {attachment.gpg_socket_path}")
        if attachment.ssh_socket_path:
            console.print(f"  SSH Socket: {attachment.ssh_socket_path}")
        console.print()
        console.print("[dim]In the container, cryptographic operations will use the vault's keys[/dim]")

    except VaultNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None
    except VaultLockedError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None
    except VaultOperationDeniedError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        err_console.print("[dim]Create a policy first with: gaol vault allow ...[/dim]")
        raise typer.Exit(1) from None
    except VaultAttachmentError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@vault_app.command(name="detach")
def vault_detach(
    container: Annotated[str, typer.Argument(help="Container to detach")],
    vault_name: Annotated[str, typer.Argument(help="Vault name")],
) -> None:
    """Detach a container from a vault."""
    from gaol.domains import get_vault_manager

    manager = get_vault_manager()

    if manager.detach(container, vault_name):
        console.print(f"[green]✓ Container detached:[/green] {container} ✕ {vault_name}")
    else:
        console.print(f"[yellow]Container '{container}' was not attached to '{vault_name}'[/yellow]")


# =============================================================================
# security domain-inspired Network Chain Commands
# =============================================================================


@chain_app.command(name="create")
def chain_create(
    name: Annotated[str, typer.Argument(help="Chain name")],
    app_container: Annotated[
        str, typer.Option("-a", "--app", help="App container (traffic source)")
    ],
    hops: Annotated[
        list[str],
        typer.Option(
            "-h", "--hop",
            help="Hop in format 'type:container' (can specify multiple in order)",
        ),
    ],
    description: Annotated[
        str | None,
        typer.Option("--desc", help="Chain description"),
    ] = None,
    kill_switch: Annotated[
        bool, typer.Option("--kill-switch/--no-kill-switch", help="Enable kill switch")
    ] = True,
) -> None:
    """Create a network chain for multi-hop traffic routing.

    Chains route traffic from an app container through multiple
    service containers (firewall, tor, vpn) for defense in depth.

    Hop types: firewall, gateway, proxy

    Examples:
        gaol chain create secure-web -a my-browser \\
            -h firewall:sys-firewall -h gateway:sys-tor

        gaol chain create vpn-chain -a work-browser \\
            -h firewall:sys-fw -h gateway:sys-vpn
    """
    from gaol.domains import ChainConfig, ChainHop, ChainHopType, get_chain_manager
    from gaol.domains.exceptions import ChainExistsError

    manager = get_chain_manager()

    # Parse hops
    parsed_hops: list[ChainHop] = []
    for i, hop_str in enumerate(hops, start=1):
        if ":" not in hop_str:
            err_console.print(f"[red]Invalid hop format:[/red] {hop_str}")
            err_console.print("[dim]Use format 'type:container' (e.g., 'gateway:sys-tor')[/dim]")
            raise typer.Exit(1)

        hop_type_str, container_name = hop_str.split(":", 1)

        try:
            hop_type = ChainHopType(hop_type_str)
        except ValueError:
            valid = ", ".join(t.value for t in ChainHopType if t != ChainHopType.APP)
            err_console.print(f"[red]Invalid hop type:[/red] {hop_type_str}")
            err_console.print(f"[dim]Valid types: {valid}[/dim]")
            raise typer.Exit(1) from None

        parsed_hops.append(ChainHop(
            hop_type=hop_type,
            container_name=container_name,
            position=i,
        ))

    config = ChainConfig(
        name=name,
        app_container=app_container,
        hops=parsed_hops,
        description=description,
        kill_switch=kill_switch,
    )

    console.print(f"[bold]Creating chain:[/bold] {name}")
    console.print(f"  App: {app_container}")
    console.print(f"  Hops: {' → '.join(h.container_name for h in parsed_hops)}")
    if kill_switch:
        console.print(f"  [yellow]Kill switch: enabled[/yellow]")
    console.print()

    try:
        state = manager.create(config)

        console.print(f"[green]✓ Chain created:[/green] {state.name}")
        console.print()
        console.print("[dim]Start with:[/dim]")
        console.print(f"  gaol chain start {name}")

    except ChainExistsError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@chain_app.command(name="list")
def chain_list(
    json_output: Annotated[
        bool, typer.Option("-j", "--json", help="Output as JSON")
    ] = False,
) -> None:
    """List all network chains."""
    from gaol.domains import get_chain_manager

    manager = get_chain_manager()
    chains = manager.list_chains()

    if not chains:
        console.print("[dim]No chains found[/dim]")
        console.print("[dim]Create one with: gaol chain create <name> -a <app> -h <type:container>[/dim]")
        return

    if json_output:
        import json
        data = [c.model_dump(mode="json") for c in chains]
        console.print_json(json.dumps(data))
        return

    table = Table(title="Network Chains")
    table.add_column("Name", style="cyan")
    table.add_column("App")
    table.add_column("Hops")
    table.add_column("Status")
    table.add_column("Exit IP")

    for c in chains:
        # Status display
        if c.status == "running":
            status = "[green]running[/green]"
        elif c.status == "degraded":
            status = "[yellow]degraded[/yellow]"
        elif c.status == "error":
            status = "[red]error[/red]"
        else:
            status = "[dim]stopped[/dim]"

        # Hops display
        hop_str = " → ".join(h.container_name for h in sorted(c.hops, key=lambda x: x.position))

        table.add_row(
            c.name,
            c.app_container,
            hop_str or "-",
            status,
            c.public_ip or "-",
        )

    console.print(table)


@chain_app.command(name="start")
def chain_start(
    name: Annotated[str, typer.Argument(help="Chain name")],
) -> None:
    """Start a network chain.

    This configures routing from the app container through all hops.
    """
    from gaol.domains import get_chain_manager
    from gaol.domains.exceptions import ChainNotFoundError, ChainStartError

    manager = get_chain_manager()

    try:
        console.print(f"[bold]Starting chain:[/bold] {name}")

        with console.status("Configuring chain routing..."):
            state = manager.start(name)

        console.print(f"[green]✓ Chain started:[/green] {name}")
        console.print(f"  App: {state.app_container}")

        # Show hop statuses
        for hop in sorted(state.hops, key=lambda h: h.position):
            hop_status = "[green]●[/green]" if hop.status == "running" else "[red]●[/red]"
            console.print(f"  {hop_status} Hop {hop.position}: {hop.container_name} ({hop.ip_address or 'no IP'})")

        if state.public_ip:
            console.print(f"  Exit IP: {state.public_ip}")

    except ChainNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None
    except ChainStartError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@chain_app.command(name="stop")
def chain_stop(
    name: Annotated[str, typer.Argument(help="Chain name")],
) -> None:
    """Stop a network chain.

    This removes routing configuration but doesn't stop hop containers.
    """
    from gaol.domains import get_chain_manager
    from gaol.domains.exceptions import ChainNotFoundError

    manager = get_chain_manager()

    try:
        manager.stop(name)
        console.print(f"[green]✓ Chain stopped:[/green] {name}")
        console.print("[dim]Hop containers are still running[/dim]")

    except ChainNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@chain_app.command(name="delete")
def chain_delete(
    name: Annotated[str, typer.Argument(help="Chain name")],
    force: Annotated[
        bool, typer.Option("-f", "--force", help="Force deletion even if running")
    ] = False,
) -> None:
    """Delete a network chain."""
    from gaol.domains import get_chain_manager
    from gaol.domains.exceptions import ChainNotFoundError

    manager = get_chain_manager()

    try:
        manager.delete(name, force=force)
        console.print(f"[green]✓ Chain deleted:[/green] {name}")

    except ChainNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None
    except RuntimeError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        err_console.print("[dim]Use --force to delete anyway[/dim]")
        raise typer.Exit(1) from None


@chain_app.command(name="status")
def chain_status(
    name: Annotated[str, typer.Argument(help="Chain name")],
) -> None:
    """Show detailed status of a network chain."""
    from gaol.domains import get_chain_manager
    from gaol.domains.exceptions import ChainNotFoundError

    manager = get_chain_manager()

    try:
        state = manager.status(name)

        # Status display
        if state.status == "running":
            status_str = "[green]running[/green]"
        elif state.status == "degraded":
            status_str = "[yellow]degraded[/yellow]"
        elif state.status == "error":
            status_str = f"[red]error: {state.error_message}[/red]"
        else:
            status_str = "[dim]stopped[/dim]"

        console.print(f"[bold]Chain:[/bold] {state.name}")
        console.print(f"  Status: {status_str}")
        console.print(f"  App: {state.app_container}")

        if state.kill_switch_active:
            console.print(f"  [red]Kill switch: ACTIVE (traffic blocked)[/red]")

        if state.public_ip:
            console.print(f"  Exit IP: {state.public_ip}")

        if state.created_at:
            console.print(f"  Created: {state.created_at.strftime('%Y-%m-%d %H:%M')}")
        if state.started_at:
            console.print(f"  Started: {state.started_at.strftime('%Y-%m-%d %H:%M')}")

        # Show hops
        if state.hops:
            console.print()
            console.print("[bold]Hops:[/bold]")
            for hop in sorted(state.hops, key=lambda h: h.position):
                if hop.status == "running":
                    status_icon = "[green]●[/green]"
                elif hop.status == "error":
                    status_icon = "[red]●[/red]"
                else:
                    status_icon = "[dim]○[/dim]"

                console.print(
                    f"  {status_icon} {hop.position}. {hop.container_name} "
                    f"({hop.hop_type.value}) - {hop.ip_address or 'no IP'}"
                )

    except ChainNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


@chain_app.command(name="kill-switch")
def chain_kill_switch(
    name: Annotated[str, typer.Argument(help="Chain name")],
    activate: Annotated[
        bool, typer.Option("--on/--off", help="Activate or deactivate kill switch")
    ] = True,
) -> None:
    """Manually activate or deactivate the kill switch.

    When active, the kill switch blocks ALL traffic from the app container.
    """
    from gaol.domains import get_chain_manager
    from gaol.domains.exceptions import ChainNotFoundError

    manager = get_chain_manager()

    try:
        if activate:
            manager.activate_kill_switch(name)
            console.print(f"[red]🛑 Kill switch ACTIVATED:[/red] {name}")
            console.print("[dim]All traffic from app container is now blocked[/dim]")
        else:
            manager.deactivate_kill_switch(name)
            console.print(f"[green]✓ Kill switch deactivated:[/green] {name}")
            console.print("[dim]Traffic routing restored[/dim]")

    except ChainNotFoundError as e:
        err_console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1) from None


if __name__ == "__main__":
    app()
