"""Desktop entry installer.

This module handles installation and management of .desktop files
in XDG-compliant directories.
"""

from __future__ import annotations

import os
import re
import shutil
import subprocess
from datetime import datetime
from pathlib import Path

import yaml

from gaol.config import get_data_dir
from gaol.desktop.exceptions import (
    DesktopEntryExistsError,
    DesktopEntryNotFoundError,
    DesktopInstallError,
    IconExtractionError,
)
from gaol.desktop.generator import DesktopEntryGenerator
from gaol.desktop.models import DesktopConfig, DesktopEntry, InstalledEntry


class DesktopInstaller:
    """Install and manage desktop entries.

    Handles installation of .desktop files to XDG applications directory,
    icon extraction from containers, and tracking of installed entries.

    Example:
        >>> from gaol.desktop import DesktopInstaller, DesktopEntry
        >>> installer = DesktopInstaller()
        >>> entry = DesktopEntry(
        ...     name="My App",
        ...     container_name="my-container",
        ...     exec_command="/usr/bin/myapp",
        ... )
        >>> installed = installer.install(entry)
        >>> print(installed.desktop_file)
    """

    def __init__(self, config: DesktopConfig | None = None) -> None:
        """Initialize the installer.

        Args:
            config: Optional configuration for installation paths.
        """
        self.config = config or DesktopConfig()
        self.generator = DesktopEntryGenerator(config)
        self._store_path = get_data_dir() / "desktop-entries.yaml"

    def get_applications_dir(self) -> Path:
        """Get the applications directory.

        Returns XDG-compliant applications directory, preferring
        XDG_DATA_HOME if set.

        Returns:
            Path to applications directory.
        """
        if self.config.applications_dir:
            return Path(self.config.applications_dir)

        xdg_data = os.environ.get("XDG_DATA_HOME")
        if xdg_data:
            return Path(xdg_data) / "applications"

        return Path.home() / ".local" / "share" / "applications"

    def get_icons_dir(self) -> Path:
        """Get the icons directory.

        Returns XDG-compliant icons directory for 48x48 app icons.

        Returns:
            Path to icons directory.
        """
        if self.config.icons_dir:
            return Path(self.config.icons_dir)

        xdg_data = os.environ.get("XDG_DATA_HOME")
        if xdg_data:
            base = Path(xdg_data)
        else:
            base = Path.home() / ".local" / "share"

        return base / "icons" / "hicolor" / "48x48" / "apps"

    def _make_filename(self, entry: DesktopEntry) -> str:
        """Generate filename for a desktop entry.

        Args:
            entry: The desktop entry.

        Returns:
            Sanitized filename with .desktop extension.
        """
        # Sanitize name for filename
        safe_name = re.sub(r"[^a-zA-Z0-9_-]", "-", entry.name.lower())
        safe_name = re.sub(r"-+", "-", safe_name).strip("-")
        return f"{self.config.prefix}{entry.container_name}-{safe_name}.desktop"

    def install(
        self,
        entry: DesktopEntry,
        overwrite: bool = False,
    ) -> InstalledEntry:
        """Install a desktop entry.

        Creates the .desktop file in the applications directory and
        records the installation for later management.

        Args:
            entry: Desktop entry to install.
            overwrite: Whether to overwrite existing entry.

        Returns:
            Record of the installed entry.

        Raises:
            DesktopEntryExistsError: If entry exists and overwrite=False.
            DesktopInstallError: If installation fails.
        """
        apps_dir = self.get_applications_dir()
        apps_dir.mkdir(parents=True, exist_ok=True)

        filename = self._make_filename(entry)
        filepath = apps_dir / filename

        # Check for existing
        if filepath.exists() and not overwrite:
            raise DesktopEntryExistsError(entry.container_name, entry.name)

        try:
            content = self.generator.generate(entry)
            filepath.write_text(content)
            filepath.chmod(0o755)
        except OSError as e:
            raise DesktopInstallError(f"Failed to write {filepath}: {e}") from e

        # Update desktop database
        self._update_desktop_database()

        # Record installation
        installed = InstalledEntry(
            container_name=entry.container_name,
            entry_name=entry.name,
            desktop_file=str(filepath),
            installed_at=datetime.now(),
            exec_command=entry.exec_command,
        )
        self._save_installed(installed)

        return installed

    def uninstall(
        self,
        container_name: str,
        entry_name: str | None = None,
    ) -> int:
        """Uninstall desktop entries for a container.

        Args:
            container_name: Container to remove entries for.
            entry_name: Specific entry name to remove, or None for all.

        Returns:
            Number of entries removed.

        Raises:
            DesktopEntryNotFoundError: If no matching entries found.
        """
        entries = self.list_installed(container_name)

        if entry_name:
            entries = [e for e in entries if e.entry_name == entry_name]

        if not entries:
            raise DesktopEntryNotFoundError(container_name, entry_name)

        removed = 0
        for entry in entries:
            filepath = Path(entry.desktop_file)
            if filepath.exists():
                filepath.unlink()
                removed += 1

            # Remove icon if we installed one
            if entry.icon_file:
                icon_path = Path(entry.icon_file)
                if icon_path.exists():
                    icon_path.unlink()

            self._remove_installed(entry)

        if removed > 0:
            self._update_desktop_database()

        return removed

    def list_installed(
        self,
        container_name: str | None = None,
    ) -> list[InstalledEntry]:
        """List installed desktop entries.

        Args:
            container_name: Filter by container name, or None for all.

        Returns:
            List of installed entries.
        """
        entries = self._load_installed()

        if container_name:
            entries = [e for e in entries if e.container_name == container_name]

        return entries

    def get_entry_content(
        self,
        container_name: str,
        entry_name: str | None = None,
    ) -> str | None:
        """Get the content of an installed .desktop file.

        Args:
            container_name: Container name.
            entry_name: Entry name, or None for first match.

        Returns:
            Content of the .desktop file, or None if not found.
        """
        entries = self.list_installed(container_name)

        if entry_name:
            entries = [e for e in entries if e.entry_name == entry_name]

        if not entries:
            return None

        filepath = Path(entries[0].desktop_file)
        if filepath.exists():
            return filepath.read_text()

        return None

    def extract_icon(
        self,
        container_name: str,
        icon_path: str,
        container_id: str,
        runtime,
        output: Path | None = None,
    ) -> Path:
        """Extract icon from container filesystem.

        Copies an icon file from inside a container to the local
        icons directory for use in desktop entries.

        Args:
            container_name: Container name.
            icon_path: Path to icon inside container.
            container_id: Container ID in the runtime.
            runtime: Runtime instance with cp method.
            output: Optional output path (default: icons_dir).

        Returns:
            Path to extracted icon.

        Raises:
            IconExtractionError: If extraction fails.
        """
        icons_dir = self.get_icons_dir()
        icons_dir.mkdir(parents=True, exist_ok=True)

        # Determine output filename
        icon_name = Path(icon_path).name
        if output:
            dest = output
        else:
            # Use container-prefixed name to avoid conflicts
            base, ext = os.path.splitext(icon_name)
            dest = icons_dir / f"{self.config.prefix}{container_name}-{base}{ext}"

        try:
            # Use runtime's cp method to extract
            runtime.cp(container_id, icon_path, str(dest), from_container=True)
        except Exception as e:
            raise IconExtractionError(container_name, icon_path, str(e)) from e

        return dest

    def _update_desktop_database(self) -> None:
        """Run update-desktop-database if available.

        This updates the desktop menu system to recognize new entries.
        """
        apps_dir = self.get_applications_dir()
        try:
            subprocess.run(
                ["update-desktop-database", str(apps_dir)],
                capture_output=True,
                check=False,
            )
        except FileNotFoundError:
            pass  # update-desktop-database not installed

    def _save_installed(self, entry: InstalledEntry) -> None:
        """Save installed entry to tracking store.

        Args:
            entry: Entry to save.
        """
        entries = self._load_installed()

        # Update or add entry
        updated = False
        for i, existing in enumerate(entries):
            if (
                existing.container_name == entry.container_name
                and existing.entry_name == entry.entry_name
            ):
                entries[i] = entry
                updated = True
                break

        if not updated:
            entries.append(entry)

        self._write_store(entries)

    def _remove_installed(self, entry: InstalledEntry) -> None:
        """Remove entry from tracking store.

        Args:
            entry: Entry to remove.
        """
        entries = self._load_installed()
        entries = [
            e
            for e in entries
            if not (
                e.container_name == entry.container_name
                and e.entry_name == entry.entry_name
            )
        ]
        self._write_store(entries)

    def _load_installed(self) -> list[InstalledEntry]:
        """Load installed entries from tracking store.

        Returns:
            List of installed entries.
        """
        if not self._store_path.exists():
            return []

        try:
            with open(self._store_path) as f:
                data = yaml.safe_load(f)

            if not data or "entries" not in data:
                return []

            return [InstalledEntry.model_validate(e) for e in data["entries"]]
        except Exception:
            return []

    def _write_store(self, entries: list[InstalledEntry]) -> None:
        """Write entries to tracking store.

        Args:
            entries: List of entries to write.
        """
        self._store_path.parent.mkdir(parents=True, exist_ok=True)

        data = {"entries": [e.model_dump(mode="json") for e in entries]}
        with open(self._store_path, "w") as f:
            yaml.safe_dump(data, f, default_flow_style=False)
