"""Nix flake generation and management."""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from gaol.nix.exceptions import (
    FlakeDevelopError,
    FlakeInitError,
    FlakeNotFoundError,
    FlakeUpdateError,
)
from gaol.nix.installer import get_nix_installer
from gaol.nix.models import NixFlake
from gaol.nix.packages import translate_packages_to_nix

if TYPE_CHECKING:
    from gaol.runtimes.base import ContainerRuntime

logger = logging.getLogger(__name__)


class FlakeManager:
    """Manage Nix flakes in containers.

    This class provides operations for:
    - Generating flake.nix files
    - Initializing flakes in containers
    - Updating flake.lock
    - Running nix develop

    Example:
        >>> manager = FlakeManager()
        >>> # Initialize a flake with Python and Node
        >>> manager.init_flake(
        ...     runtime, container_id, "/app",
        ...     packages=["python311", "nodejs"],
        ...     description="My dev environment"
        ... )
        >>>
        >>> # Enter the development shell
        >>> manager.develop(runtime, container_id, "/app")
    """

    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 FlakeManager."""
        self._installer = get_nix_installer()

    def generate_flake(
        self,
        packages: list[str],
        *,
        description: str = "Gaol development environment",
        nixpkgs_ref: str = "github:NixOS/nixpkgs/nixos-24.11",
        shell_hook: str | None = None,
        env_vars: dict[str, str] | None = None,
        inputs: dict[str, str] | None = None,
        translate: bool = True,
    ) -> str:
        """Generate flake.nix content.

        Args:
            packages: List of packages to include
            description: Flake description
            nixpkgs_ref: Reference to nixpkgs input
            shell_hook: Optional shell hook script
            env_vars: Environment variables to set
            inputs: Additional flake inputs
            translate: Translate canonical package names to nixpkgs

        Returns:
            Complete flake.nix content as string
        """
        # Translate packages if needed
        if translate:
            nix_packages = translate_packages_to_nix(packages)
        else:
            nix_packages = packages

        flake = NixFlake(
            description=description,
            nixpkgs_ref=nixpkgs_ref,
            packages=nix_packages,
            shell_hook=shell_hook,
            env_vars=env_vars or {},
            inputs=inputs or {},
        )

        return flake.generate()

    def init_flake(
        self,
        runtime: ContainerRuntime,
        container_id: str,
        path: str,
        packages: list[str],
        *,
        description: str = "Gaol development environment",
        nixpkgs_ref: str = "github:NixOS/nixpkgs/nixos-24.11",
        shell_hook: str | None = None,
        env_vars: dict[str, str] | None = None,
        translate: bool = True,
        force: bool = False,
    ) -> None:
        """Initialize a flake in the container.

        Args:
            runtime: Container runtime instance
            container_id: Container ID or name
            path: Directory path for the flake
            packages: List of packages to include
            description: Flake description
            nixpkgs_ref: Reference to nixpkgs input
            shell_hook: Optional shell hook script
            env_vars: Environment variables to set
            translate: Translate canonical package names to nixpkgs
            force: Overwrite existing flake.nix

        Raises:
            FlakeInitError: If flake initialization fails
        """
        # Generate flake content
        flake_content = self.generate_flake(
            packages,
            description=description,
            nixpkgs_ref=nixpkgs_ref,
            shell_hook=shell_hook,
            env_vars=env_vars,
            translate=translate,
        )

        # Check if flake already exists
        if not force:
            check_script = f'[ -f "{path}/flake.nix" ] && echo "exists" || echo "not_found"'
            exit_code, stdout, stderr = runtime.exec(
                container_id,
                ["bash", "-c", check_script],
            )
            if "exists" in stdout:
                raise FlakeInitError(
                    path, "flake.nix already exists. Use --force to overwrite."
                )

        # Create directory and write flake
        write_script = f"""
mkdir -p {path}
cat > {path}/flake.nix << 'FLAKE_EOF'
{flake_content}
FLAKE_EOF
"""

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

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

        # Initialize git if not already
        git_init_script = f"""
cd {path}
if [ ! -d .git ]; then
    git init -q
fi
git add flake.nix 2>/dev/null || true
"""

        runtime.exec(container_id, ["bash", "-c", git_init_script])

        # Update flake.lock
        try:
            self.update_flake(runtime, container_id, path)
        except FlakeUpdateError:
            logger.warning("Failed to create flake.lock, flake may need manual update")

        logger.info(f"Initialized flake at {path} in container {container_id}")

    def update_flake(
        self,
        runtime: ContainerRuntime,
        container_id: str,
        path: str,
        *,
        input_name: str | None = None,
    ) -> None:
        """Update flake.lock.

        Args:
            runtime: Container runtime instance
            container_id: Container ID or name
            path: Directory path containing the flake
            input_name: Specific input to update (None for all)

        Raises:
            FlakeNotFoundError: If flake.nix doesn't exist
            FlakeUpdateError: If update fails
        """
        # Check flake exists
        check_script = f'[ -f "{path}/flake.nix" ] && echo "exists" || echo "not_found"'
        exit_code, stdout, stderr = runtime.exec(
            container_id,
            ["bash", "-c", check_script],
        )
        if "not_found" in stdout:
            raise FlakeNotFoundError(path)

        # Build update command
        if input_name:
            update_cmd = f"nix flake update {input_name}"
        else:
            update_cmd = "nix flake update"

        script = f"""
{self.NIX_PROFILE_SOURCE}
cd {path}
{update_cmd}
"""

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

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

        # Add flake.lock to git
        git_add_script = f"""
