"""Nix shell environment operations."""

from __future__ import annotations

import logging
import shlex
from typing import TYPE_CHECKING

from gaol.nix.exceptions import NixNotInstalledError, NixPackageError, NixShellError
from gaol.nix.installer import get_nix_installer
from gaol.nix.packages import translate_packages_to_nix

if TYPE_CHECKING:
    from gaol.runtimes.base import ContainerRuntime

logger = logging.getLogger(__name__)


class NixShell:
    """Manage Nix shell environments in containers.

    This class provides operations for:
    - Running commands in nix-shell with specified packages
    - Installing packages via nix-env
    - Running nix develop for flake environments

    Example:
        >>> shell = NixShell()
        >>> # Run a command with specific packages
        >>> exit_code, stdout, stderr = shell.run_shell(
        ...     runtime, container_id,
        ...     packages=["python311", "nodejs"],
        ...     command="python --version"
        ... )
        >>>
        >>> # Enter a pure shell
        >>> exit_code, stdout, stderr = shell.run_shell(
        ...     runtime, container_id,
        ...     packages=["vim", "git"],
        ...     pure=True
        ... )
    """

    NIX_PROFILE_SOURCE = """
. ~/.nix-profile/etc/profile.d/nix.sh 2>/dev/null || \
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh 2>/dev/null || true
"""

    def __init__(self) -> None:
        """Initialize NixShell."""
        self._installer = get_nix_installer()

    def _ensure_nix_installed(
        self,
        runtime: ContainerRuntime,
        container_id: str,
    ) -> None:
        """Ensure Nix is installed in the container.

        Args:
            runtime: Container runtime instance
            container_id: Container ID or name

        Raises:
            NixNotInstalledError: If Nix is not installed
        """
        if not self._installer.is_installed(runtime, container_id):
            raise NixNotInstalledError(container_id)

    def run_shell(
        self,
        runtime: ContainerRuntime,
        container_id: str,
        packages: list[str],
        *,
        pure: bool = False,
        command: str | None = None,
        translate: bool = True,
    ) -> tuple[int, str, str]:
        """Run a nix-shell with specified packages.

        Args:
            runtime: Container runtime instance
            container_id: Container ID or name
            packages: List of packages to include
            pure: Use pure mode (no system packages in PATH)
            command: Optional command to run (exits after command)
            translate: Translate canonical package names to nixpkgs

        Returns:
            Tuple of (exit_code, stdout, stderr)

        Raises:
            NixNotInstalledError: If Nix is not installed
            NixShellError: If shell operation fails
        """
        self._ensure_nix_installed(runtime, container_id)

        # Translate packages if needed
        if translate:
            nix_packages = translate_packages_to_nix(packages)
        else:
            nix_packages = packages

        if not nix_packages:
            raise NixShellError("No packages specified")

        # Build the nix-shell command
        pkg_args = " ".join(f"-p {pkg}" for pkg in nix_packages)
        pure_flag = "--pure" if pure else ""

        if command:
            # Escape the command for shell
            escaped_cmd = shlex.quote(command)
            shell_cmd = f"nix-shell {pure_flag} {pkg_args} --run {escaped_cmd}"
        else:
            shell_cmd = f"nix-shell {pure_flag} {pkg_args}"

        script = f"""
{self.NIX_PROFILE_SOURCE}
{shell_cmd}
"""

        logger.debug(f"Running nix-shell: {shell_cmd}")

        exit_code, stdout, stderr = runtime.exec(
            container_id,
            ["bash", "-c", script],
        )

        return exit_code, stdout, stderr

    def install_packages(
        self,
        runtime: ContainerRuntime,
        container_id: str,
        packages: list[str],
        *,
        translate: bool = True,
    ) -> tuple[int, str, str]:
        """Install packages permanently via nix-env.

        Args:
            runtime: Container runtime instance
            container_id: Container ID or name
            packages: List of packages to install
            translate: Translate canonical package names to nixpkgs

        Returns:
            Tuple of (exit_code, stdout, stderr)

        Raises:
            NixNotInstalledError: If Nix is not installed
            NixPackageError: If package installation fails
        """
        self._ensure_nix_installed(runtime, container_id)

        # Translate packages if needed
        if translate:
            nix_packages = translate_packages_to_nix(packages)
        else:
            nix_packages = packages

        if not nix_packages:
            return 0, "No packages to install", ""

        # Build nix-env install command
        pkg_attrs = " ".join(f"nixpkgs.{pkg}" for pkg in nix_packages)
        install_cmd = f"nix-env -iA {pkg_attrs}"

        script = f"""
{self.NIX_PROFILE_SOURCE}
{install_cmd}
"""

        logger.info(f"Installing Nix packages: {', '.join(nix_packages)}")

        exit_code, stdout, stderr = runtime.exec(
            container_id,
            ["bash", "-c", script],
        )

        if exit_code != 0:
            raise NixPackageError(nix_packages, stderr or stdout or "Unknown error")

        return exit_code, stdout, stderr

    def uninstall_packages(
        self,
        runtime: ContainerRuntime,
        container_id: str,
        packages: list[str],
    ) -> tuple[int, str, str]:
        """Uninstall packages via nix-env.

        Args:
            runtime: Container runtime instance
            container_id: Container ID or name
            packages: List of packages to uninstall

        Returns:
            Tuple of (exit_code, stdout, stderr)

        Raises:
            NixNotInstalledError: If Nix is not installed
        """
        self._ensure_nix_installed(runtime, container_id)

        if not packages:
            return 0, "No packages to uninstall", ""

        pkg_list = " ".join(packages)
        uninstall_cmd = f"nix-env -e {pkg_list}"

        script = f"""
{self.NIX_PROFILE_SOURCE}
{uninstall_cmd}
"""

        logger.info(f"Uninstalling Nix packages: {', '.join(packages)}")

        exit_code, stdout, stderr = runtime.exec(
            container_id,
            ["bash", "-c", script],
        )

        return exit_code, stdout, stderr

    def list_installed(
        self,
        runtime: ContainerRuntime,
        container_id: str,
    ) -> list[str]:
        """List installed packages.

        Args:
            runtime: Container runtime instance
            container_id: Container ID or name

        Returns:
            List of installed package names

        Raises:
            NixNotInstalledError: If Nix is not installed
        """
        self._ensure_nix_installed(runtime, container_id)

        script = f"""
{self.NIX_PROFILE_SOURCE}
nix-env -q
"""

        exit_code, stdout, stderr = runtime.exec(
            container_id,
            ["bash", "-c", script],
        )

        if exit_code != 0:
            return []

        return [line.strip() for line in stdout.strip().split("\n") if line.strip()]

    def run_develop(
        self,
        runtime: ContainerRuntime,
        container_id: str,
        flake_path: str = ".",
        *,
        command: str | None = None,
    ) -> tuple[int, str, str]:
        """Run nix develop for a flake-based environment.

        Args:
            runtime: Container runtime instance
            container_id: Container ID or name
            flake_path: Path to flake directory (default: current dir)
            command: Optional command to run

        Returns:
            Tuple of (exit_code, stdout, stderr)

        Raises:
            NixNotInstalledError: If Nix is not installed
            NixShellError: If develop operation fails
        """
        self._ensure_nix_installed(runtime, container_id)

        if command:
            develop_cmd = f"nix develop {flake_path} --command {shlex.quote(command)}"
        else:
            develop_cmd = f"nix develop {flake_path}"

        script = f"""
{self.NIX_PROFILE_SOURCE}
cd {flake_path} 2>/dev/null || true
{develop_cmd}
"""

        logger.debug(f"Running nix develop: {develop_cmd}")

        exit_code, stdout, stderr = runtime.exec(
            container_id,
            ["bash", "-c", script],
        )

        return exit_code, stdout, stderr

    def search_packages(
        self,
        runtime: ContainerRuntime,
        container_id: str,
        query: str,
        *,
        limit: int = 20,
    ) -> list[dict[str, str]]:
        """Search for packages in nixpkgs.

        Args:
            runtime: Container runtime instance
            container_id: Container ID or name
            query: Search query
            limit: Maximum results to return

        Returns:
            List of package info dicts with 'name' and 'description'

        Raises:
            NixNotInstalledError: If Nix is not installed
        """
        self._ensure_nix_installed(runtime, container_id)

        # Use nix search with JSON output
        script = f"""
{self.NIX_PROFILE_SOURCE}
nix search nixpkgs {shlex.quote(query)} --json 2>/dev/null | head -c 100000
"""

        exit_code, stdout, stderr = runtime.exec(
            container_id,
            ["bash", "-c", script],
        )

        if exit_code != 0 or not stdout.strip():
            return []

        try:
            import json

            results = json.loads(stdout)
            packages = []
            count = 0
            for attr, info in results.items():
                if count >= limit:
                    break
                packages.append(
                    {
                        "name": attr.split(".")[-1],
                        "attribute": attr,
                        "description": info.get("description", ""),
                        "version": info.get("version", ""),
                    }
                )
                count += 1
            return packages
        except (json.JSONDecodeError, KeyError):
            return []


# Singleton instance
_shell: NixShell | None = None


def get_nix_shell() -> NixShell:
    """Get the singleton NixShell instance.

    Returns:
        NixShell instance
    """
    global _shell
    if _shell is None:
        _shell = NixShell()
    return _shell
