"""LSP Tool Adapter — bridges LSP capabilities to the tool interface.

The :class:`LspToolAdapter` translates LSP capabilities into
:class:`~cleveragents.tool.runtime.ToolSpec` entries so that actors
can discover and invoke language-server features through the standard
tool interface.

When an :class:`LspRuntime` is provided, generated handlers delegate
to the runtime for real LSP protocol calls.  Without a runtime, handlers
raise :class:`LspNotAvailableError` (local-mode fallback).

Based on ``docs/specification.md`` LSP Capability Exposure (lines 20695-20741)
and issue #826.
"""

from __future__ import annotations

from collections.abc import Callable
from typing import TYPE_CHECKING, Any

import structlog

from cleveragents.lsp.errors import LspNotAvailableError
from cleveragents.lsp.models import LspCapability, LspServerConfig

if TYPE_CHECKING:
    from cleveragents.lsp.runtime import LspRuntime

logger = structlog.get_logger(__name__)

# Maps each capability to the tool name suffix and description used
# when generating ToolSpec entries.
_CAPABILITY_TOOL_MAP: dict[LspCapability, tuple[str, str]] = {
    LspCapability.DIAGNOSTICS: (
        "diagnostics",
        "Retrieve diagnostics (errors/warnings) for a file",
    ),
    LspCapability.HOVER: (
        "hover",
        "Get type information and documentation at a position",
    ),
    LspCapability.COMPLETIONS: (
        "completions",
        "Get code completion suggestions at a position",
    ),
    LspCapability.DEFINITIONS: (
        "definition",
        "Go to the definition of a symbol",
    ),
    LspCapability.REFERENCES: (
        "references",
        "Find all references to a symbol",
    ),
    LspCapability.RENAME: (
        "rename",
        "Rename a symbol across the workspace",
    ),
    LspCapability.CODE_ACTIONS: (
        "code-actions",
        "Retrieve available code actions for a range",
    ),
    LspCapability.FORMATTING: (
        "format",
        "Format a document or selection",
    ),
    LspCapability.SIGNATURE_HELP: (
        "signature",
        "Get function signature information at a call site",
    ),
    LspCapability.DOCUMENT_SYMBOLS: (
        "symbols",
        "List all symbols in a file",
    ),
    LspCapability.WORKSPACE_SYMBOLS: (
        "workspace-symbols",
        "Search for symbols across the entire workspace",
    ),
}


def _make_local_mode_handler(
    server_name: str,
    capability: LspCapability,
) -> Callable[..., Any]:
    """Create a handler that raises :class:`LspNotAvailableError`.

    Used as fallback when no runtime is provided.

    Args:
        server_name: The namespaced LSP server name.
        capability: The capability this handler represents.

    Returns:
        A callable that always raises.
    """

    def _handler(**_kwargs: Any) -> Any:
        raise LspNotAvailableError(
            f"LSP capability '{capability.value}' for server "
            f"'{server_name}' is not available in local mode",
            details={"server": server_name, "capability": capability.value},
        )

    return _handler


def _make_runtime_handler(
    runtime: LspRuntime,
    server_name: str,
    capability: LspCapability,
) -> Callable[..., Any]:
    """Create a handler that delegates to the LSP runtime.

    Args:
        runtime: The functional LSP runtime.
        server_name: The namespaced LSP server name.
        capability: The capability this handler represents.

    Returns:
        A callable that invokes the runtime method.
    """

    def _handler(**kwargs: Any) -> Any:
        # workspace_symbols is query-based, not file-based
        if capability == LspCapability.WORKSPACE_SYMBOLS:
            query: str = kwargs.get("query", "")
            if not query:
                return {"error": "query is required"}
            raise LspNotAvailableError(
                f"LSP capability '{capability.value}' is not yet implemented "
                f"for server '{server_name}'",
                details={"server": server_name, "capability": capability.value},
            )

        file_path: str = kwargs.get("file_path", "")
        if not file_path:
            return {"error": "file_path is required"}

        if capability == LspCapability.DIAGNOSTICS:
            items = runtime.get_diagnostics(server_name, file_path)
            return {"diagnostics": items}

        if capability == LspCapability.COMPLETIONS:
            line: int = kwargs.get("line", 1)
            column: int = kwargs.get("column", 1)
            items = runtime.get_completions(server_name, file_path, line, column)
            return {"completions": items}

        if capability == LspCapability.HOVER:
            line = kwargs.get("line", 1)
            column = kwargs.get("column", 1)
            result = runtime.get_hover(server_name, file_path, line, column)
            return {"hover": result}

        if capability == LspCapability.DEFINITIONS:
            line = kwargs.get("line", 1)
            column = kwargs.get("column", 1)
            items = runtime.get_definitions(server_name, file_path, line, column)
            return {"definitions": items}

        # Capabilities not yet implemented raise an explicit error
        raise LspNotAvailableError(
            f"LSP capability '{capability.value}' is not yet implemented "
            f"for server '{server_name}'",
            details={"server": server_name, "capability": capability.value},
        )

    return _handler


