"""Systemd unit file parser.

This module provides parsing functionality for systemd service files,
including handling of drop-in directories and template services.
"""

from __future__ import annotations

import re
from pathlib import Path
from typing import Any

from gaol.service_migration.exceptions import (
    ServiceNotFoundError,
    ServiceParseError,
    TemplateServiceError,
)
from gaol.service_migration.models import (
    ParsedSystemdService,
    ServiceInstallSection,
    ServiceSection,
    UnitSection,
)

# Standard systemd service file locations
SYSTEMD_PATHS = [
    Path("/etc/systemd/system"),
    Path("/run/systemd/system"),
    Path("/lib/systemd/system"),
    Path("/usr/lib/systemd/system"),
]


class SystemdUnitParser:
    """Parser for systemd unit files.

    Handles service file discovery, parsing, and merging of drop-in files.
    """

    def __init__(self, search_paths: list[Path] | None = None):
        """Initialize the parser.

        Args:
            search_paths: Custom paths to search for service files.
                         Defaults to standard systemd locations.
        """
        self.search_paths = search_paths or SYSTEMD_PATHS

    def find_service_file(self, service_name: str) -> Path:
        """Find the service file for a given service name.

        Args:
            service_name: Service name (with or without .service suffix)

        Returns:
            Path to the service file

        Raises:
            ServiceNotFoundError: If service file cannot be found
            TemplateServiceError: If a template service is specified without instance
        """
        # Normalize service name
        if not service_name.endswith(".service"):
            service_name = f"{service_name}.service"

        # Check for template service without instance
        if "@." in service_name:
            raise TemplateServiceError(service_name.replace(".service", ""))

        # Handle template instances (foo@instance.service)
        template_name = None
        if "@" in service_name:
            base, _ = service_name.split("@", 1)
            template_name = f"{base}@.service"

        searched = []
        for path in self.search_paths:
            service_path = path / service_name
            if service_path.exists():
                return service_path

            # Check for template if this is an instance
            if template_name:
                template_path = path / template_name
                if template_path.exists():
                    return template_path

            searched.append(str(path))

        raise ServiceNotFoundError(service_name, searched)

    def find_dropin_files(self, service_name: str, _service_path: Path) -> list[Path]:
        """Find drop-in configuration files for a service.

        Drop-in files are in directories named <service>.d/ and override
        or extend the main service configuration.

        Args:
            service_name: Service name (with .service suffix)
            _service_path: Path to the main service file (reserved for future use)

        Returns:
            List of drop-in file paths, sorted by name
        """
        dropin_files = []

        # Look for drop-in directories in each search path
        for search_path in self.search_paths:
            dropin_dir = search_path / f"{service_name}.d"
            if dropin_dir.exists() and dropin_dir.is_dir():
                # Find all .conf files and sort them
                conf_files = sorted(dropin_dir.glob("*.conf"))
                dropin_files.extend(conf_files)

        return dropin_files

    def _parse_unit_file(self, file_path: Path) -> dict[str, dict[str, Any]]:
        """Parse a single systemd unit file.

        Args:
            file_path: Path to the unit file

        Returns:
            Dictionary of sections with their key-value pairs

        Raises:
            ServiceParseError: If file cannot be parsed
        """
        try:
            content = file_path.read_text()
        except OSError as e:
            raise ServiceParseError(file_path.name, f"Cannot read file: {e}") from e

        # Pre-process content to handle systemd's multi-line and list features
        sections: dict[str, dict[str, Any]] = {}
        current_section = None
        line_continuation = ""

        for line in content.split("\n"):
            # Handle line continuations (backslash at end)
            if line.endswith("\\"):
                line_continuation += line[:-1] + " "
                continue

            if line_continuation:
                line = line_continuation + line
                line_continuation = ""

            # Strip comments and whitespace
            line = line.strip()
            if not line or line.startswith("#") or line.startswith(";"):
                continue

            # Section headers
            section_match = re.match(r"^\[([^\]]+)\]$", line)
            if section_match:
                current_section = section_match.group(1)
                if current_section not in sections:
                    sections[current_section] = {}
                continue

            if current_section is None:
                continue

            # Key-value pairs
            if "=" in line:
                key, _, value = line.partition("=")
                key = key.strip()
                value = value.strip()

                # Handle list values (multiple lines with same key)
                if key in sections[current_section]:
                    existing = sections[current_section][key]
                    if isinstance(existing, list):
                        existing.append(value)
                    else:
                        sections[current_section][key] = [existing, value]
                else:
                    sections[current_section][key] = value

        return sections

    def _merge_sections(
        self, base: dict[str, dict[str, Any]], override: dict[str, dict[str, Any]]
    ) -> dict[str, dict[str, Any]]:
        """Merge drop-in configuration over base configuration.

        Args:
            base: Base configuration sections
            override: Override configuration sections

        Returns:
            Merged configuration
        """
        result = {section: dict(values) for section, values in base.items()}

        for section, values in override.items():
            if section not in result:
                result[section] = {}

            for key, value in values.items():
                # Empty value clears the setting
                if value == "" or value == [""]:
                    if key in result[section]:
                        del result[section][key]
                elif isinstance(value, list):
                    # Lists are extended, not replaced
                    if key in result[section]:
                        existing = result[section][key]
                        if isinstance(existing, list):
                            result[section][key] = existing + value
                        else:
                            result[section][key] = [existing] + value
                    else:
                        result[section][key] = value
                else:
                    result[section][key] = value

        return result

    def _ensure_list(self, value: str | list | None) -> list[str]:
        """Convert a value to a list, handling space-separated values."""
        if value is None:
            return []
        if isinstance(value, list):
            result = []
            for v in value:
                result.extend(v.split())
            return result
        return value.split()

    def _parse_environment(self, value: str | list | None) -> dict[str, str]:
        """Parse Environment= directives into a dictionary."""
        if value is None:
            return {}

        result = {}
        values = value if isinstance(value, list) else [value]

        for v in values:
            # Handle multiple assignments in one line
            # Environment="VAR1=value1" "VAR2=value2"
            # Or Environment=VAR1=value1
            for part in re.findall(r'"([^"]+)"|(\S+)', v):
                item = part[0] or part[1]
                if "=" in item:
                    key, _, val = item.partition("=")
                    result[key] = val

        return result

    def _parse_bool(self, value: str | None) -> bool:
        """Parse a boolean systemd value."""
        if value is None:
            return False
        return value.lower() in ("yes", "true", "1", "on")

    def _parse_int(self, value: str | None) -> int | None:
        """Parse an integer value, stripping suffixes."""
        if value is None:
            return None
        # Remove common suffixes like 's' for seconds
        value = re.sub(r"[a-zA-Z]+$", "", value.strip())
        try:
            return int(value)
        except ValueError:
            return None

    def _sections_to_model(
        self, sections: dict[str, dict[str, Any]], name: str, file_path: Path
    ) -> ParsedSystemdService:
        """Convert parsed sections to the ParsedSystemdService model.

        Args:
            sections: Parsed configuration sections
            name: Service name
            file_path: Path to the service file

        Returns:
            ParsedSystemdService model
        """
        unit_data = sections.get("Unit", {})
        service_data = sections.get("Service", {})
        install_data = sections.get("Install", {})

        # Parse [Unit] section
        unit = UnitSection(
            description=unit_data.get("Description"),
            documentation=self._ensure_list(unit_data.get("Documentation")),
            after=self._ensure_list(unit_data.get("After")),
            before=self._ensure_list(unit_data.get("Before")),
            wants=self._ensure_list(unit_data.get("Wants")),
            requires=self._ensure_list(unit_data.get("Requires")),
            conflicts=self._ensure_list(unit_data.get("Conflicts")),
            binds_to=self._ensure_list(unit_data.get("BindsTo")),
            part_of=self._ensure_list(unit_data.get("PartOf")),
            condition_path_exists=self._ensure_list(unit_data.get("ConditionPathExists")),
        )

        # Parse [Service] section
        service = ServiceSection(
            type=service_data.get("Type", "simple"),
            exec_start=service_data.get("ExecStart"),
            exec_start_pre=self._ensure_list(service_data.get("ExecStartPre")),
            exec_start_post=self._ensure_list(service_data.get("ExecStartPost")),
            exec_stop=service_data.get("ExecStop"),
            exec_stop_post=self._ensure_list(service_data.get("ExecStopPost")),
            exec_reload=service_data.get("ExecReload"),
            user=service_data.get("User", "root"),
            group=service_data.get("Group"),
            working_directory=service_data.get("WorkingDirectory"),
            root_directory=service_data.get("RootDirectory"),
            environment=self._parse_environment(service_data.get("Environment")),
            environment_file=self._ensure_list(service_data.get("EnvironmentFile")),
            state_directory=self._ensure_list(service_data.get("StateDirectory")),
            cache_directory=self._ensure_list(service_data.get("CacheDirectory")),
            logs_directory=self._ensure_list(service_data.get("LogsDirectory")),
            configuration_directory=self._ensure_list(service_data.get("ConfigurationDirectory")),
            runtime_directory=self._ensure_list(service_data.get("RuntimeDirectory")),
            restart=service_data.get("Restart", "no"),
            restart_sec=self._parse_int(service_data.get("RestartSec")),
            timeout_start_sec=self._parse_int(service_data.get("TimeoutStartSec")),
            timeout_stop_sec=self._parse_int(service_data.get("TimeoutStopSec")),
            pid_file=service_data.get("PIDFile"),
            standard_output=service_data.get("StandardOutput"),
            standard_error=service_data.get("StandardError"),
            syslog_identifier=service_data.get("SyslogIdentifier"),
            capability_bounding_set=self._ensure_list(service_data.get("CapabilityBoundingSet")),
            ambient_capabilities=self._ensure_list(service_data.get("AmbientCapabilities")),
            no_new_privileges=self._parse_bool(service_data.get("NoNewPrivileges")),
            private_tmp=self._parse_bool(service_data.get("PrivateTmp")),
            private_network=self._parse_bool(service_data.get("PrivateNetwork")),
            protect_system=service_data.get("ProtectSystem"),
            protect_home=service_data.get("ProtectHome"),
            read_only_paths=self._ensure_list(service_data.get("ReadOnlyPaths")),
            read_write_paths=self._ensure_list(service_data.get("ReadWritePaths")),
            bind_paths=self._ensure_list(service_data.get("BindPaths")),
            bind_read_only_paths=self._ensure_list(service_data.get("BindReadOnlyPaths")),
            limit_nofile=service_data.get("LimitNOFILE"),
            limit_nproc=service_data.get("LimitNPROC"),
            limit_memlock=service_data.get("LimitMEMLOCK"),
            remain_after_exit=self._parse_bool(service_data.get("RemainAfterExit")),
            kill_mode=service_data.get("KillMode"),
            kill_signal=service_data.get("KillSignal"),
        )

        # Parse [Install] section
        install = ServiceInstallSection(
            wanted_by=self._ensure_list(install_data.get("WantedBy")),
            required_by=self._ensure_list(install_data.get("RequiredBy")),
            alias=self._ensure_list(install_data.get("Alias")),
            also=self._ensure_list(install_data.get("Also")),
        )

        # Check for template service
        is_template = "@." in file_path.name
        template_instance = None
        if "@" in name and "@." not in name:
            _, template_instance = name.split("@", 1)
            template_instance = template_instance.replace(".service", "")

        # Keep raw sections for non-standard keys
        raw_sections = {
            section: dict(values)
            for section, values in sections.items()
            if section not in ("Unit", "Service", "Install")
        }

        return ParsedSystemdService(
            name=name.replace(".service", ""),
            file_path=file_path,
            unit=unit,
            service=service,
            install=install,
            is_template=is_template,
            template_instance=template_instance,
            raw_sections=raw_sections,
        )

    def parse(self, service_name: str) -> ParsedSystemdService:
        """Parse a systemd service file.

        Args:
            service_name: Service name (with or without .service suffix)

        Returns:
            ParsedSystemdService model with all parsed data

        Raises:
            ServiceNotFoundError: If service cannot be found
            ServiceParseError: If service cannot be parsed
        """
        # Normalize service name
        if not service_name.endswith(".service"):
            normalized_name = f"{service_name}.service"
        else:
            normalized_name = service_name

        # Find the service file
        service_path = self.find_service_file(service_name)

        # Parse the main service file
        sections = self._parse_unit_file(service_path)

        # Find and merge drop-in files
        dropin_files = self.find_dropin_files(normalized_name, service_path)
        for dropin_path in dropin_files:
            dropin_sections = self._parse_unit_file(dropin_path)
            sections = self._merge_sections(sections, dropin_sections)

        # Convert to model
        parsed = self._sections_to_model(sections, normalized_name, service_path)
        parsed.dropin_files = dropin_files

        return parsed

    def parse_file(self, file_path: str | Path) -> ParsedSystemdService:
        """Parse a systemd service file from a specific path.

        Args:
            file_path: Path to the service file

        Returns:
            ParsedSystemdService model

        Raises:
            ServiceParseError: If service cannot be parsed
        """
        path = Path(file_path)
        if not path.exists():
            raise ServiceNotFoundError(path.name, [str(path.parent)])

        sections = self._parse_unit_file(path)
        return self._sections_to_model(sections, path.name, path)
