"""Template container manager.

Manages template containers and spawned instances using OverlayFS.
"""

from __future__ import annotations

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

from gaol.domains.exceptions import (
    TemplateExistsError,
    TemplateInUseError,
    TemplateNotFoundError,
    TemplateSpawnError,
)
from gaol.domains.models import (
    TemplateConfig,
    TemplateInstance,
    TemplateState,
    TemplateType,
)
from gaol.domains.store import DomainsFileStore, get_domain_store

if TYPE_CHECKING:
    from gaol.models.container import ContainerConfig

# Base directories
TEMPLATES_DIR = Path("/var/lib/gaol/templates")
INSTANCES_DIR = Path("/var/lib/gaol/instances")
MACHINES_DIR = Path("/var/lib/machines")


class TemplateManager:
    """Manage template containers and spawn instances.

    Templates are read-only base containers that can spawn multiple
    instances using OverlayFS. Each instance gets its own overlay
    for changes while sharing the template's root filesystem.

    Example:
        >>> manager = TemplateManager()
        >>> config = TemplateConfig(name="debian-dev", profiles=["base", "dev-python"])
        >>> template = manager.create_template(config)
        >>> instance = manager.spawn("debian-dev", "my-project")
        >>> manager.start_instance(instance.name)
    """

    def __init__(self, store: DomainsFileStore | None = None) -> None:
        """Initialize the template manager."""
        self._store = store or get_domain_store()

    def _run_cmd(
        self,
        cmd: list[str],
        *,
        check: bool = True,
        capture_output: bool = True,
    ) -> subprocess.CompletedProcess[str]:
        """Run a command and return the result."""
        try:
            result = subprocess.run(
                cmd,
                check=check,
                capture_output=capture_output,
                text=True,
            )
            return result
        except subprocess.CalledProcessError as e:
            raise RuntimeError(f"Command failed: {' '.join(cmd)}\n{e.stderr}") from e

    def _get_template_path(self, name: str) -> Path:
        """Get the path to a template's root filesystem."""
        return TEMPLATES_DIR / name

    def _get_instance_overlay_path(self, name: str) -> Path:
        """Get the path to an instance's overlay upperdir."""
        return INSTANCES_DIR / name / "upper"

    def _get_instance_work_path(self, name: str) -> Path:
        """Get the path to an instance's overlay workdir."""
        return INSTANCES_DIR / name / "work"

    def _get_instance_merged_path(self, name: str) -> Path:
        """Get the path to an instance's merged mount (in /var/lib/machines)."""
        return MACHINES_DIR / name

    def create_template(self, config: TemplateConfig) -> TemplateState:
        """Create a new template container.

        Bootstraps a root filesystem and applies profiles.

        Args:
            config: Template configuration.

        Returns:
            New template state.

        Raises:
            TemplateExistsError: If template already exists.
        """
        if self._store.template_exists(config.name):
            raise TemplateExistsError(config.name)

        template_path = self._get_template_path(config.name)

        # Create template directory
        self._run_cmd(["sudo", "mkdir", "-p", str(template_path)])

        try:
            # Bootstrap using debootstrap
            self._bootstrap_template(config.name, config.distro)

            # Install base packages
            self._install_base_packages(config.name)

            # Apply profiles if any
            if config.profiles:
                self._apply_profiles(config.name, config.profiles)

            # Install additional packages
            if config.packages:
                self._install_packages(config.name, config.packages)

            # Get size
            size_bytes = self._get_directory_size(template_path)

            # Create state
            state = TemplateState(
                name=config.name,
                distro=config.distro,
                template_type=config.template_type,
                profiles=list(config.profiles),
                root_path=template_path,
                created_at=datetime.now(),
                size_bytes=size_bytes,
                description=config.description,
                default_memory_mb=config.default_memory_mb,
                default_cpu_count=config.default_cpu_count,
            )

            self._store.save_template(state)
            return state

        except Exception as e:
            # Clean up on failure
            if template_path.exists():
                self._run_cmd(["sudo", "rm", "-rf", str(template_path)], check=False)
            raise RuntimeError(f"Failed to create template: {e}") from e

    def _bootstrap_template(self, name: str, distro: str) -> None:
        """Bootstrap a template root filesystem using debootstrap."""
        from gaol.distro import get_debootstrap_cmd, get_mirror

        template_path = self._get_template_path(name)

        bootstrap_cmd = get_debootstrap_cmd(distro)
        mirror = get_mirror(distro) or "http://deb.debian.org/debian"

        cmd_parts = bootstrap_cmd.split()
        full_cmd = ["sudo"] + cmd_parts + [str(template_path), mirror]

        self._run_cmd(full_cmd, capture_output=False)

    def _install_base_packages(self, name: str) -> None:
        """Install base packages in template."""
        template_path = self._get_template_path(name)
        packages = ["systemd", "systemd-sysv", "dbus", "systemd-resolved"]

        self._run_cmd(
            ["sudo", "systemd-nspawn", "-D", str(template_path), "apt-get", "update"],
            capture_output=False,
        )

        self._run_cmd(
            [
                "sudo", "systemd-nspawn", "-D", str(template_path),
                "env", "DEBIAN_FRONTEND=noninteractive",
                "apt-get", "install", "-y", *packages,
            ],
            capture_output=False,
        )

    def _apply_profiles(self, name: str, profiles: list[str]) -> None:
        """Apply profiles to template."""
        from gaol.profiles.applicator import get_profile_applicator

        template_path = self._get_template_path(name)
        applicator = get_profile_applicator()
        merged = applicator.resolve_and_merge(profiles)

        # Install packages from profiles
        packages = list(merged.packages)
        if packages:
            self._run_cmd(
                [
                    "sudo", "systemd-nspawn", "-D", str(template_path),
                    "env", "DEBIAN_FRONTEND=noninteractive",
                    "apt-get", "install", "-y", *packages,
                ],
                capture_output=False,
            )

        # Run setup commands
        for cmd in merged.setup_commands:
            self._run_cmd(
                ["sudo", "systemd-nspawn", "-D", str(template_path), "bash", "-c", cmd],
                check=False,
            )

    def _install_packages(self, name: str, packages: list[str]) -> None:
        """Install additional packages in template."""
        template_path = self._get_template_path(name)
        self._run_cmd(
            [
                "sudo", "systemd-nspawn", "-D", str(template_path),
                "env", "DEBIAN_FRONTEND=noninteractive",
                "apt-get", "install", "-y", *packages,
            ],
            capture_output=False,
        )

    def _get_directory_size(self, path: Path) -> int:
        """Get total size of a directory in bytes."""
        result = self._run_cmd(["sudo", "du", "-sb", str(path)])
        return int(result.stdout.split()[0])

    def list_templates(
        self, template_type: TemplateType | None = None
    ) -> list[TemplateState]:
        """List all templates.

        Args:
            template_type: Optional filter by type.

        Returns:
            List of template states.
        """
        templates = self._store.list_templates()
        if template_type:
            templates = [t for t in templates if t.template_type == template_type]
        return templates

    def get_template(self, name: str) -> TemplateState:
        """Get a template by name.

        Args:
            name: Template name.

        Returns:
            Template state.

        Raises:
            TemplateNotFoundError: If template doesn't exist.
        """
        template = self._store.get_template(name)
        if not template:
            raise TemplateNotFoundError(name)
        return template

    def delete_template(self, name: str, force: bool = False) -> None:
        """Delete a template.

        Args:
            name: Template name.
            force: Force deletion even if instances exist.

        Raises:
            TemplateNotFoundError: If template doesn't exist.
            TemplateInUseError: If template has instances and force=False.
        """
        template = self.get_template(name)

        if template.instances and not force:
            raise TemplateInUseError(name, len(template.instances))

        # Stop and delete any instances
        if force:
            for instance_name in list(template.instances):
                try:
                    self.destroy_instance(instance_name)
                except Exception:
                    pass  # Best effort

        # Delete template filesystem
        template_path = self._get_template_path(name)
        if template_path.exists():
            self._run_cmd(["sudo", "rm", "-rf", str(template_path)])

        self._store.delete_template(name)

    def update_template(self, name: str) -> TemplateState:
        """Update a template (run apt upgrade).

        Args:
            name: Template name.

        Returns:
            Updated template state.

        Raises:
            TemplateNotFoundError: If template doesn't exist.
        """
        template = self.get_template(name)
        template_path = self._get_template_path(name)

        # Update packages
        self._run_cmd(
            ["sudo", "systemd-nspawn", "-D", str(template_path), "apt-get", "update"],
            capture_output=False,
        )
        self._run_cmd(
            [
                "sudo", "systemd-nspawn", "-D", str(template_path),
                "env", "DEBIAN_FRONTEND=noninteractive",
                "apt-get", "dist-upgrade", "-y",
            ],
            capture_output=False,
        )

        # Update state
        template.version += 1
        template.updated_at = datetime.now()
        template.size_bytes = self._get_directory_size(template_path)

        self._store.save_template(template)
        return template

    def spawn(
        self,
        template_name: str,
        instance_name: str,
        *,
        ephemeral: bool = False,
        preserve_paths: list[str] | None = None,
    ) -> TemplateInstance:
        """Spawn a new instance from a template.

        Creates an overlay filesystem on top of the template.

        Args:
            template_name: Name of the source template.
            instance_name: Name for the new instance.
            ephemeral: If True, instance is destroyed on stop.
            preserve_paths: Paths to preserve even for ephemeral instances.

        Returns:
            New instance.

        Raises:
            TemplateNotFoundError: If template doesn't exist.
            TemplateSpawnError: If spawn fails.
        """
        template = self.get_template(template_name)

        # Check if instance already exists
        instance_merged = self._get_instance_merged_path(instance_name)
        if instance_merged.exists():
            raise TemplateSpawnError(
                template_name, instance_name, "Instance already exists"
            )

        try:
            # Create instance directories
            overlay_path = self._get_instance_overlay_path(instance_name)
            work_path = self._get_instance_work_path(instance_name)

            self._run_cmd(["sudo", "mkdir", "-p", str(overlay_path)])
            self._run_cmd(["sudo", "mkdir", "-p", str(work_path)])
            self._run_cmd(["sudo", "mkdir", "-p", str(instance_merged)])

            # Mount overlay
            self._mount_overlay(
                template.root_path,
                overlay_path,
                work_path,
                instance_merged,
            )

            # Create nspawn config file
            self._create_instance_nspawn_config(instance_name, ephemeral)

            # Create instance record
            instance = TemplateInstance(
                name=instance_name,
                template_name=template_name,
                template_version=template.version,
                ephemeral=ephemeral,
                overlay_path=overlay_path,
                work_path=work_path,
                preserve_paths=preserve_paths or [],
            )

            # Track instance
            self._store.add_template_instance(template_name, instance_name)

            return instance

        except Exception as e:
            # Clean up on failure
            self._cleanup_instance(instance_name)
            raise TemplateSpawnError(template_name, instance_name, str(e)) from e

    def _mount_overlay(
        self,
        lower: Path,
        upper: Path,
        work: Path,
        merged: Path,
    ) -> None:
        """Mount an overlay filesystem."""
        options = f"lowerdir={lower},upperdir={upper},workdir={work}"
        self._run_cmd([
            "sudo", "mount", "-t", "overlay", "overlay",
            "-o", options,
            str(merged),
        ])

    def _create_instance_nspawn_config(
        self, instance_name: str, ephemeral: bool
    ) -> None:
        """Create nspawn configuration for an instance."""
        config = """[Exec]
Boot=yes
PrivateUsers=no

[Network]
Private=no
"""
        nspawn_path = Path(f"/etc/systemd/nspawn/{instance_name}.nspawn")
        self._run_cmd(
            ["sudo", "tee", str(nspawn_path)],
            capture_output=True,
        )
        # Write content via stdin
        subprocess.run(
            ["sudo", "tee", str(nspawn_path)],
            input=config,
            text=True,
            capture_output=True,
        )

    def _cleanup_instance(self, instance_name: str) -> None:
        """Clean up instance files and mounts."""
        merged = self._get_instance_merged_path(instance_name)
        instance_dir = INSTANCES_DIR / instance_name

        # Unmount overlay
        if merged.exists():
            self._run_cmd(["sudo", "umount", str(merged)], check=False)
            self._run_cmd(["sudo", "rmdir", str(merged)], check=False)

        # Remove instance directory
        if instance_dir.exists():
            self._run_cmd(["sudo", "rm", "-rf", str(instance_dir)], check=False)

        # Remove nspawn config
        nspawn_path = Path(f"/etc/systemd/nspawn/{instance_name}.nspawn")
        if nspawn_path.exists():
            self._run_cmd(["sudo", "rm", str(nspawn_path)], check=False)

    def start_instance(self, instance_name: str) -> None:
        """Start an instance container.

        Args:
            instance_name: Name of the instance.
        """
        self._run_cmd(["sudo", "machinectl", "start", instance_name])

    def stop_instance(self, instance_name: str) -> None:
        """Stop an instance container.

        Args:
            instance_name: Name of the instance.
        """
        self._run_cmd(["sudo", "machinectl", "stop", instance_name], check=False)

    def destroy_instance(self, instance_name: str) -> None:
        """Stop and destroy an instance.

        Args:
            instance_name: Name of the instance.
        """
        # Stop if running
        self.stop_instance(instance_name)

        # Find which template this belongs to
        for template in self._store.list_templates():
            if instance_name in template.instances:
                self._store.remove_template_instance(template.name, instance_name)
                break

        # Clean up files
        self._cleanup_instance(instance_name)

    def list_instances(self, template_name: str | None = None) -> list[str]:
        """List spawned instances.

        Args:
            template_name: Optional filter by template.

        Returns:
            List of instance names.
        """
        if template_name:
            template = self.get_template(template_name)
            return list(template.instances)

        # All instances across all templates
        all_instances = []
        for template in self._store.list_templates():
            all_instances.extend(template.instances)
        return all_instances


# Global instance
_manager: TemplateManager | None = None


def get_template_manager() -> TemplateManager:
    """Get the global TemplateManager instance."""
    global _manager
    if _manager is None:
        _manager = TemplateManager()
    return _manager
