"""Disposable container manager.

Manages ephemeral containers that are destroyed after use.
"""

from __future__ import annotations

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

from gaol.domains.exceptions import (
    DisposableExistsError,
    DisposableLaunchError,
    DisposableNotFoundError,
)
from gaol.domains.models import (
    DisposableConfig,
    DisposableInstance,
    DisposableType,
)
from gaol.domains.store import DomainsFileStore, get_domain_store
from gaol.domains.template_manager import TemplateManager, get_template_manager

if TYPE_CHECKING:
    pass


class DisposableManager:
    """Manage disposable containers.

    Disposables are ephemeral containers spawned from templates.
    Each launch creates a fresh instance that is automatically
    destroyed when the application exits.

    Example:
        >>> manager = DisposableManager()
        >>> config = DisposableConfig(
        ...     name="browser",
        ...     template_name="debian-browser",
        ...     application="/usr/bin/firefox",
        ... )
        >>> manager.create(config)
        >>> instance = manager.launch("browser")
        >>> # Firefox runs, container destroyed on exit
    """

    def __init__(
        self,
        store: DomainsFileStore | None = None,
        template_manager: TemplateManager | None = None,
    ) -> None:
        """Initialize the disposable manager."""
        self._store = store or get_domain_store()
        self._template_manager = template_manager or get_template_manager()

    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 _generate_instance_id(self, disposable_name: str) -> str:
        """Generate a unique instance ID."""
        suffix = secrets.token_hex(4)
        return f"disp-{disposable_name}-{suffix}"

    def create(self, config: DisposableConfig) -> None:
        """Create a new disposable configuration.

        Args:
            config: Disposable configuration.

        Raises:
            DisposableExistsError: If disposable already exists.
            TemplateNotFoundError: If template doesn't exist.
        """
        if self._store.disposable_exists(config.name):
            raise DisposableExistsError(config.name)

        # Verify template exists
        self._template_manager.get_template(config.template_name)

        self._store.save_disposable(config)

    def get(self, name: str) -> DisposableConfig:
        """Get a disposable configuration.

        Args:
            name: Disposable name.

        Returns:
            Disposable configuration.

        Raises:
            DisposableNotFoundError: If disposable doesn't exist.
        """
        config = self._store.get_disposable(name)
        if not config:
            raise DisposableNotFoundError(name)
        return config

    def delete(self, name: str) -> None:
        """Delete a disposable configuration.

        Also terminates any running instances.

        Args:
            name: Disposable name.

        Raises:
            DisposableNotFoundError: If disposable doesn't exist.
        """
        if not self._store.disposable_exists(name):
            raise DisposableNotFoundError(name)

        # Terminate running instances
        for instance in self._store.list_disposable_instances(name):
            try:
                self.terminate(instance.instance_id)
            except Exception:
                pass  # Best effort

        self._store.delete_disposable(name)

    def list_disposables(
        self, disposable_type: DisposableType | None = None
    ) -> list[DisposableConfig]:
        """List all disposable configurations.

        Args:
            disposable_type: Optional filter by type.

        Returns:
            List of disposable configurations.
        """
        configs = self._store.list_disposables()
        if disposable_type:
            configs = [c for c in configs if c.disposable_type == disposable_type]
        return configs

    def launch(
        self,
        name: str,
        arguments: list[str] | None = None,
        *,
        wait: bool = True,
    ) -> DisposableInstance:
        """Launch a disposable container instance.

        Spawns a fresh instance from the template, runs the application,
        and destroys the container when the application exits.

        Args:
            name: Disposable name.
            arguments: Additional arguments for the application.
            wait: If True, wait for application to exit then cleanup.
                  If False, return immediately (caller must cleanup).

        Returns:
            Disposable instance.

        Raises:
            DisposableNotFoundError: If disposable doesn't exist.
            DisposableLaunchError: If launch fails.
        """
        config = self.get(name)
        instance_id = self._generate_instance_id(name)
        container_name = instance_id

        try:
            # Spawn from template
            template_instance = self._template_manager.spawn(
                config.template_name,
                container_name,
                ephemeral=True,
                preserve_paths=self._get_preserve_paths(config),
            )

            # Start the container
            self._template_manager.start_instance(container_name)

            # Wait for container to be ready
            self._wait_for_container(container_name)

            # Create instance record
            instance = DisposableInstance(
                instance_id=instance_id,
                disposable_name=name,
                template_name=config.template_name,
                container_name=container_name,
                started_at=datetime.now(),
                arguments=arguments or [],
            )

            self._store.add_disposable_instance(instance)

            # Run the application
            if config.application:
                app_args = list(config.application_args) + (arguments or [])

                if wait:
                    # Run and wait, then cleanup
                    try:
                        self._run_application(
                            container_name,
                            config.application,
                            app_args,
                        )
                    finally:
                        self.terminate(instance_id)
                else:
                    # Run in background
                    self._run_application_background(
                        container_name,
                        config.application,
                        app_args,
                    )

            return instance

        except Exception as e:
            # Clean up on failure
            try:
                self._template_manager.destroy_instance(container_name)
            except Exception:
                pass
            raise DisposableLaunchError(name, str(e)) from e

    def _get_preserve_paths(self, config: DisposableConfig) -> list[str]:
        """Get paths to preserve for a disposable."""
        paths = list(config.preserve_paths)
        if config.preserve_downloads:
            paths.append("~/Downloads")
        return paths

    def _wait_for_container(self, container_name: str, timeout: int = 30) -> None:
        """Wait for a container to be running."""
        import time

        start = time.time()
        while time.time() - start < timeout:
            result = self._run_cmd(
                ["sudo", "machinectl", "show", container_name, "--property=State"],
                check=False,
            )
            if result.returncode == 0 and "running" in result.stdout.lower():
                return
            time.sleep(0.5)

        raise RuntimeError(f"Container {container_name} failed to start")

    def _run_application(
        self,
        container_name: str,
        application: str,
        arguments: list[str],
    ) -> None:
        """Run an application in a container and wait for it to exit."""
        cmd = ["sudo", "machinectl", "shell", f"root@{container_name}"]
        cmd.extend(["--", application] + arguments)

        subprocess.run(cmd, check=False)

    def _run_application_background(
        self,
        container_name: str,
        application: str,
        arguments: list[str],
    ) -> None:
        """Run an application in a container in the background."""
        cmd = ["sudo", "machinectl", "shell", f"root@{container_name}"]
        cmd.extend(["--", application] + arguments)

        subprocess.Popen(
            cmd,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )

    def terminate(self, instance_id: str) -> None:
        """Terminate a running disposable instance.

        Args:
            instance_id: Instance ID to terminate.
        """
        instances = self._store.list_disposable_instances()
        instance = None
        for inst in instances:
            if inst.instance_id == instance_id:
                instance = inst
                break

        if not instance:
            return

        # Destroy the container
        try:
            self._template_manager.destroy_instance(instance.container_name)
        except Exception:
            pass

        # Remove tracking
        self._store.remove_disposable_instance(instance_id)

    def list_instances(
        self, disposable_name: str | None = None
    ) -> list[DisposableInstance]:
        """List running disposable instances.

        Args:
            disposable_name: Optional filter by disposable name.

        Returns:
            List of running instances.
        """
        return self._store.list_disposable_instances(disposable_name)

    def cleanup_stale(self) -> int:
        """Clean up stale instance records.

        Removes tracking for instances whose containers no longer exist.

        Returns:
            Number of stale instances cleaned up.
        """
        cleaned = 0
        for instance in self._store.list_disposable_instances():
            # Check if container exists
            result = self._run_cmd(
                ["sudo", "machinectl", "show", instance.container_name],
                check=False,
            )
            if result.returncode != 0:
                self._store.remove_disposable_instance(instance.instance_id)
                cleaned += 1

        return cleaned

    def open_file(
        self,
        file_path: str,
        disposable_name: str | None = None,
    ) -> DisposableInstance:
        """Open a file in a disposable container.

        Creates a temporary disposable if one isn't specified,
        copies the file into the container, and opens it.

        Args:
            file_path: Path to the file to open.
            disposable_name: Disposable to use (default: auto-select by MIME type).

        Returns:
            Disposable instance.
        """
        # Determine which disposable to use
        if not disposable_name:
            disposable_name = self._select_disposable_for_file(file_path)

        config = self.get(disposable_name)
        instance_id = self._generate_instance_id(disposable_name)
        container_name = instance_id

        try:
            # Spawn container
            self._template_manager.spawn(
                config.template_name,
                container_name,
                ephemeral=True,
            )
            self._template_manager.start_instance(container_name)
            self._wait_for_container(container_name)

            # Copy file into container
            dest_path = f"/tmp/{Path(file_path).name}"
            self._run_cmd([
                "sudo", "cp", file_path,
                f"/var/lib/machines/{container_name}{dest_path}",
            ])

            # Create instance record
            instance = DisposableInstance(
                instance_id=instance_id,
                disposable_name=disposable_name,
                template_name=config.template_name,
                container_name=container_name,
                started_at=datetime.now(),
                arguments=[dest_path],
            )
            self._store.add_disposable_instance(instance)

            # Open file with application (or xdg-open)
            app = config.application or "xdg-open"
            self._run_application(container_name, app, [dest_path])

            # Cleanup
            self.terminate(instance_id)

            return instance

        except Exception as e:
            try:
                self._template_manager.destroy_instance(container_name)
            except Exception:
                pass
            raise DisposableLaunchError(disposable_name or "file-viewer", str(e)) from e

    def _select_disposable_for_file(self, file_path: str) -> str:
        """Select a disposable based on file type.

        Args:
            file_path: Path to the file.

        Returns:
            Name of the disposable to use.
        """
        # Check MIME type
        import mimetypes

        mime_type, _ = mimetypes.guess_type(file_path)

        # Find matching disposable
        for config in self._store.list_disposables():
            if mime_type and mime_type in config.mime_types:
                return config.name

        # Default to file_viewer type if exists
        for config in self._store.list_disposables():
            if config.disposable_type == DisposableType.FILE_VIEWER:
                return config.name

        raise DisposableNotFoundError("No suitable disposable found for file")


# Global instance
_manager: DisposableManager | None = None


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