"""Dependency discovery for service migration.

This module discovers packages, configuration files, data directories,
and network ports required by a systemd service.
"""

from __future__ import annotations

import re
import subprocess
from pathlib import Path
from typing import TYPE_CHECKING

from gaol.service_migration.models import (
    WELLKNOWN_SERVICES,
    DiscoveredDependencies,
    MigrationWarning,
    MigrationWarningSeverity,
)

if TYPE_CHECKING:
    from gaol.service_migration.models import ParsedSystemdService


class DependencyDiscovery:
    """Discovers dependencies required by a systemd service.

    Uses multiple strategies:
    1. dpkg/rpm package lookup for binaries
    2. Well-known service registry
    3. Configuration file patterns
    4. Network port discovery via ss/netstat
    """

    def __init__(self, use_wellknown: bool = True, query_system: bool = True):
        """Initialize the discovery engine.

        Args:
            use_wellknown: Whether to use well-known service registry
            query_system: Whether to query the running system (dpkg, ss)
        """
        self.use_wellknown = use_wellknown
        self.query_system = query_system
        self._package_manager: str | None = None

    def _detect_package_manager(self) -> str | None:
        """Detect the system's package manager."""
        if self._package_manager is not None:
            return self._package_manager

        # Check for dpkg (Debian/Ubuntu)
        try:
            result = subprocess.run(["which", "dpkg"], capture_output=True, text=True, check=False)
            if result.returncode == 0:
                self._package_manager = "dpkg"
                return self._package_manager
        except (OSError, subprocess.SubprocessError):
            pass

        # Check for rpm (RHEL/Fedora/SUSE)
        try:
            result = subprocess.run(["which", "rpm"], capture_output=True, text=True, check=False)
            if result.returncode == 0:
                self._package_manager = "rpm"
                return self._package_manager
        except (OSError, subprocess.SubprocessError):
            pass

        return None

    def discover_binary_package(self, binary_path: str) -> str | None:
        """Find the package that provides a binary.

        Args:
            binary_path: Absolute path to the binary

        Returns:
            Package name, or None if not found
        """
        if not self.query_system:
            return None

        pm = self._detect_package_manager()
        if not pm:
            return None

        try:
            if pm == "dpkg":
                result = subprocess.run(
                    ["dpkg", "-S", binary_path],
                    capture_output=True,
                    text=True,
                    check=False,
                )
                if result.returncode == 0 and result.stdout:
                    # Output format: "package-name: /path/to/file"
                    package = result.stdout.strip().split(":")[0]
                    # Handle diversion format
                    if package.startswith("diversion by"):
                        return None
                    return package

            elif pm == "rpm":
                result = subprocess.run(
                    ["rpm", "-qf", binary_path],
                    capture_output=True,
                    text=True,
                    check=False,
                )
                if result.returncode == 0 and result.stdout:
                    # Output is just the package name
                    return result.stdout.strip()

        except (OSError, subprocess.SubprocessError):
            pass

        return None

    def discover_listening_ports(self, service_name: str, pid: int | None = None) -> list[int]:
        """Discover ports the service is listening on.

        Args:
            service_name: Service name for process matching
            pid: Optional PID of the service

        Returns:
            List of port numbers
        """
        if not self.query_system:
            return []

        ports: set[int] = set()

        try:
            # Use ss (preferred) or netstat
            result = subprocess.run(
                ["ss", "-tlnp"],
                capture_output=True,
                text=True,
                check=False,
            )
            if result.returncode == 0:
                for line in result.stdout.split("\n"):
                    # Look for service name or PID in the output
                    if pid and f"pid={pid}" in line:
                        # Extract port from address:port
                        match = re.search(r":(\d+)\s+", line)
                        if match:
                            ports.add(int(match.group(1)))
                    elif service_name.lower() in line.lower():
                        match = re.search(r":(\d+)\s+", line)
                        if match:
                            ports.add(int(match.group(1)))

        except (OSError, subprocess.SubprocessError):
            pass

        return sorted(ports)

    def discover_config_files(self, service: ParsedSystemdService) -> tuple[list[str], list[str]]:
        """Discover configuration files and directories.

        Args:
            service: Parsed systemd service

        Returns:
            Tuple of (config_files, config_directories)
        """
        config_files: list[str] = []
        config_directories: list[str] = []

        # From EnvironmentFile directives (strip leading -)
        for env_file in service.service.environment_file:
            path = env_file.lstrip("-")
            if Path(path).is_file():
                config_files.append(path)
            elif Path(path).is_dir():
                config_directories.append(path)

        # From ConfigurationDirectory (creates /etc/<name>)
        for conf_dir in service.service.configuration_directory:
            path = f"/etc/{conf_dir}"
            if Path(path).exists():
                config_directories.append(path)

        # Common config patterns based on service name
        common_patterns = [
            f"/etc/{service.name}/",
            f"/etc/{service.name}.conf",
            f"/etc/{service.name}.d/",
            f"/etc/default/{service.name}",
        ]

        for pattern in common_patterns:
            path = Path(pattern)
            if path.exists():
                if path.is_dir():
                    if pattern not in config_directories:
                        config_directories.append(pattern)
                else:
                    if pattern not in config_files:
                        config_files.append(pattern)

        return config_files, config_directories

    def discover_state_directories(
        self, service: ParsedSystemdService
    ) -> tuple[list[str], list[str]]:
        """Discover state and data directories.

        Args:
            service: Parsed systemd service

        Returns:
            Tuple of (state_directories, data_directories)
        """
        state_dirs: list[str] = []
        data_dirs: list[str] = []

        # From StateDirectory (creates /var/lib/<name>)
        for state_dir in service.service.state_directory:
            path = f"/var/lib/{state_dir}"
            if Path(path).exists():
                state_dirs.append(path)
            else:
                # Still add it - service might create it
                state_dirs.append(path)

        # From CacheDirectory (creates /var/cache/<name>)
        for cache_dir in service.service.cache_directory:
            path = f"/var/cache/{cache_dir}"
            if Path(path).exists():
                state_dirs.append(path)

        # From LogsDirectory (creates /var/log/<name>)
        for logs_dir in service.service.logs_directory:
            path = f"/var/log/{logs_dir}"
            if Path(path).exists():
                state_dirs.append(path)

        # Common data patterns
        common_patterns = [
            f"/var/lib/{service.name}/",
            f"/var/log/{service.name}/",
            f"/var/cache/{service.name}/",
        ]

        for pattern in common_patterns:
            path = Path(pattern)
            if path.exists() and path.is_dir() and pattern not in state_dirs:
                state_dirs.append(pattern)

        return state_dirs, data_dirs

    def discover_socket_units(self, service: ParsedSystemdService) -> list[str]:
        """Find associated socket units.

        Args:
            service: Parsed systemd service

        Returns:
            List of socket unit names
        """
        socket_units: list[str] = []

        # Check for Requires/Wants of socket units
        for unit in service.unit.requires + service.unit.wants:
            if unit.endswith(".socket"):
                socket_units.append(unit)

        # Check for corresponding .socket file
        socket_name = f"{service.name}.socket"
        socket_path = service.file_path.parent / socket_name
        if socket_path.exists():
            socket_units.append(socket_name)

        return socket_units

    def discover_timer_units(self, service: ParsedSystemdService) -> list[str]:
        """Find associated timer units.

        Args:
            service: Parsed systemd service

        Returns:
            List of timer unit names
        """
        timer_units: list[str] = []

        # Check for corresponding .timer file
        timer_name = f"{service.name}.timer"
        timer_path = service.file_path.parent / timer_name
        if timer_path.exists():
            timer_units.append(timer_name)

        return timer_units

    def discover_capabilities(self, service: ParsedSystemdService) -> list[str]:
        """Discover required Linux capabilities.

        Args:
            service: Parsed systemd service

        Returns:
            List of capability names
        """
        capabilities: list[str] = []

        # From AmbientCapabilities
        capabilities.extend(service.service.ambient_capabilities)

        # From CapabilityBoundingSet (if it adds specific caps)
        for cap in service.service.capability_bounding_set:
            if not cap.startswith("~") and cap not in capabilities:
                capabilities.append(cap)

        return capabilities

    def _apply_wellknown_data(
        self, service_name: str, dependencies: DiscoveredDependencies
    ) -> None:
        """Apply well-known service data to discovered dependencies.

        Args:
            service_name: Service name
            dependencies: Dependencies to update in place
        """
        # Try exact match first, then common variations
        service_base = service_name.replace("-server", "").replace(".service", "")
        candidates = [service_name, service_base, f"{service_base}-server"]

        wellknown = None
        for candidate in candidates:
            if candidate in WELLKNOWN_SERVICES:
                wellknown = WELLKNOWN_SERVICES[candidate]
                break

        if not wellknown:
            return

        # Add packages if not already discovered
        for pkg in wellknown.packages:
            if pkg not in dependencies.packages:
                dependencies.packages.append(pkg)

        # Add ports if not already discovered
        for port in wellknown.ports:
            if port not in dependencies.listening_ports:
                dependencies.listening_ports.append(port)

        # Add config paths if not already discovered
        for path in wellknown.config_paths:
            if path.endswith("/"):
                if path not in dependencies.config_directories:
                    dependencies.config_directories.append(path)
            else:
                if path not in dependencies.config_files:
                    dependencies.config_files.append(path)

        # Add data paths if not already discovered
        for path in wellknown.data_paths:
            if path not in dependencies.data_directories:
                dependencies.data_directories.append(path)

    def discover(
        self, service: ParsedSystemdService
    ) -> tuple[DiscoveredDependencies, list[MigrationWarning]]:
        """Discover all dependencies for a service.

        Args:
            service: Parsed systemd service

        Returns:
            Tuple of (DiscoveredDependencies, list of warnings)
        """
        warnings: list[MigrationWarning] = []
        dependencies = DiscoveredDependencies()

        # Discover binary package
        binary_path = service.get_binary_path()
        if binary_path:
            package = self.discover_binary_package(binary_path)
            if package:
                dependencies.packages.append(package)
                dependencies.binary_package_map[binary_path] = package
            elif self.query_system:
                warnings.append(
                    MigrationWarning(
                        category="package_discovery",
                        message=f"Could not determine package for {binary_path}",
                        severity=MigrationWarningSeverity.WARNING,
                        suggestion="Specify packages manually with --packages",
                    )
                )

        # Discover config files and directories
        config_files, config_dirs = self.discover_config_files(service)
        dependencies.config_files.extend(config_files)
        dependencies.config_directories.extend(config_dirs)

        # Discover state and data directories
        state_dirs, data_dirs = self.discover_state_directories(service)
        dependencies.state_directories.extend(state_dirs)
        dependencies.data_directories.extend(data_dirs)

        # Discover network ports
        ports = self.discover_listening_ports(service.name)
        dependencies.listening_ports.extend(ports)

        # Discover socket and timer units
        socket_units = self.discover_socket_units(service)
        dependencies.socket_units.extend(socket_units)
        if socket_units:
            warnings.append(
                MigrationWarning(
                    category="socket_activation",
                    message=f"Service uses socket activation: {', '.join(socket_units)}",
                    severity=MigrationWarningSeverity.WARNING,
                    suggestion="Socket activation may not work in container. Consider using ports instead.",
                )
            )

        timer_units = self.discover_timer_units(service)
        dependencies.timer_units.extend(timer_units)
        if timer_units:
            warnings.append(
                MigrationWarning(
                    category="timer_units",
                    message=f"Service has associated timers: {', '.join(timer_units)}",
                    severity=MigrationWarningSeverity.INFO,
                    suggestion="Timer units will need to be recreated in the container.",
                )
            )

        # Discover capabilities
        capabilities = self.discover_capabilities(service)
        dependencies.required_capabilities.extend(capabilities)
        if capabilities:
            warnings.append(
                MigrationWarning(
                    category="capabilities",
                    message=f"Service requires capabilities: {', '.join(capabilities)}",
                    severity=MigrationWarningSeverity.INFO,
                )
            )

        # Apply well-known service data
        if self.use_wellknown:
            self._apply_wellknown_data(service.name, dependencies)

        # Check for potentially problematic services
        self._check_service_warnings(service, warnings)

        # Sort ports
        dependencies.listening_ports = sorted(set(dependencies.listening_ports))

        return dependencies, warnings

    def _check_service_warnings(
        self, service: ParsedSystemdService, warnings: list[MigrationWarning]
    ) -> None:
        """Check for potential migration issues.

        Args:
            service: Parsed systemd service
            warnings: List to append warnings to
        """
        # Check for kernel/hardware access indicators
        kernel_indicators = [
            "udev",
            "kernel",
            "dbus",
            "polkit",
            "systemd-logind",
            "accounts-daemon",
        ]

        for indicator in kernel_indicators:
            if indicator in service.name.lower():
                warnings.append(
                    MigrationWarning(
                        category="system_service",
                        message=f"Service '{service.name}' appears to be a system service",
                        severity=MigrationWarningSeverity.CRITICAL,
                        suggestion="System services typically cannot run in containers.",
                    )
                )
                break

        # Check for PrivateNetwork
        if service.service.private_network:
            warnings.append(
                MigrationWarning(
                    category="private_network",
                    message="Service uses PrivateNetwork=yes",
                    severity=MigrationWarningSeverity.INFO,
                    suggestion="Container provides network isolation by default.",
                )
            )

        # Check for RootDirectory (already chrooted)
        if service.service.root_directory:
            warnings.append(
                MigrationWarning(
                    category="chroot",
                    message=f"Service uses RootDirectory={service.service.root_directory}",
                    severity=MigrationWarningSeverity.WARNING,
                    suggestion="Chroot inside container may be unnecessary.",
                )
            )

        # Check for certain unit dependencies
        problematic_deps = ["local-fs.target", "sysinit.target", "basic.target"]
        for dep in service.unit.after + service.unit.requires:
            if dep in problematic_deps:
                warnings.append(
                    MigrationWarning(
                        category="boot_dependency",
                        message=f"Service depends on {dep}",
                        severity=MigrationWarningSeverity.INFO,
                        suggestion="Boot targets work differently in containers.",
                    )
                )
                break

        # Check for specific binaries that indicate issues
        binary = service.get_binary_path()
        if binary:
            problematic_binaries = {
                "/sbin/iptables": "firewall",
                "/usr/sbin/iptables": "firewall",
                "/sbin/ip": "network_config",
                "/sbin/modprobe": "kernel_modules",
                "/sbin/insmod": "kernel_modules",
                "/usr/bin/docker": "container_runtime",
                "/usr/bin/dockerd": "container_runtime",
            }

            if binary in problematic_binaries:
                category = problematic_binaries[binary]
                warnings.append(
                    MigrationWarning(
                        category=category,
                        message=f"Service uses {binary}",
                        severity=MigrationWarningSeverity.WARNING,
                        suggestion="This may require privileged mode or host access.",
                    )
                )