cd {path}
git add flake.lock 2>/dev/null || true
"""
        runtime.exec(container_id, ["bash", "-c", git_add_script])

        logger.info(f"Updated flake.lock at {path}")

    def develop(
        self,
        runtime: ContainerRuntime,
        container_id: str,
        path: str,
        *,
        command: str | None = None,
    ) -> tuple[int, str, str]:
        """Enter flake development shell.

        Args:
            runtime: Container runtime instance
            container_id: Container ID or name
            path: Directory path containing the flake
            command: Optional command to run (exits after command)

        Returns:
            Tuple of (exit_code, stdout, stderr)

        Raises:
            FlakeNotFoundError: If flake.nix doesn't exist
            FlakeDevelopError: If nix develop fails
        """
        # Check flake exists
        check_script = f'[ -f "{path}/flake.nix" ] && echo "exists" || echo "not_found"'
        exit_code, stdout, stderr = runtime.exec(
            container_id,
            ["bash", "-c", check_script],
        )
        if "not_found" in stdout:
            raise FlakeNotFoundError(path)

        # Build develop command
        if command:
            import shlex

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

        script = f"""
{self.NIX_PROFILE_SOURCE}
cd {path}
{develop_cmd}
"""

        logger.debug(f"Running: {develop_cmd} in {path}")

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

        if exit_code != 0 and not command:
            # Only raise error if not running a command (command may have non-zero exit)
            raise FlakeDevelopError(path, stderr or stdout or "Unknown error")

        return exit_code, stdout, stderr

    def check_flake(
        self,
        runtime: ContainerRuntime,
        container_id: str,
        path: str,
    ) -> dict[str, bool | str]:
        """Check flake status and validity.

        Args:
            runtime: Container runtime instance
            container_id: Container ID or name
            path: Directory path containing the flake

        Returns:
            Dict with status information
        """
        result = {
            "exists": False,
            "has_lock": False,
            "valid": False,
            "error": None,
        }

        # Check if flake.nix exists
        check_script = f"""
[ -f "{path}/flake.nix" ] && echo "flake_exists" || echo "no_flake"
[ -f "{path}/flake.lock" ] && echo "lock_exists" || echo "no_lock"
"""
        exit_code, stdout, stderr = runtime.exec(
            container_id,
            ["bash", "-c", check_script],
        )

        result["exists"] = "flake_exists" in stdout
        result["has_lock"] = "lock_exists" in stdout

        if not result["exists"]:
            return result

        # Check if flake is valid
        check_valid_script = f"""
{self.NIX_PROFILE_SOURCE}
cd {path}
nix flake check 2>&1
"""
        exit_code, stdout, stderr = runtime.exec(
            container_id,
            ["bash", "-c", check_valid_script],
        )

        result["valid"] = exit_code == 0
        if exit_code != 0:
            result["error"] = stderr or stdout

        return result

    def show_flake(
        self,
        runtime: ContainerRuntime,
        container_id: str,
        path: str,
    ) -> dict | None:
        """Show flake metadata.

        Args:
            runtime: Container runtime instance
            container_id: Container ID or name
            path: Directory path containing the flake

        Returns:
            Flake metadata dict or None if not a valid flake
        """
        script = f"""
{self.NIX_PROFILE_SOURCE}
cd {path}
nix flake show --json 2>/dev/null
"""
        exit_code, stdout, stderr = runtime.exec(
            container_id,
            ["bash", "-c", script],
        )

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

        try:
            import json

            return json.loads(stdout)
        except json.JSONDecodeError:
            return None

    def get_inputs(
        self,
        runtime: ContainerRuntime,
        container_id: str,
        path: str,
    ) -> dict[str, str]:
        """Get flake inputs from flake.lock.

        Args:
            runtime: Container runtime instance
            container_id: Container ID or name
            path: Directory path containing the flake

        Returns:
            Dict of input names to their locked revisions
        """
        script = f"cat {path}/flake.lock 2>/dev/null"
        exit_code, stdout, stderr = runtime.exec(
            container_id,
            ["bash", "-c", script],
        )

        if exit_code != 0 or not stdout.strip():
            return {}

        try:
            import json

            lock_data = json.loads(stdout)
            nodes = lock_data.get("nodes", {})
            inputs = {}

            for name, node in nodes.items():
                if name == "root":
                    continue
                locked = node.get("locked", {})
                rev = locked.get("rev", "")
                if rev:
                    inputs[name] = rev[:12]

            return inputs
        except json.JSONDecodeError:
            return {}


# Singleton instance
_manager: FlakeManager | None = None


def get_flake_manager() -> FlakeManager:
    """Get the singleton FlakeManager instance.

    Returns:
        FlakeManager instance
    """
    global _manager
    if _manager is None:
        _manager = FlakeManager()
    return _manager
