"""Container specification generator for service migration.

This module generates ContainerSpec objects from analyzed service
migrations, creating the necessary configuration for running the
service in a container.
"""

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING

from gaol.specs.models import (
    ContainerSpec,
    SpecNetwork,
    SpecService,
)

if TYPE_CHECKING:
    from gaol.service_migration.models import MigrationAnalysis


class SpecGenerator:
    """Generates ContainerSpec from migration analysis.

    Converts a MigrationAnalysis into a ContainerSpec suitable for
    creating a container that runs the migrated service.
    """

    def __init__(
        self,
        default_runtime: str = "nspawn",
        default_distro: str = "trixie",
        default_profiles: list[str] | None = None,
    ):
        """Initialize the generator.

        Args:
            default_runtime: Default container runtime
            default_distro: Default base distribution
            default_profiles: Default profiles to apply
        """
        self.default_runtime = default_runtime
        self.default_distro = default_distro
        self.default_profiles = default_profiles or ["base"]

    def generate(
        self,
        analysis: MigrationAnalysis,
        container_name: str | None = None,
        runtime: str | None = None,
        distro: str | None = None,
        copy_config: bool = True,
        copy_data: bool = False,
        additional_packages: list[str] | None = None,
        additional_ports: list[int] | None = None,
    ) -> ContainerSpec:
        """Generate a ContainerSpec from migration analysis.

        Args:
            analysis: Migration analysis to generate from
            container_name: Override container name (default: service name)
            runtime: Override runtime (default: from analysis or nspawn)
            distro: Override distro (default: from analysis or trixie)
            copy_config: Whether to mount/copy config files
            copy_data: Whether to mount data directories
            additional_packages: Extra packages to install
            additional_ports: Extra ports to expose

        Returns:
            ContainerSpec ready for container creation
        """
        service = analysis.service
        deps = analysis.dependencies

        # Determine container name
        name = container_name or self._sanitize_name(service.name)

        # Determine runtime and distro
        used_runtime = runtime or analysis.recommended_runtime or self.default_runtime
        used_distro = distro or analysis.recommended_distro or self.default_distro

        # Build service configuration
        spec_service = self._build_service(service)

        # Build volumes list
        volumes = self._build_volumes(deps, copy_config, copy_data)

        # Build port mappings
        ports = self._build_ports(deps, additional_ports)

        # Collect packages
        packages = list(deps.packages)
        if additional_packages:
            packages.extend(additional_packages)

        # Build environment variables
        environment = dict(service.service.environment)

        # Determine if privileged mode is needed
        privileged = analysis.requires_privileged

        # Build the spec
        spec = ContainerSpec(
            name=name,
            runtime=used_runtime,
            distro=used_distro,
            profiles=list(self.default_profiles),
            network=SpecNetwork(
                mode="host" if analysis.requires_host_network else "shared",
                ports=ports,
            ),
            volumes=volumes,
            environment=environment,
            privileged=privileged,
            services=[spec_service],
        )

        return spec

    def _sanitize_name(self, name: str) -> str:
        """Sanitize service name for use as container name.

        Args:
            name: Service name

        Returns:
            Valid container name
        """
        # Remove .service suffix
        name = name.replace(".service", "")

        # Replace @ in template instances
        name = name.replace("@", "-")

        # Replace other invalid characters
        name = name.replace("_", "-")

        # Ensure it starts with alphanumeric
        if name and not name[0].isalnum():
            name = "s" + name

        return name.lower()

    def _build_service(self, parsed_service) -> SpecService:
        """Build a SpecService from parsed service data.

        Args:
            parsed_service: ParsedSystemdService model

        Returns:
            SpecService configuration
        """
        svc = parsed_service.service

        # Get the command from ExecStart
        exec_start = svc.exec_start
        if isinstance(exec_start, list):
            # Use the first command if there are multiple
            command = exec_start[0] if exec_start else ""
        else:
            command = exec_start or ""

        # Strip prefix characters (-, @, +, !, !!)
        command = command.lstrip("-@+!")

        # Map restart policy
        restart = svc.restart
        if restart == "always":
            restart = "always"
        elif restart in ("on-failure", "on-abnormal", "on-abort", "on-watchdog"):
            restart = "on-failure"
        elif restart == "on-success":
            restart = "on-failure"  # Best approximation
        else:
            restart = "on-failure"  # Default

        return SpecService(
            name=parsed_service.name,
            command=command,
            user=svc.user,
            workdir=svc.working_directory,
            description=parsed_service.unit.description or f"{parsed_service.name} service",
            restart=restart,
            environment=dict(svc.environment),
            enabled=True,
        )

    def _build_volumes(
        self,
        deps,
        copy_config: bool,
        copy_data: bool,
    ) -> list[str]:
        """Build volume mount list.

        Args:
            deps: DiscoveredDependencies
            copy_config: Whether to mount config files
            copy_data: Whether to mount data directories

        Returns:
            List of volume mount strings
        """
        volumes: list[str] = []

        if copy_config:
            # Mount config files as read-only
            for config_file in deps.config_files:
                if Path(config_file).exists():
                    volumes.append(f"{config_file}:{config_file}:ro")

            # Mount config directories as read-only
            for config_dir in deps.config_directories:
                # Ensure trailing slash consistency
                config_dir = config_dir.rstrip("/")
                if Path(config_dir).exists():
                    volumes.append(f"{config_dir}:{config_dir}:ro")

        if copy_data:
            # Mount state directories as read-write
            for state_dir in deps.state_directories:
                state_dir = state_dir.rstrip("/")
                if Path(state_dir).exists():
                    volumes.append(f"{state_dir}:{state_dir}")

            # Mount data directories as read-write
            for data_dir in deps.data_directories:
                data_dir = data_dir.rstrip("/")
                if Path(data_dir).exists():
                    volumes.append(f"{data_dir}:{data_dir}")

        return volumes

    def _build_ports(
        self,
        deps,
        additional_ports: list[int] | None,
    ) -> list[str]:
        """Build port mapping list.

        Args:
            deps: DiscoveredDependencies
            additional_ports: Extra ports to expose

        Returns:
            List of port mapping strings
        """
        all_ports: set[int] = set(deps.listening_ports)

        if additional_ports:
            all_ports.update(additional_ports)

        # Map each port to itself (host:container)
        return [f"{port}:{port}" for port in sorted(all_ports)]

    def to_yaml(self, spec: ContainerSpec) -> str:
        """Convert ContainerSpec to YAML format.

        Args:
            spec: ContainerSpec to convert

        Returns:
            YAML string representation
        """
        import yaml

        # Convert to dict, excluding defaults
        data = spec.model_dump(
            exclude_defaults=True,
            exclude_none=True,
            exclude_unset=True,
        )

        # Clean up empty collections
        if not data.get("volumes"):
            data.pop("volumes", None)
        if not data.get("environment"):
            data.pop("environment", None)
        if data.get("network", {}).get("ports") == []:
            data.get("network", {}).pop("ports", None)
        if data.get("network") == {}:
            data.pop("network", None)

        # Add comment header
        header = f"""\
# Container spec for migrated service: {spec.name}
# Generated by: gaol service migrate
#
# Review this configuration before creating the container.
# Adjust volumes, ports, and packages as needed.
#
"""

        return header + yaml.dump(data, default_flow_style=False, sort_keys=False)

    def to_toml(self, spec: ContainerSpec) -> str:
        """Convert ContainerSpec to TOML format.

        Args:
            spec: ContainerSpec to convert

        Returns:
            TOML string representation
        """
        try:
            import tomli_w
        except ImportError:
            try:
                import tomlkit as tomli_w
            except ImportError as e:
                raise ImportError("tomli_w or tomlkit required for TOML output") from e

        # Convert to dict
        data = spec.model_dump(
            exclude_defaults=True,
            exclude_none=True,
            exclude_unset=True,
        )

        # Clean up empty collections
        if not data.get("volumes"):
            data.pop("volumes", None)
        if not data.get("environment"):
            data.pop("environment", None)

        # Add comment header
        header = f"""\
# Container spec for migrated service: {spec.name}
# Generated by: gaol service migrate
#
# Review this configuration before creating the container.
# Adjust volumes, ports, and packages as needed.

"""

        return header + tomli_w.dumps(data)
