"""libvirt/QEMU VM runtime implementation."""

from __future__ import annotations

import json
import shutil
import subprocess
import uuid
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING

from gaol.models.container import (
    ContainerState,
    ContainerStats,
    ContainerStatus,
    NetworkMode,
)
from gaol.profiles.applicator import MergedProfile, get_profile_applicator
from gaol.runtimes.base import ContainerRuntime

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

# Base directory for libvirt VMs managed by gaol
# Try system directory first, fall back to user directory
def _get_libvirt_base_dir() -> Path:
    """Get base directory for libvirt VMs."""
    system_dir = Path("/var/lib/gaol/libvirt")
    user_dir = Path.home() / ".local/share/gaol/libvirt"

    # Try system dir if we have write access
    if system_dir.exists() and system_dir.is_dir():
        return system_dir

    # Fall back to user directory
    return user_dir


LIBVIRT_DIR = _get_libvirt_base_dir()
IMAGES_DIR = LIBVIRT_DIR / "images"
VMS_DIR = LIBVIRT_DIR / "vms"

# Default VM settings
DEFAULT_MEMORY_MB = 2048
DEFAULT_VCPUS = 2
DEFAULT_DISK_GB = 20

# Label prefix for gaol metadata
LABEL_PREFIX = "dev.gaol"


