"""Pydantic domain models for the LSP subsystem.

Provides the data models used by the LSP Registry, Runtime, and
Tool Adapter:

- :class:`LspTransport` -- enum of LSP transport mechanisms.
- :class:`LspCapability` -- enum of LSP capabilities exposed as tools.
- :class:`LspServerConfig` -- registration record for a language server.
- :class:`LspBinding` -- binds an LSP server to an actor graph node.

Namespaced names follow the project-wide pattern
``[[server:]namespace/]name``.

Based on ``docs/specification.md`` LSP Registry (lines 20499-20555)
and issue #835.
"""

from __future__ import annotations

from enum import StrEnum
from typing import Any

from pydantic import BaseModel, ConfigDict, Field, field_validator


class LspTransport(StrEnum):
    """Transport mechanism for LSP server communication.

    Determines how the LSP Runtime connects to the language server
    process.  Per specification (line 20551): ``stdio`` (default)
    or ``tcp``.

    Members:
        STDIO: Standard input/output (default). The server is spawned
            as a subprocess and communicates via stdin/stdout JSON-RPC.
        TCP: TCP socket connection. The server listens on a port and
            the runtime connects as a client.
    """

    STDIO = "stdio"
    TCP = "tcp"


class LspCapability(StrEnum):
    """LSP capabilities that can be exposed as callable tools.

    Each member maps to a Language Server Protocol feature that the
    :class:`LspToolAdapter` can present to actors.
    """

    DIAGNOSTICS = "diagnostics"
    HOVER = "hover"
    COMPLETIONS = "completions"
    DEFINITIONS = "definitions"
    REFERENCES = "references"
    RENAME = "rename"
    CODE_ACTIONS = "code_actions"
    FORMATTING = "formatting"
    SIGNATURE_HELP = "signature_help"
    DOCUMENT_SYMBOLS = "document_symbols"
    WORKSPACE_SYMBOLS = "workspace_symbols"


class LspServerConfig(BaseModel):
    """Registration record for a language server.

    Attributes:
        name: Namespaced server name (e.g. ``local/pyright``).
        description: Human-readable description of the server's purpose.
        languages: Programming languages served (e.g. ``["python"]``).
        command: Executable command to start the server.
        args: Additional CLI arguments for the server process.
        transport: Communication transport (stdio or tcp).
        env: Extra environment variables for the server process.
        capabilities: LSP capabilities this server exposes.
        initialization: Custom ``initializationOptions`` sent during
            the LSP ``initialize`` handshake.
        workspace_settings: Workspace-specific settings sent via
            ``workspace/didChangeConfiguration`` after initialization.
    """

    name: str = Field(
        ...,
        min_length=1,
        description="Namespaced server name (e.g. local/pyright)",
    )
    description: str = Field(
        default="",
        max_length=1000,
        description="Human-readable description of the language server",
    )
    languages: list[str] = Field(
        default_factory=list,
        description="Programming languages served (e.g. ['python'])",
    )
    command: str = Field(
        ...,
        min_length=1,
        description="Executable command to start the server",
    )
    args: list[str] = Field(
        default_factory=list,
        description="Additional CLI arguments for the server process",
    )
    transport: LspTransport = Field(
        default=LspTransport.STDIO,
        description="Communication transport: stdio (default) or tcp",
    )
    env: dict[str, str] = Field(
        default_factory=dict,
        description="Extra environment variables for the server process",
    )
    capabilities: list[LspCapability] = Field(
        default_factory=list,
        description="LSP capabilities this server exposes",
    )
    initialization: dict[str, Any] = Field(
        default_factory=dict,
        description=("LSP initializationOptions sent during the initialize handshake"),
    )
    workspace_settings: dict[str, Any] = Field(
        default_factory=dict,
        description=(
            "Workspace configuration sent via "
            "workspace/didChangeConfiguration after initialization"
        ),
    )

    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
    )

    @field_validator("name")
    @classmethod
    def _validate_name(cls, value: str) -> str:
        """Validate namespaced name contains a slash separator."""
        if "/" not in value:
            raise ValueError(
                f"LSP server name must be namespaced (contain '/'): got '{value}'"
            )
        return value

    @field_validator("languages")
    @classmethod
    def _validate_languages(cls, value: list[str]) -> list[str]:
        """Ensure language strings are non-empty and lowercased."""
        result: list[str] = []
        for lang in value:
            stripped = lang.strip().lower()
            if not stripped:
                raise ValueError("Language entries must be non-empty strings")
            result.append(stripped)
        return result

    @property
    def namespace(self) -> str:
        """Extract the namespace portion of the name."""
        return self.name.rsplit("/", 1)[0]

    @property
    def short_name(self) -> str:
        """Extract the short name portion after the last slash."""
        return self.name.rsplit("/", 1)[-1]

    def to_dict(self) -> dict[str, Any]:
        """Serialize to a plain dictionary."""
        return self.model_dump(mode="json")


class LspBinding(BaseModel):
    """Binds an LSP server to an actor graph node.

    When an actor activates, bindings are used to determine which
    language servers to start for each graph node.

    Attributes:
        node_name: Name of the actor graph node.
        lsp_server_name: Namespaced name of the LSP server.
        languages: Subset of languages to bind (empty = all).
        auto_detect: Whether to auto-detect language from file context.
    """

    node_name: str = Field(
        ...,
        min_length=1,
        description="Name of the actor graph node",
    )
    lsp_server_name: str = Field(
        ...,
        min_length=1,
        description="Namespaced name of the LSP server",
    )
    languages: list[str] = Field(
        default_factory=list,
        description="Subset of languages to bind (empty = all)",
    )
    auto_detect: bool = Field(
        default=True,
        description="Whether to auto-detect language from file context",
    )

    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
    )

    @field_validator("lsp_server_name")
    @classmethod
    def _validate_server_name(cls, value: str) -> str:
        """Validate that server name is namespaced."""
        if "/" not in value:
            raise ValueError(
                f"LSP server name must be namespaced (contain '/'): got '{value}'"
            )
        return value


__all__ = [
    "LspBinding",
    "LspCapability",
    "LspServerConfig",
    "LspTransport",
]
