# type: ignore
"""
Provides Nix specific instantiation of the LanguageServer class using nixd (Nix Language Server).

Note: Windows is not supported as Nix itself doesn't support Windows natively.
"""

import logging
import os
import pathlib
import platform
import shutil
import subprocess
from pathlib import Path

from overrides import override

from solidlsp import ls_types
from solidlsp.ls import DocumentSymbols, LSPFileBuffer, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings

log = logging.getLogger(__name__)


class NixLanguageServer(SolidLanguageServer):
    """
    Provides Nix specific instantiation of the LanguageServer class using nixd.
    """

    def _extend_nix_symbol_range_to_include_semicolon(
        self, symbol: ls_types.UnifiedSymbolInformation, file_content: str
    ) -> ls_types.UnifiedSymbolInformation:
        """
        Extend symbol range to include trailing semicolon for Nix attribute symbols.

        nixd provides ranges that exclude semicolons (expression-level), but serena needs
        statement-level ranges that include semicolons for proper replacement.
        """
        range_info = symbol["range"]
        end_line = range_info["end"]["line"]
        end_char = range_info["end"]["character"]

        # Split file content into lines
        lines = file_content.split("\n")
        if end_line >= len(lines):
            return symbol

        line = lines[end_line]

        # Check if there's a semicolon immediately after the current range end
        if end_char < len(line) and line[end_char] == ";":
            # Extend range to include the semicolon
            new_range = {"start": range_info["start"], "end": {"line": end_line, "character": end_char + 1}}

            # Create modified symbol with extended range
            extended_symbol = symbol.copy()
            extended_symbol["range"] = new_range

            # CRITICAL: Also update the location.range if it exists
            if extended_symbol.get("location"):
                location = extended_symbol["location"].copy()
                if "range" in location:
                    location["range"] = new_range.copy()
                extended_symbol["location"] = location

            return extended_symbol

        return symbol

    @override
    def request_document_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer | None = None) -> DocumentSymbols:
        # Override to extend Nix symbol ranges to include trailing semicolons.
        # nixd provides expression-level ranges (excluding semicolons) but serena needs
        # statement-level ranges (including semicolons) for proper symbol replacement.

        # Get symbols from parent implementation
        document_symbols = super().request_document_symbols(relative_file_path, file_buffer=file_buffer)

        # Get file content for range extension
        file_content = self.language_server.retrieve_full_file_content(relative_file_path)

        # Extend ranges for all symbols recursively
        def extend_symbol_and_children(symbol: ls_types.UnifiedSymbolInformation) -> ls_types.UnifiedSymbolInformation:
            # Extend this symbol's range
            extended = self._extend_nix_symbol_range_to_include_semicolon(symbol, file_content)

            # Extend children recursively
            if extended.get("children"):
                extended["children"] = [extend_symbol_and_children(child) for child in extended["children"]]

            return extended

        # Apply range extension to all symbols
        extended_root_symbols = [extend_symbol_and_children(sym) for sym in document_symbols.root_symbols]

        return DocumentSymbols(extended_root_symbols)

    @override
    def is_ignored_dirname(self, dirname: str) -> bool:
        # For Nix projects, we should ignore:
        # - result: nix build output symlinks
        # - result-*: multiple build outputs
        # - .direnv: direnv cache
        return super().is_ignored_dirname(dirname) or dirname in ["result", ".direnv"] or dirname.startswith("result-")

    @staticmethod
    def _get_nixd_version():
        """Get the installed nixd version or None if not found."""
        try:
            result = subprocess.run(["nixd", "--version"], capture_output=True, text=True, check=False)
            if result.returncode == 0:
                # nixd outputs version like: nixd 2.0.0
                return result.stdout.strip()
        except FileNotFoundError:
            return None
        return None

    @staticmethod
    def _check_nixd_installed():
        """Check if nixd is installed in the system."""
        return shutil.which("nixd") is not None

    @staticmethod
    def _get_nixd_path():
        """Get the path to nixd executable."""
        # First check if it's in PATH
        nixd_path = shutil.which("nixd")
        if nixd_path:
            return nixd_path

        # Check common installation locations
        home = Path.home()
        possible_paths = [
            home / ".local" / "bin" / "nixd",
            home / ".serena" / "language_servers" / "nixd" / "nixd",
            home / ".nix-profile" / "bin" / "nixd",
            Path("/usr/local/bin/nixd"),
            Path("/run/current-system/sw/bin/nixd"),  # NixOS system profile
            Path("/opt/homebrew/bin/nixd"),  # Homebrew on Apple Silicon
            Path("/usr/local/opt/nixd/bin/nixd"),  # Homebrew on Intel Mac
        ]

        # Add Windows-specific paths
        if platform.system() == "Windows":
            possible_paths.extend(
                [
                    home / "AppData" / "Local" / "nixd" / "nixd.exe",
                    home / ".serena" / "language_servers" / "nixd" / "nixd.exe",
                ]
            )

        for path in possible_paths:
            if path.exists():
                return str(path)

        return None

    @staticmethod
    def _install_nixd_with_nix():
        """Install nixd using nix if available."""
        # Check if nix is available
        if not shutil.which("nix"):
            return None

        print("Installing nixd using nix... This may take a few minutes.")
        try:
            # Try to install nixd using nix profile
            result = subprocess.run(
                ["nix", "profile", "install", "github:nix-community/nixd"],
                capture_output=True,
                text=True,
                check=False,
                timeout=600,  # 10 minute timeout for building
            )

            if result.returncode == 0:
                # Check if nixd is now in PATH
                nixd_path = shutil.which("nixd")
                if nixd_path:
                    print(f"Successfully installed nixd at: {nixd_path}")
                    return nixd_path
            else:
                # Try nix-env as fallback
                result = subprocess.run(
                    ["nix-env", "-iA", "nixpkgs.nixd"],
                    capture_output=True,
                    text=True,
                    check=False,
                    timeout=600,
                )
                if result.returncode == 0:
                    nixd_path = shutil.which("nixd")
                    if nixd_path:
                        print(f"Successfully installed nixd at: {nixd_path}")
                        return nixd_path
                print(f"Failed to install nixd: {result.stderr}")

        except subprocess.TimeoutExpired:
            print("Nix install timed out after 10 minutes")
        except (OSError, RuntimeError, ValueError, subprocess.SubprocessError) as e:
            print(f"Error installing nixd with nix: {e}")

        return None

    @staticmethod
    def _setup_runtime_dependency():
        """
        Check if required Nix runtime dependencies are available.
        Attempts to install nixd if not present.
        """
        # First check if Nix is available (nixd needs it at runtime)
        if not shutil.which("nix"):
            print("WARNING: Nix is not installed. nixd requires Nix to function properly.")
            raise RuntimeError("Nix is required for nixd. Please install Nix from https://nixos.org/download.html")

        nixd_path = NixLanguageServer._get_nixd_path()

        if not nixd_path:
            print("nixd not found. Attempting to install...")

            # Try to install with nix if available
            nixd_path = NixLanguageServer._install_nixd_with_nix()

            if not nixd_path:
                raise RuntimeError(
                    "nixd (Nix Language Server) is not installed.\n"
                    "Please install nixd using one of the following methods:\n"
                    "  - Using Nix flakes: nix profile install github:nix-community/nixd\n"
                    "  - From nixpkgs: nix-env -iA nixpkgs.nixd\n"
                    "  - On macOS with Homebrew: brew install nixd\n\n"
                    "After installation, make sure 'nixd' is in your PATH."
                )

        # Verify nixd works
        try:
            result = subprocess.run([nixd_path, "--version"], capture_output=True, text=True, check=False, timeout=5)
            if result.returncode != 0:
                raise RuntimeError(f"nixd failed to run: {result.stderr}")
        except (OSError, RuntimeError, ValueError, subprocess.SubprocessError) as e:
            raise RuntimeError(f"Failed to verify nixd installation: {e}")

        return nixd_path

    def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
        nixd_path = self._setup_runtime_dependency()

        super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd=nixd_path, cwd=repository_root_path), "nix", solidlsp_settings)
        self.request_id = 0

    @staticmethod
    def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
        """
        Returns the initialize params for nixd.
        """
        root_uri = pathlib.Path(repository_absolute_path).as_uri()
        initialize_params = {
            "locale": "en",
            "capabilities": {
                "textDocument": {
                    "synchronization": {"didSave": True, "dynamicRegistration": True},
                    "definition": {"dynamicRegistration": True},
                    "references": {"dynamicRegistration": True},
                    "documentSymbol": {
                        "dynamicRegistration": True,
                        "hierarchicalDocumentSymbolSupport": True,
                        "symbolKind": {"valueSet": list(range(1, 27))},
                    },
                    "completion": {
                        "dynamicRegistration": True,
                        "completionItem": {
                            "snippetSupport": True,
                            "commitCharactersSupport": True,
                            "documentationFormat": ["markdown", "plaintext"],
                            "deprecatedSupport": True,
                            "preselectSupport": True,
                        },
                    },
                    "hover": {
                        "dynamicRegistration": True,
                        "contentFormat": ["markdown", "plaintext"],
                    },
                    "signatureHelp": {
                        "dynamicRegistration": True,
                        "signatureInformation": {
                            "documentationFormat": ["markdown", "plaintext"],
                            "parameterInformation": {"labelOffsetSupport": True},
                        },
                    },
                    "codeAction": {
                        "dynamicRegistration": True,
                        "codeActionLiteralSupport": {
                            "codeActionKind": {
                                "valueSet": [
                                    "",
                                    "quickfix",
                                    "refactor",
                                    "refactor.extract",
                                    "refactor.inline",
                                    "refactor.rewrite",
                                    "source",
                                    "source.organizeImports",
                                ]
                            }
                        },
                    },
                    "rename": {"dynamicRegistration": True, "prepareSupport": True},
                },
                "workspace": {
                    "workspaceFolders": True,
                    "didChangeConfiguration": {"dynamicRegistration": True},
                    "configuration": True,
                    "symbol": {
                        "dynamicRegistration": True,
                        "symbolKind": {"valueSet": list(range(1, 27))},
                    },
                },
            },
            "processId": os.getpid(),
            "rootPath": repository_absolute_path,
            "rootUri": root_uri,
            "workspaceFolders": [
                {
                    "uri": root_uri,
                    "name": os.path.basename(repository_absolute_path),
                }
            ],
            "initializationOptions": {
                # nixd specific options
                "nixpkgs": {"expr": "import <nixpkgs> { }"},
                "formatting": {"command": ["nixpkgs-fmt"]},  # or ["alejandra"] or ["nixfmt"]
                "options": {
                    "enable": True,
                    "target": {
                        "installable": "",  # Will be auto-detected from flake.nix if present
                    },
                },
            },
        }
        return initialize_params

    def _start_server(self):
        """Start nixd server process"""

        def register_capability_handler(params):
            return

        def window_log_message(msg):
            log.info(f"LSP: window/logMessage: {msg}")

        def do_nothing(params):
            return

        self.server.on_request("client/registerCapability", register_capability_handler)
        self.server.on_notification("window/logMessage", window_log_message)
        self.server.on_notification("$/progress", do_nothing)
        self.server.on_notification("textDocument/publishDiagnostics", do_nothing)

        log.info("Starting nixd server process")
        self.server.start()
        initialize_params = self._get_initialize_params(self.repository_root_path)

        log.info("Sending initialize request from LSP client to LSP server and awaiting response")
        init_response = self.server.send.initialize(initialize_params)

        # Verify server capabilities
        assert "textDocumentSync" in init_response["capabilities"]
        assert "definitionProvider" in init_response["capabilities"]
        assert "documentSymbolProvider" in init_response["capabilities"]
        assert "referencesProvider" in init_response["capabilities"]

        self.server.notify.initialized({})

        # nixd server is typically ready immediately after initialization