class LspToolAdapter:
    """Generates tool specs from LSP server capabilities.

    When *runtime* is provided, generated handlers delegate to the
    runtime.  Without a runtime, handlers raise
    :class:`LspNotAvailableError`.

    Args:
        runtime: Optional functional LSP runtime for real protocol calls.
    """

    def __init__(self, runtime: LspRuntime | None = None) -> None:
        self._runtime = runtime

    def generate_tool_specs(
        self,
        config: LspServerConfig,
    ) -> list[dict[str, Any]]:
        """Generate tool-spec dicts from *config*'s capabilities.

        Each returned dict has the shape expected by
        :class:`~cleveragents.tool.runtime.ToolSpec`:

        - ``name``:  ``<server_name>/<capability_suffix>``
        - ``description``:  Human-readable description.
        - ``handler``:  Callable (delegates to runtime or raises).
        - ``input_schema``:  Minimal JSON Schema stub.

        Args:
            config: The LSP server configuration whose capabilities
                should be turned into tool specs.

        Returns:
            List of tool-spec dicts, one per capability.

        Raises:
            ValueError: If *config* is ``None``.
        """
        if config is None:
            raise ValueError("config must not be None")

        specs: list[dict[str, Any]] = []
        for capability in config.capabilities:
            suffix, description = _CAPABILITY_TOOL_MAP.get(
                capability,
                (capability.value, f"LSP {capability.value} operation"),
            )
            tool_name = f"{config.name}/{suffix}"

            if self._runtime is not None:
                handler = _make_runtime_handler(
                    self._runtime,
                    config.name,
                    capability,
                )
            else:
                handler = _make_local_mode_handler(config.name, capability)

            spec: dict[str, Any] = {
                "name": tool_name,
                "description": description,
                "handler": handler,
                "input_schema": _input_schema_for(capability),
            }
            specs.append(spec)
            mode = "runtime" if self._runtime else "local-stub"
            logger.debug(
                "lsp.tool_adapter.generated",
                tool=tool_name,
                mode=mode,
            )

        return specs


# Schema category constants (hoisted for zero-alloc lookup).
_FILE_ONLY_CAPABILITIES = (
    LspCapability.DIAGNOSTICS,
    LspCapability.FORMATTING,
    LspCapability.DOCUMENT_SYMBOLS,
)
_POSITION_BASED_CAPABILITIES = (
    LspCapability.HOVER,
    LspCapability.COMPLETIONS,
    LspCapability.DEFINITIONS,
    LspCapability.REFERENCES,
    LspCapability.CODE_ACTIONS,
    LspCapability.SIGNATURE_HELP,
)
_RENAME_CAPABILITY = (LspCapability.RENAME,)
_QUERY_BASED_CAPABILITIES = (LspCapability.WORKSPACE_SYMBOLS,)


def _input_schema_for(capability: LspCapability) -> dict[str, Any]:
    """Return a minimal JSON Schema for the given capability.

    Args:
        capability: The LSP capability.

    Returns:
        A JSON Schema dict.

    Raises:
        ValueError: If no schema is defined for the capability.
    """
    base: dict[str, Any] = {
        "type": "object",
        "properties": {},
        "additionalProperties": False,
    }

    if capability in _FILE_ONLY_CAPABILITIES:
        base["properties"] = {
            "file_path": {
                "type": "string",
                "description": "Path to the file",
            },
        }
        base["required"] = ["file_path"]
    elif capability in _POSITION_BASED_CAPABILITIES:
        base["properties"] = {
            "file_path": {
                "type": "string",
                "description": "Path to the file",
            },
            "line": {
                "type": "integer",
                "description": "1-based line number",
            },
            "column": {
                "type": "integer",
                "description": "1-based column number",
            },
        }
        base["required"] = ["file_path", "line", "column"]
    elif capability in _RENAME_CAPABILITY:
        base["properties"] = {
            "file_path": {
                "type": "string",
                "description": "Path to the file",
            },
            "line": {
                "type": "integer",
                "description": "1-based line number",
            },
            "column": {
                "type": "integer",
                "description": "1-based column number",
            },
            "new_name": {
                "type": "string",
                "description": "New name for the symbol",
            },
        }
        base["required"] = ["file_path", "line", "column", "new_name"]
    elif capability in _QUERY_BASED_CAPABILITIES:
        base["properties"] = {
            "query": {
                "type": "string",
                "description": "Symbol search query",
            },
        }
        base["required"] = ["query"]
    else:
        raise ValueError(f"No input schema defined for capability: {capability}")
    return base


__all__ = [
    "LspToolAdapter",
]
