"""LSP Actor Service — wires LSP runtime into actor execution pipeline.

This service manages the lifecycle of LSP servers for actors with LSP bindings.
When an actor with LSP bindings is activated, this service:

1. Creates an LspRuntime instance
2. Activates the required LSP servers via LspRuntime.activate_bindings()
3. Generates tool specs from LspToolAdapter
4. Injects LSP tools into the actor's tool set

Based on docs/specification.md LSP Integration (Server Lifecycle).
"""

from __future__ import annotations

from typing import Any

import structlog

from cleveragents.lsp.runtime import LspRuntime
from cleveragents.lsp.tool_adapter import LspToolAdapter

logger = structlog.get_logger(__name__)


class LspActorService:
    """Manages LSP server lifecycle for actors with LSP bindings.

    This service bridges the gap between actor compilation metadata
    (which extracts lsp_bindings) and actor execution (which needs
    LSP servers running and tools available).

    Attributes:
        _runtime: The functional LSP runtime instance.
        _adapter: The LSP tool adapter for generating tool specs.
        _active_bindings: Mapping of actor name -> activated bindings.
    """

    def __init__(self, runtime: LspRuntime | None = None) -> None:
        """Initialize the LSP actor service.

        Args:
            runtime: Optional LspRuntime to use. When omitted, a default
                LspRuntime() is constructed. Pass a pre-built runtime
                (e.g. with test-double registry/lifecycle) to inject
                collaborators without reaching into private attributes.
        """
        # NB: Use ``is None`` rather than ``or`` — LspRuntime holds an
        # LspRegistry which is falsy when empty, so ``runtime or
        # LspRuntime()`` would silently discard a caller-supplied
        # runtime whose registry has not been populated yet.
        self._runtime = runtime if runtime is not None else LspRuntime()
        self._adapter = LspToolAdapter(self._runtime)
        self._active_bindings: dict[str, list[Any]] = {}

    @property
    def runtime(self) -> LspRuntime:
        """The underlying LSP runtime."""
        return self._runtime

    @property
    def adapter(self) -> LspToolAdapter:
        """The underlying LSP tool adapter."""
        return self._adapter

    def activate_actor_bindings(
        self,
        actor_name: str,
        bindings: list[Any],
        workspace_path: str,
    ) -> list[dict[str, Any]]:
        """Activate LSP servers for an actor and generate tool specs.

        When an actor with LSP bindings is activated (for plan execution
        or `agents actor run`), this method:

        1. Calls LspRuntime.activate_bindings() to start servers
        2. Generates tool specs from the activated servers
        3. Returns the tool specs for injection into the actor

        Args:
            actor_name: The namespaced actor name (e.g., "local/my-actor").
            bindings: List of LspBinding objects from CompilationMetadata.
            workspace_path: Root directory for the language servers.

        Returns:
            List of tool-spec dicts ready for injection into the actor.
            Each dict has keys: name, description, handler, input_schema.

        Raises:
            ValueError: If actor_name or workspace_path is empty.
        """
        if not actor_name:
            raise ValueError("actor_name must be a non-empty string")
        if not workspace_path:
            raise ValueError("workspace_path must be a non-empty string")

        logger.info(
            "lsp_actor_service.activating_bindings",
            actor=actor_name,
            binding_count=len(bindings),
            workspace=workspace_path,
        )

        # Activate the servers
        started_servers = self._runtime.activate_bindings(bindings, workspace_path)
        self._active_bindings[actor_name] = bindings

        # Generate tool specs from the activated servers
        tool_specs: list[dict[str, Any]] = []
        for binding in bindings:
            server_name = getattr(binding, "lsp_server_name", "")
            if not server_name:
                continue

            try:
                config = self._runtime.registry.get_or_raise(server_name)
                specs = self._adapter.generate_tool_specs(config)
                tool_specs.extend(specs)
                logger.debug(
                    "lsp_actor_service.generated_tool_specs",
                    actor=actor_name,
                    server=server_name,
                    spec_count=len(specs),
                )
            except Exception as exc:
                logger.warning(
                    "lsp_actor_service.tool_spec_generation_failed",
                    actor=actor_name,
                    server=server_name,
                    error=str(exc),
                )

        logger.info(
            "lsp_actor_service.activation_complete",
            actor=actor_name,
            started_servers=started_servers,
            total_tool_specs=len(tool_specs),
        )

        return tool_specs

    def deactivate_actor_bindings(self, actor_name: str) -> None:
        """Deactivate LSP servers for an actor.

        Releases references to LSP servers acquired by the actor.
        Servers are only terminated when all references are released.

        Args:
            actor_name: The namespaced actor name.
        """
        bindings = self._active_bindings.pop(actor_name, [])
        if not bindings:
            return

        logger.info(
            "lsp_actor_service.deactivating_bindings",
            actor=actor_name,
            binding_count=len(bindings),
        )

        self._runtime.deactivate_bindings(bindings)

    def shutdown(self) -> None:
        """Shut down all LSP servers."""
        logger.info("lsp_actor_service.shutting_down")
        self._runtime.stop_all()
        self._active_bindings.clear()


__all__ = [
    "LspActorService",
]
