"""Nix installer for containers."""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from gaol.nix.exceptions import (
    NixChannelError,
    NixInstallationError,
)
from gaol.nix.models import NixConfig, NixStatus

if TYPE_CHECKING:
    from gaol.runtimes.base import ContainerRuntime

logger = logging.getLogger(__name__)


class NixInstaller:
    """Install and manage Nix in containers.

    This class handles:
    - Installing Nix (single-user or daemon mode)
    - Configuring channels
    - Setting up experimental features (flakes)
    - Adding binary caches

    Example:
        >>> installer = NixInstaller()
        >>> installer.install(runtime, container_id)
        >>> installer.configure_channel(runtime, container_id, "nixos-24.11")
    """

    # Single-user installation (recommended for containers)
    SINGLE_USER_INSTALL_SCRIPT = """
set -e

# Check if Nix is already installed
if command -v nix &> /dev/null; then
    echo "Nix is already installed"
    exit 0
fi

# Install prerequisites
if command -v apt-get &> /dev/null; then
    apt-get update -qq
    apt-get install -y -qq curl xz-utils sudo
elif command -v dnf &> /dev/null; then
    dnf install -y -q curl xz sudo
elif command -v apk &> /dev/null; then
    apk add --no-cache curl xz sudo bash
fi

# Create nix user if running as root
if [ "$(id -u)" = "0" ]; then
    # Create a non-root user for nix installation
    if ! id -u nixuser &>/dev/null; then
        useradd -m nixuser || true
    fi
fi

# Download and run the official Nix installer
curl -L https://nixos.org/nix/install -o /tmp/nix-install.sh
chmod +x /tmp/nix-install.sh

# Run single-user install
sh /tmp/nix-install.sh --no-daemon

# Source nix profile
if [ -f ~/.nix-profile/etc/profile.d/nix.sh ]; then
    . ~/.nix-profile/etc/profile.d/nix.sh
fi

# Clean up
rm -f /tmp/nix-install.sh

echo "Nix installation complete"
"""

    # Multi-user (daemon) installation
    DAEMON_INSTALL_SCRIPT = """
set -e

# Check if Nix is already installed
if command -v nix &> /dev/null; then
    echo "Nix is already installed"
    exit 0
fi

# Install prerequisites
if command -v apt-get &> /dev/null; then
    apt-get update -qq
    apt-get install -y -qq curl xz-utils sudo systemd
elif command -v dnf &> /dev/null; then
    dnf install -y -q curl xz sudo systemd
fi

# Download and run the official Nix installer with daemon mode
curl -L https://nixos.org/nix/install -o /tmp/nix-install.sh
chmod +x /tmp/nix-install.sh

# Run daemon install
sh /tmp/nix-install.sh --daemon --yes

# Source nix profile
if [ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]; then
    . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
fi

# Clean up
rm -f /tmp/nix-install.sh

echo "Nix daemon installation complete"
"""

    # Script to enable flakes
    ENABLE_FLAKES_SCRIPT = """
mkdir -p ~/.config/nix
cat > ~/.config/nix/nix.conf << 'EOF'
experimental-features = nix-command flakes
EOF
echo "Flakes enabled"
"""

    def install(
        self,
        runtime: ContainerRuntime,
        container_id: str,
        *,
        daemon: bool = False,
        channel: str = "nixpkgs-unstable",
        enable_flakes: bool = True,
        config: NixConfig | None = None,
    ) -> bool:
        """Install Nix in the container.

        Args:
            runtime: Container runtime instance
            container_id: Container ID or name
            daemon: Use daemon mode (multi-user) instead of single-user
            channel: Nix channel to configure
            enable_flakes: Enable flakes experimental feature
            config: Optional NixConfig for advanced configuration

        Returns:
            True if installation successful

        Raises:
            NixInstallationError: If installation fails
        """
        logger.info(f"Installing Nix in container {container_id}")

        # Choose installation script
        script = self.DAEMON_INSTALL_SCRIPT if daemon else self.SINGLE_USER_INSTALL_SCRIPT

        # Run installation
        exit_code, stdout, stderr = runtime.exec(
            container_id,
            ["bash", "-c", script],
            user="root",
        )

        if exit_code != 0:
            error_msg = stderr or stdout or "Unknown error"
            raise NixInstallationError(container_id, error_msg)

        logger.debug(f"Nix installation output: {stdout}")

        # Enable flakes if requested
        if enable_flakes:
            self._enable_flakes(runtime, container_id, config)

        # Configure channel
        try:
            self.configure_channel(runtime, container_id, channel)
        except NixChannelError:
            logger.warning(f"Failed to configure channel {channel}, continuing anyway")

        # Apply additional config if provided
        if config:
            self._apply_config(runtime, container_id, config)

        logger.info(f"Nix installed successfully in container {container_id}")
        return True

    def _enable_flakes(
        self,
        runtime: ContainerRuntime,
        container_id: str,
        config: NixConfig | None = None,
    ) -> None:
        """Enable flakes experimental feature.

        Args:
            runtime: Container runtime instance
            container_id: Container ID or name
            config: Optional NixConfig with additional experimental features
        """
        if config and config.experimental_features:
            features = " ".join(config.experimental_features)
            script = f"""
mkdir -p ~/.config/nix
cat > ~/.config/nix/nix.conf << 'EOF'
experimental-features = {features}
EOF
"""
        else:
            script = self.ENABLE_FLAKES_SCRIPT

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

        if exit_code != 0:
            logger.warning(f"Failed to enable flakes: {stderr or stdout}")

    def _apply_config(
        self,
        runtime: ContainerRuntime,
        container_id: str,
        config: NixConfig,
    ) -> None:
        """Apply NixConfig settings.

        Args:
            runtime: Container runtime instance
            container_id: Container ID or name
            config: NixConfig to apply
        """
        nix_conf = config.get_nix_conf()

        script = f"""
mkdir -p ~/.config/nix
cat > ~/.config/nix/nix.conf << 'NIXCONF'
{nix_conf}
NIXCONF
"""

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

        if exit_code != 0:
            logger.warning(f"Failed to apply Nix config: {stderr or stdout}")

    def is_installed(
        self,
        runtime: ContainerRuntime,
        container_id: str,
    ) -> bool:
        """Check if Nix is installed in the container.

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

        Returns:
            True if Nix is installed
        """
        # Try to find nix binary
        check_script = """
# Source nix profile if available
[ -f ~/.nix-profile/etc/profile.d/nix.sh ] && . ~/.nix-profile/etc/profile.d/nix.sh
[ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ] && . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh

# Check for nix command
command -v nix &> /dev/null && echo "installed" || echo "not_installed"
"""

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

        return "installed" in stdout

    def get_status(
        self,
        runtime: ContainerRuntime,
        container_id: str,
    ) -> NixStatus:
        """Get Nix installation status.

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

        Returns:
            NixStatus object with installation details
        """
        status = NixStatus()

        # Check if installed
        if not self.is_installed(runtime, container_id):
            return status

        status.installed = True

        # Get version
        version_script = """
. ~/.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
nix --version 2>/dev/null || echo "unknown"
"""
        exit_code, stdout, stderr = runtime.exec(
            container_id,
            ["bash", "-c", version_script],
        )
        if exit_code == 0 and "nix" in stdout.lower():
            # Parse version from "nix (Nix) 2.x.x"
            parts = stdout.strip().split()
            if len(parts) >= 3:
                status.version = parts[-1]

        # Check daemon mode
        daemon_check = "[ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ] && echo daemon || echo single"
        exit_code, stdout, stderr = runtime.exec(
            container_id,
            ["bash", "-c", daemon_check],
        )
        status.daemon_mode = "daemon" in stdout

        # Get channel
        channel_script = """
. ~/.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
nix-channel --list 2>/dev/null | head -1 | awk '{print $1}'
"""
        exit_code, stdout, stderr = runtime.exec(
            container_id,
            ["bash", "-c", channel_script],
        )
        if exit_code == 0 and stdout.strip():
            status.channel = stdout.strip()

        # Get store size
        store_size_script = "du -sb /nix/store 2>/dev/null | cut -f1 || echo 0"
        exit_code, stdout, stderr = runtime.exec(
            container_id,
            ["bash", "-c", store_size_script],
        )
        if exit_code == 0:
            try:
                status.store_size = int(stdout.strip())
            except ValueError:
                pass

        # Count packages
        pkg_count_script = """
. ~/.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
nix-env -q 2>/dev/null | wc -l || echo 0
"""
        exit_code, stdout, stderr = runtime.exec(
            container_id,
            ["bash", "-c", pkg_count_script],
        )
        if exit_code == 0:
            try:
                status.num_packages = int(stdout.strip())
            except ValueError:
                pass

        # Check flake support
        flake_check = """
. ~/.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
nix flake --help &>/dev/null && echo "yes" || echo "no"
"""
        exit_code, stdout, stderr = runtime.exec(
            container_id,
            ["bash", "-c", flake_check],
        )
        status.flake_support = "yes" in stdout

        return status

    def configure_channel(
        self,
        runtime: ContainerRuntime,
        container_id: str,
        channel: str,
    ) -> None:
        """Configure Nix channel.

        Args:
            runtime: Container runtime instance
            container_id: Container ID or name
            channel: Channel name (e.g., nixpkgs-unstable, nixos-24.11)

        Raises:
            NixChannelError: If channel configuration fails
        """
        # Map channel name to URL
        if channel.startswith("http"):
            channel_url = channel
        elif channel.startswith("nixos-"):
            channel_url = f"https://nixos.org/channels/{channel}"
        elif channel == "nixpkgs-unstable":
            channel_url = "https://nixos.org/channels/nixpkgs-unstable"
        else:
            channel_url = f"https://nixos.org/channels/{channel}"

        script = f"""
. ~/.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
nix-channel --add {channel_url} nixpkgs
nix-channel --update
"""

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

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

        logger.info(f"Configured channel {channel} in container {container_id}")

    def add_binary_cache(
        self,
        runtime: ContainerRuntime,
        container_id: str,
        url: str,
        public_key: str,
    ) -> None:
        """Add a binary cache for faster package downloads.

        Args:
            runtime: Container runtime instance
            container_id: Container ID or name
            url: Cache URL (e.g., https://cache.example.com)
            public_key: Public key for the cache

        Raises:
            NixInstallationError: If adding cache fails
        """
        script = f"""
mkdir -p ~/.config/nix

# Read existing config or start fresh
if [ -f ~/.config/nix/nix.conf ]; then
    existing=$(cat ~/.config/nix/nix.conf)
else
    existing=""
fi

# Add substituter if not already present
if ! echo "$existing" | grep -q "{url}"; then
    echo "substituters = https://cache.nixos.org {url}" >> ~/.config/nix/nix.conf
    echo "trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= {public_key}" >> ~/.config/nix/nix.conf
fi
"""

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

        if exit_code != 0:
            raise NixInstallationError(
                container_id, f"Failed to add binary cache: {stderr or stdout}"
            )

        logger.info(f"Added binary cache {url} to container {container_id}")

    def garbage_collect(
        self,
        runtime: ContainerRuntime,
        container_id: str,
        *,
        delete_older_than: str | None = None,
    ) -> tuple[int, str]:
        """Run Nix garbage collection.

        Args:
            runtime: Container runtime instance
            container_id: Container ID or name
            delete_older_than: Delete generations older than this (e.g., "30d")

        Returns:
            Tuple of (freed_bytes, output_message)
        """
        if delete_older_than:
            gc_cmd = f"nix-collect-garbage --delete-older-than {delete_older_than}"
        else:
            gc_cmd = "nix-collect-garbage"

        script = f"""
. ~/.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
{gc_cmd} 2>&1
"""

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

        output = stdout + stderr

        # Try to parse freed space from output
        freed = 0
        if "freed" in output.lower():
            # Try to find freed bytes
            import re

            match = re.search(r"(\d+(?:\.\d+)?)\s*(bytes|KiB|MiB|GiB)", output)
            if match:
                value = float(match.group(1))
                unit = match.group(2)
                multipliers = {"bytes": 1, "KiB": 1024, "MiB": 1024**2, "GiB": 1024**3}
                freed = int(value * multipliers.get(unit, 1))

        return freed, output


# Singleton instance
_installer: NixInstaller | None = None


def get_nix_installer() -> NixInstaller:
    """Get the singleton NixInstaller instance.

    Returns:
        NixInstaller instance
    """
    global _installer
    if _installer is None:
        _installer = NixInstaller()
    return _installer