class LibvirtRuntime(ContainerRuntime):
    """libvirt/QEMU VM runtime for full isolation."""

    def __init__(self) -> None:
        """Initialize the libvirt runtime."""
        self._conn = None

    def _ensure_dirs(self) -> None:
        """Ensure required directories exist (called lazily when needed)."""
        try:
            for d in [LIBVIRT_DIR, IMAGES_DIR, VMS_DIR]:
                d.mkdir(parents=True, exist_ok=True)
        except PermissionError:
            # Fall back to user directory
            user_dir = Path.home() / ".local/share/gaol/libvirt"
            for subdir in ["images", "vms"]:
                (user_dir / subdir).mkdir(parents=True, exist_ok=True)

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

    @property
    def conn(self):
        """Get or create libvirt connection."""
        if self._conn is None:
            try:
                import libvirt

                self._conn = libvirt.open("qemu:///system")
            except Exception as e:
                raise RuntimeError(f"Failed to connect to libvirt: {e}") from e
        return self._conn

    def is_available(self) -> bool:
        """Check if libvirt is available."""
        # Check for libvirt Python module
        try:
            import libvirt  # noqa: F401
        except ImportError:
            return False

        # Check for virsh command
        if not shutil.which("virsh"):
            return False

        # Check for qemu-img
        if not shutil.which("qemu-img"):
            return False

        # Try to connect
        try:
            conn = self.conn
            return conn is not None
        except Exception:
            return False

    def _get_vm_dir(self, name: str) -> Path:
        """Get the directory for a VM."""
        return VMS_DIR / name

    def _get_metadata_path(self, name: str) -> Path:
        """Get path to VM metadata file."""
        return self._get_vm_dir(name) / "metadata.json"

    def _save_metadata(self, name: str, metadata: dict) -> None:
        """Save VM metadata."""
        path = self._get_metadata_path(name)
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(json.dumps(metadata, indent=2, default=str))

    def _load_metadata(self, name: str) -> dict | None:
        """Load VM metadata."""
        path = self._get_metadata_path(name)
        if path.exists():
            return json.loads(path.read_text())
        return None

    def _download_cloud_image(self, distro: str) -> Path:
        """Download a cloud image if not cached."""
        from gaol.distro import get_cloud_image_url

        try:
            url = get_cloud_image_url(distro)
        except KeyError as e:
            raise RuntimeError(f"Unsupported distro for libvirt: {distro}") from e

        filename = f"{distro}-cloud.qcow2"
        image_path = IMAGES_DIR / filename

        if image_path.exists():
            return image_path

        # Download the image
        result = subprocess.run(
            ["curl", "-L", "-o", str(image_path), url],
            capture_output=True,
            text=True,
        )
        if result.returncode != 0:
            raise RuntimeError(f"Failed to download cloud image: {result.stderr}")

        return image_path

    def _create_cloud_init(
        self,
        vm_dir: Path,
        name: str,
        config: ContainerConfig,
        merged_profile: MergedProfile | None,
    ) -> Path:
        """Create cloud-init ISO for VM provisioning."""
        cloud_init_dir = vm_dir / "cloud-init"
        cloud_init_dir.mkdir(parents=True, exist_ok=True)

        # Generate setup script from profiles
        setup_script = ""
        if merged_profile:
            setup_script = merged_profile.generate_setup_script(config.distro)

        # Meta-data
        meta_data = f"instance-id: {name}\nlocal-hostname: {name}\n"
        (cloud_init_dir / "meta-data").write_text(meta_data)

        # User-data with cloud-config
        packages = []
        if merged_profile:
            packages = list(merged_profile.packages)

        user_data = f"""#cloud-config
hostname: {name}
manage_etc_hosts: true

users:
  - name: gaol
    sudo: ALL=(ALL) NOPASSWD:ALL
    shell: /bin/bash
    ssh_authorized_keys: []

packages:
  - curl
  - wget
  - vim
  - openssh-server
"""
        if packages:
            user_data += "  - " + "\n  - ".join(packages) + "\n"

        if setup_script:
            user_data += f"""
runcmd:
  - |
{self._indent_script(setup_script, 4)}
"""

        # Add environment variables
        if config.environment:
            env_lines = [f'  - echo "{k}={v}" >> /etc/environment' for k, v in config.environment.items()]
            if "runcmd:" not in user_data:
                user_data += "\nruncmd:\n"
            user_data += "\n".join(env_lines) + "\n"

        (cloud_init_dir / "user-data").write_text(user_data)

        # Create cloud-init ISO
        iso_path = vm_dir / "cloud-init.iso"
        result = subprocess.run(
            [
                "genisoimage",
                "-output", str(iso_path),
                "-volid", "cidata",
                "-joliet",
                "-rock",
                str(cloud_init_dir / "user-data"),
                str(cloud_init_dir / "meta-data"),
            ],
            capture_output=True,
            text=True,
        )

        # Fallback to mkisofs if genisoimage not available
        if result.returncode != 0:
            result = subprocess.run(
                [
                    "mkisofs",
                    "-output", str(iso_path),
                    "-volid", "cidata",
                    "-joliet",
                    "-rock",
                    str(cloud_init_dir / "user-data"),
                    str(cloud_init_dir / "meta-data"),
                ],
                capture_output=True,
                text=True,
            )

        if result.returncode != 0:
            raise RuntimeError(f"Failed to create cloud-init ISO: {result.stderr}")

        return iso_path

    def _indent_script(self, script: str, spaces: int) -> str:
        """Indent a script for embedding in YAML."""
        indent = " " * spaces
        return "\n".join(indent + line for line in script.splitlines())

    def _generate_domain_xml(
        self,
        name: str,
        disk_path: Path,
        cloud_init_iso: Path,
        config: ContainerConfig,
    ) -> str:
        """Generate libvirt domain XML."""
        memory_mb = config.resources.memory_mb or DEFAULT_MEMORY_MB
        vcpus = int(config.resources.cpu_count or DEFAULT_VCPUS)
        vm_uuid = str(uuid.uuid4())

        # Network configuration
        network_xml = '<interface type="network"><source network="default"/></interface>'
        if config.network.mode == NetworkMode.BRIDGED:
            network_xml = '<interface type="bridge"><source bridge="br0"/></interface>'
        elif config.network.mode == NetworkMode.HOST:
            network_xml = '<interface type="direct" trustGuestRxFilters="yes"><source dev="eth0" mode="passthrough"/></interface>'

        # GPU passthrough configuration
        gpu_xml = ""
        if config.gpu and config.gpu.enabled:
            gpu_xml = self._generate_gpu_passthrough_xml(config)

        domain_xml = f"""<domain type='kvm'>
  <name>{name}</name>
  <uuid>{vm_uuid}</uuid>
  <metadata>
    <{LABEL_PREFIX}:gaol xmlns:{LABEL_PREFIX}="{LABEL_PREFIX}">
      <{LABEL_PREFIX}:managed>true</{LABEL_PREFIX}:managed>
      <{LABEL_PREFIX}:distro>{config.distro}</{LABEL_PREFIX}:distro>
      <{LABEL_PREFIX}:profiles>{','.join(config.profiles)}</{LABEL_PREFIX}:profiles>
    </{LABEL_PREFIX}:gaol>
  </metadata>
  <memory unit='MiB'>{memory_mb}</memory>
  <vcpu>{vcpus}</vcpu>
  <os>
    <type arch='x86_64'>hvm</type>
    <boot dev='hd'/>
  </os>
  <features>
    <acpi/>
    <apic/>
  </features>
  <cpu mode='host-passthrough'/>
  <clock offset='utc'/>
  <on_poweroff>destroy</on_poweroff>
  <on_reboot>restart</on_reboot>
  <on_crash>destroy</on_crash>
  <devices>
    <emulator>/usr/bin/qemu-system-x86_64</emulator>
    <disk type='file' device='disk'>
      <driver name='qemu' type='qcow2'/>
      <source file='{disk_path}'/>
      <target dev='vda' bus='virtio'/>
    </disk>
    <disk type='file' device='cdrom'>
      <driver name='qemu' type='raw'/>
      <source file='{cloud_init_iso}'/>
      <target dev='sda' bus='sata'/>
      <readonly/>
    </disk>
    {network_xml}
    <serial type='pty'>
      <target port='0'/>
    </serial>
    <console type='pty'>
      <target type='serial' port='0'/>
    </console>
    <channel type='unix'>
      <target type='virtio' name='org.qemu.guest_agent.0'/>
    </channel>
    <graphics type='vnc' port='-1' autoport='yes'/>
{gpu_xml}  </devices>
</domain>"""
        return domain_xml

    def _generate_gpu_passthrough_xml(self, config: ContainerConfig) -> str:
        """Generate XML for GPU PCI passthrough.

        Args:
            config: Container configuration with GPU settings.

        Returns:
            XML string for hostdev entries, or empty string if no GPUs.
        """
        if not config.gpu or not config.gpu.enabled:
            return ""

        from gaol.gpu import GPUDetector, GPUVendor
        from gaol.gpu.amd import AmdGPU
        from gaol.gpu.intel import IntelGPU
        from gaol.gpu.nvidia import NvidiaGPU

        # Detect GPUs and determine which to passthrough
        gpus = GPUDetector.detect_all()
        if not gpus:
            return ""

        # Filter GPUs by vendor if specified
        vendor = config.gpu.vendor
        if vendor != GPUVendor.AUTO:
            gpus = [g for g in gpus if g.vendor == vendor]

        # Filter by device IDs if specified
        if config.gpu.device_ids:
            gpus = [g for g in gpus if g.index in config.gpu.device_ids]

        if not gpus:
            return ""

        # Get PCI addresses for passthrough
        pci_addresses = [g.pci_bus_id for g in gpus if g.pci_bus_id]
        if not pci_addresses:
            return ""

        # Generate XML based on vendor
        # All vendors use the same hostdev XML format for PCI passthrough
        first_gpu = gpus[0]
        if first_gpu.vendor == GPUVendor.NVIDIA:
            libvirt_config = NvidiaGPU.get_libvirt_config(config.gpu, pci_addresses)
        elif first_gpu.vendor == GPUVendor.AMD:
            libvirt_config = AmdGPU.get_libvirt_config(config.gpu, pci_addresses)
        elif first_gpu.vendor == GPUVendor.INTEL:
            # Check for integrated GPU which cannot be passed through
            if IntelGPU.is_integrated_gpu(first_gpu.pci_bus_id):
                return ""  # Cannot passthrough integrated Intel GPU
            libvirt_config = IntelGPU.get_libvirt_config(config.gpu, pci_addresses)
        else:
            return ""

        return libvirt_config.get("hostdev_xml", "")

    def create(self, config: ContainerConfig) -> str:
        """Create a new VM."""
        # Ensure directories exist
        self._ensure_dirs()

        name = config.name
        vm_dir = self._get_vm_dir(name)

        if vm_dir.exists():
            raise RuntimeError(f"VM already exists: {name}")

        vm_dir.mkdir(parents=True, exist_ok=True)

        try:
            # Download or use cached cloud image
            base_image = self._download_cloud_image(config.distro)

            # Create disk from base image
            disk_size_gb = (config.resources.disk_mb or DEFAULT_DISK_GB * 1024) // 1024
            disk_path = vm_dir / f"{name}.qcow2"

            result = subprocess.run(
                [
                    "qemu-img", "create",
                    "-f", "qcow2",
                    "-F", "qcow2",
                    "-b", str(base_image),
                    str(disk_path),
                    f"{disk_size_gb}G",
                ],
                capture_output=True,
                text=True,
            )
            if result.returncode != 0:
                raise RuntimeError(f"Failed to create disk: {result.stderr}")

            # Get merged profile for setup
            merged_profile = None
            if config.profiles:
                applicator = get_profile_applicator()
                merged_profile = applicator.resolve_and_merge(config.profiles)

            # Create cloud-init ISO
            cloud_init_iso = self._create_cloud_init(vm_dir, name, config, merged_profile)

            # Generate and save domain XML
            domain_xml = self._generate_domain_xml(name, disk_path, cloud_init_iso, config)
            (vm_dir / "domain.xml").write_text(domain_xml)

            # Define the domain in libvirt
            dom = self.conn.defineXML(domain_xml)
            if dom is None:
                raise RuntimeError("Failed to define VM in libvirt")

            # Save metadata (including network config for port forwarding)
            self._save_metadata(name, {
                "name": name,
                "distro": config.distro,
                "profiles": config.profiles,
                "created_at": datetime.now().isoformat(),
                "uuid": dom.UUIDString(),
                "network": {
                    "mode": config.network.mode.value,
                    "ports": config.network.ports,
                    "dns": config.network.dns,
                    "hostname": config.network.hostname,
                },
            })

            return name

        except Exception as e:
            # Clean up on failure
            if vm_dir.exists():
                shutil.rmtree(vm_dir)
            raise RuntimeError(f"Failed to create VM: {e}") from e

    def start(self, container_id: str) -> None:
        """Start a VM."""
        try:
            dom = self.conn.lookupByName(container_id)
            if dom.isActive():
                return  # Already running
            dom.create()

            # Set up port forwarding if configured
            self._setup_port_forwarding(container_id)
        except Exception as e:
            raise RuntimeError(f"Failed to start VM: {e}") from e

    def stop(self, container_id: str, timeout: int = 10) -> None:
        """Stop a VM."""
        try:
            # Remove port forwarding rules before stopping
            self._teardown_port_forwarding(container_id)

            dom = self.conn.lookupByName(container_id)
            if not dom.isActive():
                return  # Already stopped

            # Try graceful shutdown first
            dom.shutdown()

            # Wait for shutdown
            import time
            for _ in range(timeout):
                time.sleep(1)
                if not dom.isActive():
                    return

            # Force stop
            dom.destroy()
        except Exception as e:
            raise RuntimeError(f"Failed to stop VM: {e}") from e

    def destroy(self, container_id: str, force: bool = False) -> None:
        """Remove a VM."""
        try:
            # Remove port forwarding rules
            self._teardown_port_forwarding(container_id)

            dom = self.conn.lookupByName(container_id)

            # Stop if running
            if dom.isActive():
                if force:
                    dom.destroy()
                else:
                    raise RuntimeError("VM is running. Use force=True to stop and remove.")

            # Undefine the domain
            dom.undefine()

            # Remove VM directory
            vm_dir = self._get_vm_dir(container_id)
            if vm_dir.exists():
                shutil.rmtree(vm_dir)

        except Exception as e:
            if "Domain not found" in str(e):
                # Clean up orphaned directory
                vm_dir = self._get_vm_dir(container_id)
                if vm_dir.exists():
                    shutil.rmtree(vm_dir)
                return
            raise RuntimeError(f"Failed to destroy VM: {e}") from e

    def exec(
        self,
        container_id: str,
        command: list[str],
        *,
        interactive: bool = False,
        tty: bool = False,
        user: str | None = None,
        workdir: str | None = None,
        environment: dict[str, str] | None = None,
    ) -> tuple[int, str, str]:
        """Execute a command in a VM via SSH or guest agent."""
        # Get VM IP address
        ip = self._get_vm_ip(container_id)
        if not ip:
            raise RuntimeError("Cannot get VM IP address. Is qemu-guest-agent installed?")

        # Build SSH command
        ssh_user = user or "gaol"
        ssh_cmd = [
            "ssh",
            "-o", "StrictHostKeyChecking=no",
            "-o", "UserKnownHostsFile=/dev/null",
            "-o", "ConnectTimeout=10",
            f"{ssh_user}@{ip}",
        ]

        if workdir:
            command = ["cd", workdir, "&&"] + command

        # Add environment variables
        if environment:
            env_prefix = " ".join(f"{k}={v}" for k, v in environment.items())
            command = ["env", env_prefix] + command

        ssh_cmd.extend(command)

        if interactive and tty:
            # Run interactively
            result = subprocess.run(ssh_cmd)
            return result.returncode, "", ""
        else:
            result = subprocess.run(ssh_cmd, capture_output=True, text=True)
            return result.returncode, result.stdout, result.stderr

    def _get_vm_ip(self, container_id: str) -> str | None:
        """Get VM IP address via libvirt."""
        try:
            import libvirt

            dom = self.conn.lookupByName(container_id)
            if not dom.isActive():
                return None

            # Try guest agent first
            try:
                ifaces = dom.interfaceAddresses(libvirt.VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_AGENT)
                for iface_info in ifaces.values():
                    for addr in iface_info.get("addrs", []):
                        if addr["type"] == 0:  # IPv4
                            return addr["addr"]
            except libvirt.libvirtError:
                pass

            # Fallback to lease info
            try:
                ifaces = dom.interfaceAddresses(libvirt.VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_LEASE)
                for iface_info in ifaces.values():
                    for addr in iface_info.get("addrs", []):
                        if addr["type"] == 0:  # IPv4
                            return addr["addr"]
            except libvirt.libvirtError:
                pass

            return None
        except Exception:
            return None

    def _wait_for_ip(self, container_id: str, timeout: int = 60) -> str | None:
        """Wait for VM to obtain an IP address.

        Args:
            container_id: VM name
            timeout: Maximum seconds to wait

        Returns:
            IP address or None if not obtained in time
        """
        import time

        start = time.time()
        while time.time() - start < timeout:
            ip = self._get_vm_ip(container_id)
            if ip:
                return ip
            time.sleep(2)
        return None

    def _setup_port_forwarding(self, container_id: str) -> None:
        """Set up port forwarding rules for a VM.

        Args:
            container_id: VM name
        """
        metadata = self._load_metadata(container_id)
        if not metadata:
            return

        network = metadata.get("network", {})
        ports = network.get("ports", {})
        if not ports:
            return

        # Wait for VM to get an IP address
        ip = self._wait_for_ip(container_id, timeout=60)
        if not ip:
            # Log warning but don't fail - VM may not have agent installed
            return

        try:
            from gaol.networking import (
                NetworkConfigV2,
                PortMapping,
                PortMappingList,
                get_network_manager,
            )

            # Convert legacy ports dict to PortMappingList
            mappings = [
                PortMapping(host_port=int(hp), container_port=int(cp))
                for hp, cp in ports.items()
            ]

            if mappings:
                network_config = NetworkConfigV2(
                    ports=PortMappingList(mappings=mappings)
                )
                manager = get_network_manager()
                manager.setup_container_network(container_id, network_config, ip)
        except Exception:
            # Don't fail VM start if port forwarding fails
            pass

    def _teardown_port_forwarding(self, container_id: str) -> None:
        """Remove port forwarding rules for a VM.

        Args:
            container_id: VM name
        """
        # Get current IP before stopping
        ip = self._get_vm_ip(container_id)
        if not ip:
            return

        try:
            from gaol.networking import get_network_manager

            manager = get_network_manager()
            manager.firewall.clear_rules_for_vm(ip)
        except Exception:
            # Don't fail VM stop if cleanup fails
            pass

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

        try:
            # Get all defined domains
            domains = self.conn.listAllDomains() if all else self.conn.listAllDomains(1)

            for dom in domains:
                name = dom.name()

                # Check if managed by gaol
                metadata = self._load_metadata(name)
                if metadata is None:
                    continue

                state, _ = dom.state()
                status = self._libvirt_state_to_status(state)

                states.append(ContainerState(
                    id=dom.UUIDString(),
                    name=name,
                    runtime="libvirt",
                    status=status,
                    image=metadata.get("distro", "unknown"),
                    created_at=datetime.fromisoformat(metadata["created_at"]) if metadata.get("created_at") else None,
                    ip_address=self._get_vm_ip(name) if status == ContainerStatus.RUNNING else None,
                    profiles=metadata.get("profiles", []),
                ))

        except Exception as e:
            raise RuntimeError(f"Failed to list VMs: {e}") from e

        return states

    def _libvirt_state_to_status(self, state: int) -> ContainerStatus:
        """Convert libvirt domain state to ContainerStatus."""
        import libvirt

        state_map = {
            libvirt.VIR_DOMAIN_NOSTATE: ContainerStatus.UNKNOWN,
            libvirt.VIR_DOMAIN_RUNNING: ContainerStatus.RUNNING,
            libvirt.VIR_DOMAIN_BLOCKED: ContainerStatus.RUNNING,
            libvirt.VIR_DOMAIN_PAUSED: ContainerStatus.PAUSED,
            libvirt.VIR_DOMAIN_SHUTDOWN: ContainerStatus.STOPPED,
            libvirt.VIR_DOMAIN_SHUTOFF: ContainerStatus.STOPPED,
            libvirt.VIR_DOMAIN_CRASHED: ContainerStatus.DEAD,
            libvirt.VIR_DOMAIN_PMSUSPENDED: ContainerStatus.PAUSED,
        }
        return state_map.get(state, ContainerStatus.UNKNOWN)

    def status(self, container_id: str) -> ContainerState:
        """Get VM status."""
        try:
            dom = self.conn.lookupByName(container_id)
            metadata = self._load_metadata(container_id)

            state, _ = dom.state()
            status = self._libvirt_state_to_status(state)

            return ContainerState(
                id=dom.UUIDString(),
                name=container_id,
                runtime="libvirt",
                status=status,
                image=metadata.get("distro", "unknown") if metadata else "unknown",
                created_at=datetime.fromisoformat(metadata["created_at"]) if metadata and metadata.get("created_at") else None,
                ip_address=self._get_vm_ip(container_id) if status == ContainerStatus.RUNNING else None,
                profiles=metadata.get("profiles", []) if metadata else [],
            )
        except Exception as e:
            raise RuntimeError(f"VM not found: {container_id}") from e

    def logs(
        self,
        container_id: str,
        *,
        follow: bool = False,  # noqa: ARG002
        tail: int | None = None,
        since: str | None = None,  # noqa: ARG002
    ) -> str:
        """Get VM console logs."""
        try:
            # Verify VM exists
            self.conn.lookupByName(container_id)

            # Try to get console output from serial port
            # This requires the VM to have serial console configured
            vm_dir = self._get_vm_dir(container_id)
            console_log = vm_dir / "console.log"

            if console_log.exists():
                content = console_log.read_text()
                if tail:
                    lines = content.splitlines()
                    content = "\n".join(lines[-tail:])
                return content

            return "Console logging not available for this VM"
        except Exception as e:
            raise RuntimeError(f"Failed to get logs: {e}") from e

    def pause(self, container_id: str) -> None:
        """Pause a VM."""
        try:
            dom = self.conn.lookupByName(container_id)
            dom.suspend()
        except Exception as e:
            raise RuntimeError(f"Failed to pause VM: {e}") from e

    def unpause(self, container_id: str) -> None:
        """Resume a paused VM."""
        try:
            dom = self.conn.lookupByName(container_id)
            dom.resume()
        except Exception as e:
            raise RuntimeError(f"Failed to resume VM: {e}") from e

    def copy_to(self, container_id: str, src: str, dest: str) -> None:
        """Copy files to VM via SCP."""
        ip = self._get_vm_ip(container_id)
        if not ip:
            raise RuntimeError("Cannot get VM IP address")

        result = subprocess.run(
            [
                "scp",
                "-o", "StrictHostKeyChecking=no",
                "-o", "UserKnownHostsFile=/dev/null",
                "-r",
                src,
                f"gaol@{ip}:{dest}",
            ],
            capture_output=True,
            text=True,
        )
        if result.returncode != 0:
            raise RuntimeError(f"Failed to copy to VM: {result.stderr}")

    def copy_from(self, container_id: str, src: str, dest: str) -> None:
        """Copy files from VM via SCP."""
        ip = self._get_vm_ip(container_id)
        if not ip:
            raise RuntimeError("Cannot get VM IP address")

        result = subprocess.run(
            [
                "scp",
                "-o", "StrictHostKeyChecking=no",
                "-o", "UserKnownHostsFile=/dev/null",
                "-r",
                f"gaol@{ip}:{src}",
                dest,
            ],
            capture_output=True,
            text=True,
        )
        if result.returncode != 0:
            raise RuntimeError(f"Failed to copy from VM: {result.stderr}")

    def setup(self, container_id: str) -> tuple[int, str, str]:  # noqa: ARG002
        """Setup is done via cloud-init during VM creation."""
        # Cloud-init handles the setup automatically
        return 0, "Setup handled by cloud-init", ""

    def snapshot_create(
        self, container_id: str, name: str, description: str | None = None
    ) -> Snapshot:
        """Create a VM snapshot."""
        from gaol.models.container import Snapshot

        try:
            dom = self.conn.lookupByName(container_id)

            # Create snapshot XML
            snap_xml = f"""<domainsnapshot>
  <name>{name}</name>
  <description>{description or ''}</description>
</domainsnapshot>"""

            import libvirt
            dom.snapshotCreateXML(snap_xml, libvirt.VIR_DOMAIN_SNAPSHOT_CREATE_ATOMIC)

            return Snapshot(
                id=f"{container_id}-{name}",
                name=name,
                container_id=dom.UUIDString(),
                container_name=container_id,
                created_at=datetime.now(),
                description=description,
            )
        except Exception as e:
            raise RuntimeError(f"Failed to create snapshot: {e}") from e

    def snapshot_list(self, container_id: str) -> list[Snapshot]:
        """List VM snapshots."""
        from gaol.models.container import Snapshot

        snapshots = []
        try:
            dom = self.conn.lookupByName(container_id)

            for snap in dom.listAllSnapshots():
                # Get snapshot name
                name = snap.getName()

                snapshots.append(Snapshot(
                    id=f"{container_id}-{name}",
                    name=name,
                    container_id=dom.UUIDString(),
                    container_name=container_id,
                    created_at=datetime.now(),  # Would need to parse from XML
                ))

        except Exception as e:
            raise RuntimeError(f"Failed to list snapshots: {e}") from e

        return snapshots

    def snapshot_restore(self, container_id: str, snapshot_name: str) -> None:
        """Restore VM from snapshot."""
        try:
            dom = self.conn.lookupByName(container_id)
            snap = dom.snapshotLookupByName(snapshot_name)
            dom.revertToSnapshot(snap)
        except Exception as e:
            raise RuntimeError(f"Failed to restore snapshot: {e}") from e

    def snapshot_delete(self, container_id: str, snapshot_name: str) -> None:
        """Delete a VM snapshot."""
        try:
            dom = self.conn.lookupByName(container_id)
            snap = dom.snapshotLookupByName(snapshot_name)
            snap.delete()
        except Exception as e:
            raise RuntimeError(f"Failed to delete snapshot: {e}") from e

    def stats(self, container_id: str) -> ContainerStats:
        """Get VM resource statistics."""
        try:
            dom = self.conn.lookupByName(container_id)

            if not dom.isActive():
                raise RuntimeError(f"VM not running: {container_id}")

            # Get CPU stats (cpu_time needs two samples for percentage)
            dom.getCPUStats(True)

            # Get memory stats
            mem_stats = dom.memoryStats()
            memory_used = mem_stats.get("rss", 0) * 1024  # KB to bytes
            memory_limit = dom.maxMemory() * 1024  # KB to bytes

            # Get block stats
            block_read = 0
            block_write = 0
            try:
                # Get block device path from domain XML
                xml = dom.XMLDesc()
                if "vda" in xml:
                    block_stats = dom.blockStats("vda")
                    block_read = block_stats[1]  # rd_bytes
                    block_write = block_stats[3]  # wr_bytes
            except Exception:
                pass

            # Get network stats
            network_rx = 0
            network_tx = 0
            try:
                # Try to get network interface stats
                import libvirt
                ifaces = dom.interfaceAddresses(libvirt.VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_LEASE)
                for iface_name in ifaces:
                    try:
                        net_stats = dom.interfaceStats(iface_name)
                        network_rx += net_stats[0]  # rx_bytes
                        network_tx += net_stats[4]  # tx_bytes
                    except Exception:
                        pass
            except Exception:
                pass

            # Get vCPU count
            vcpu_count = dom.vcpusFlags()

            return ContainerStats(
                container_id=dom.UUIDString(),
                container_name=container_id,
                cpu_percent=0.0,  # Would need two samples to calculate
                cpu_count=vcpu_count,
                memory_used_bytes=memory_used,
                memory_limit_bytes=memory_limit,
                memory_percent=(memory_used / memory_limit * 100) if memory_limit > 0 else 0,
                network_rx_bytes=network_rx,
                network_tx_bytes=network_tx,
                block_read_bytes=block_read,
                block_write_bytes=block_write,
                pids=0,  # Not easily available for VMs
            )

        except Exception as e:
            raise RuntimeError(f"Failed to get stats: {e}") from e

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

        Args:
            container_id: Name of the VM to export
            dest_path: Directory to write the export file to
            compression: Compression type (None, "gzip", "xz")

        Returns:
            Path to the exported tarball file

        Raises:
            RuntimeError: If export fails
        """
        # Check for virt-tar-out (from libguestfs-tools)
        if not shutil.which("virt-tar-out"):
            raise RuntimeError(
                "virt-tar-out not found. Install libguestfs-tools: "
                "apt install libguestfs-tools"
            )

        # Get the disk path
        vm_dir = self._get_vm_dir(container_id)
        disk_path = vm_dir / f"{container_id}.qcow2"

        if not disk_path.exists():
            raise RuntimeError(f"VM disk not found: {disk_path}")

        # Check if running and stop if needed
        was_running = False
        try:
            dom = self.conn.lookupByName(container_id)
            if dom.isActive():
                was_running = True
                self.stop(container_id)
        except Exception:
            pass

        try:
            # Determine output filename and compression flag
            dest_dir = Path(dest_path)
            dest_dir.mkdir(parents=True, exist_ok=True)

            if compression == "gzip":
                output_file = dest_dir / f"{container_id}.tar.gz"
            elif compression == "xz":
                output_file = dest_dir / f"{container_id}.tar.xz"
            else:
                output_file = dest_dir / f"{container_id}.tar"

            # Use virt-tar-out to extract filesystem from QCOW2
            # virt-tar-out -a disk.qcow2 / output.tar
            cmd = ["virt-tar-out", "-a", str(disk_path), "/", str(output_file)]

            result = subprocess.run(cmd, capture_output=True, text=True)
            if result.returncode != 0:
                raise RuntimeError(f"virt-tar-out failed: {result.stderr}")

            # Apply compression if specified (virt-tar-out doesn't do compression)
            if compression == "gzip":
                uncompressed = dest_dir / f"{container_id}.tar"
                if uncompressed.exists():
                    # virt-tar-out created uncompressed tar
                    subprocess.run(["gzip", "-f", str(uncompressed)], check=True)
            elif compression == "xz":
                uncompressed = dest_dir / f"{container_id}.tar"
                if uncompressed.exists():
                    subprocess.run(["xz", "-f", str(uncompressed)], check=True)

            return str(output_file)

        finally:
            # Restart VM if it was running
            if was_running:
                try:
                    self.start(container_id)
                except Exception:
                    pass

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

        Uses guestfish to create a disk and populate it with the tarball contents.

        Args:
            config: Container configuration for the new VM
            source_path: Path to the tarball to import

        Returns:
            VM name (container ID)

        Raises:
            RuntimeError: If import fails
        """
        # Check for guestfish
        if not shutil.which("guestfish"):
            raise RuntimeError(
                "guestfish not found. Install libguestfs-tools: "
                "apt install libguestfs-tools"
            )

        name = config.name
        vm_dir = self._get_vm_dir(name)

        if vm_dir.exists():
            raise RuntimeError(f"VM already exists: {name}")

        vm_dir.mkdir(parents=True, exist_ok=True)

        # Ensure directories exist
        self._ensure_dirs()

        try:
            # Determine disk size from tarball or use default
            disk_size_gb = (config.resources.disk_mb or DEFAULT_DISK_GB * 1024) // 1024
            disk_path = vm_dir / f"{name}.qcow2"

            # Decompress tarball if needed
            source = Path(source_path)
            tar_path = source

            if source.suffix == ".gz" or source.name.endswith(".tar.gz"):
                tar_path = vm_dir / "import.tar"
                result = subprocess.run(
                    ["gunzip", "-c", str(source)],
                    stdout=open(tar_path, "wb"),
                    stderr=subprocess.PIPE,
                )
                if result.returncode != 0:
                    raise RuntimeError(f"Failed to decompress: {result.stderr.decode()}")
            elif source.suffix == ".xz" or source.name.endswith(".tar.xz"):
                tar_path = vm_dir / "import.tar"
                result = subprocess.run(
                    ["xz", "-dc", str(source)],
                    stdout=open(tar_path, "wb"),
                    stderr=subprocess.PIPE,
                )
                if result.returncode != 0:
                    raise RuntimeError(f"Failed to decompress: {result.stderr.decode()}")

            # Create QCOW2 disk
            result = subprocess.run(
                ["qemu-img", "create", "-f", "qcow2", str(disk_path), f"{disk_size_gb}G"],
                capture_output=True,
                text=True,
            )
            if result.returncode != 0:
                raise RuntimeError(f"Failed to create disk: {result.stderr}")

            # Use guestfish to partition, format, and extract tarball
            # This creates an ext4 filesystem and extracts the tarball into it
            guestfish_commands = f"""
add {disk_path}
run
part-disk /dev/sda mbr
mkfs ext4 /dev/sda1
mount /dev/sda1 /
tar-in {tar_path} / compress:
"""
            result = subprocess.run(
                ["guestfish"],
                input=guestfish_commands,
                capture_output=True,
                text=True,
            )
            if result.returncode != 0:
                raise RuntimeError(f"guestfish failed: {result.stderr}")

            # Clean up temporary tar file
            temp_tar = vm_dir / "import.tar"
            if temp_tar.exists():
                temp_tar.unlink()

            # Get merged profile for cloud-init (for package installs, etc.)
            merged_profile = None
            if config.profiles:
                applicator = get_profile_applicator()
                merged_profile = applicator.resolve_and_merge(config.profiles)

            # Create cloud-init ISO for networking and basic setup
            cloud_init_iso = self._create_cloud_init(vm_dir, name, config, merged_profile)

            # Generate and save domain XML
            domain_xml = self._generate_domain_xml(name, disk_path, cloud_init_iso, config)
            (vm_dir / "domain.xml").write_text(domain_xml)

            # Define the domain in libvirt
            dom = self.conn.defineXML(domain_xml)
            if dom is None:
                raise RuntimeError("Failed to define VM in libvirt")

            # Save metadata
            self._save_metadata(name, {
                "name": name,
                "distro": config.distro,
                "profiles": config.profiles,
                "created_at": datetime.now().isoformat(),
                "uuid": dom.UUIDString(),
                "migrated_from": source_path,
                "network": {
                    "mode": config.network.mode.value,
                    "ports": config.network.ports,
                    "dns": config.network.dns,
                    "hostname": config.network.hostname,
                },
            })

            return name

        except Exception as e:
            # Clean up on failure
            if vm_dir.exists():
                shutil.rmtree(vm_dir)
            raise RuntimeError(f"Failed to import filesystem: {e}") from e

    def get_filesystem_size(self, container_id: str) -> int:
        """Get the size of a VM's disk image.

        Args:
            container_id: Name of the VM

        Returns:
            Size in bytes

        Raises:
            RuntimeError: If VM not found
        """
        vm_dir = self._get_vm_dir(container_id)
        disk_path = vm_dir / f"{container_id}.qcow2"

        if not disk_path.exists():
            raise RuntimeError(f"VM disk not found: {disk_path}")

        # Use qemu-img info to get virtual size (actual used space may be smaller)
        result = subprocess.run(
            ["qemu-img", "info", "--output=json", str(disk_path)],
            capture_output=True,
            text=True,
        )
        if result.returncode != 0:
            # Fall back to file size
            return disk_path.stat().st_size

        try:
            info = json.loads(result.stdout)
            # Return actual disk size, not virtual size
            return info.get("actual-size", disk_path.stat().st_size)
        except Exception:
            return disk_path.stat().st_size
