"""LSP server management commands for CleverAgents CLI.

The ``agents lsp`` command group manages Language Server Protocol
(LSP) server registrations in the global LSP Registry.

## Commands

| Command                | Description                               |
|------------------------|-------------------------------------------|
| ``agents lsp add``     | Register LSP server from YAML config file |
| ``agents lsp remove``  | Remove a registered LSP server            |
| ``agents lsp list``    | List registered LSP servers               |
| ``agents lsp show``    | Show full details for an LSP server       |

## Config-Only Add

LSP servers are registered **exclusively** via a YAML config file:

```bash
agents lsp add --config ./lsp/pyright.yaml
```

### YAML Configuration File

```yaml
name: local/pyright
command: pyright-langserver
args: ["--stdio"]
languages: ["python"]
capabilities: ["diagnostics", "completions", "hover"]
```

## Error Handling

| Error                      | Cause                                    |
|----------------------------|------------------------------------------|
| ``Config file error``      | File not found or not readable           |
| ``Schema validation error``| YAML does not match LspServerConfig      |
| ``Already registered``     | Server exists (without ``--update``)     |
| ``Not registered``         | Server not found for show/remove         |

Based on specification.md "agents lsp" section and implementation_plan.md
task M2.7.lsp-stubs.
"""

from __future__ import annotations

import json
import logging
import os
import re as _re
from pathlib import Path
from typing import Annotated, Any

import structlog
import typer
import yaml
from pydantic import ValidationError as PydanticValidationError
from rich.console import Console
from rich.panel import Panel
from rich.table import Table

from cleveragents.cli.formatting import OutputFormat, format_output
from cleveragents.lsp.errors import LspServerNotFoundError
from cleveragents.lsp.models import LspServerConfig
from cleveragents.lsp.registry import LspRegistry

app = typer.Typer(help="Manage LSP (Language Server Protocol) servers in the registry.")
console = Console()

_FORMAT_HELP = "Output format: json, yaml, plain, table, or rich (default: rich)"

# ---------------------------------------------------------------------------
# Module-level registry singleton (in-memory; replaced by DI container
# when production registry lands).
# ---------------------------------------------------------------------------
_registry: LspRegistry | None = None


def _get_registry() -> LspRegistry:
    """Get or create the module-level LspRegistry instance."""
    global _registry
    if _registry is None:
        _registry = LspRegistry()
    return _registry


def _reset_registry() -> None:
    """Reset the registry (used by tests)."""
    global _registry
    _registry = None


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _config_dict(config: LspServerConfig) -> dict[str, Any]:
    """Serialize an LspServerConfig for non-rich output formats."""
    data = config.to_dict()
    data["namespace"] = config.namespace
    data["short_name"] = config.short_name
    return data


# ---------------------------------------------------------------------------
# CLI commands
# ---------------------------------------------------------------------------


@app.command("add")
def add(
    config: Annotated[
        Path,
        typer.Option(
            "--config",
            "-c",
            help="Path to the LSP server YAML configuration file",
        ),
    ],
    update: Annotated[
        bool,
        typer.Option(
            "--update",
            "-u",
            help="Allow overwriting an existing LSP server registration",
        ),
    ] = False,
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Register a new LSP server from a YAML configuration file.

    Examples:
        agents lsp add --config ./lsp/pyright.yaml
        agents lsp add --config ./lsp/pyright.yaml --update
    """
    try:
        # Typer's ``exists`` parameter defaults to ``False`` (no
        # automatic file-existence validation).  The manual check
        # below is deliberate: it provides a user-friendly Rich
        # error message instead of Typer's default traceback-style
        # ``FileNotFoundError``.
        if not config.exists():
            console.print(f"[red]Config file error:[/red] {config} not found")
            raise typer.Abort()

        raw = yaml.safe_load(config.read_text(encoding="utf-8"))
        if not isinstance(raw, dict):
            console.print("[red]Schema validation error:[/red] YAML must be a mapping")
            raise typer.Abort()

        server_config = LspServerConfig(**raw)
        registry = _get_registry()

        if update and registry.get(server_config.name) is not None:
            registry.remove(server_config.name)

        registry.register(server_config)

        if fmt != OutputFormat.RICH.value:
            data = _config_dict(server_config)
            data["registered"] = True
            typer.echo(format_output(data, fmt))
            return

        langs = ", ".join(server_config.languages) or "(none)"
        caps = ", ".join(c.value for c in server_config.capabilities) or "(none)"
        details = (
            f"[bold]Name:[/bold] {server_config.name}\n"
            f"[bold]Command:[/bold] {server_config.command}\n"
            f"[bold]Languages:[/bold] {langs}\n"
            f"[bold]Capabilities:[/bold] {caps}"
        )
        console.print(Panel(details, title="LSP Server Registered", expand=False))
        console.print("[green]OK[/green] LSP server registered")

    except PydanticValidationError as exc:
        console.print(f"[red]Schema validation error:[/red] {exc}")
        raise typer.Abort() from exc
    except ValueError as exc:
        error_msg = str(exc)
        console.print(f"[red]Error:[/red] {error_msg}")
        if "already registered" in error_msg:
            console.print(
                "\nTo overwrite, re-run with [cyan]--update[/cyan]:\n\n"
                f"  agents lsp add --config {config} --update"
            )
        raise typer.Abort() from exc


@app.command("remove")
def remove(
    name: Annotated[
        str,
        typer.Argument(help="Namespaced LSP server name to remove"),
    ],
    yes: Annotated[
        bool,
        typer.Option("--yes", "-y", help="Skip confirmation prompt"),
    ] = False,
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Remove a registered LSP server.

    Examples:
        agents lsp remove local/pyright
        agents lsp remove local/pyright --yes
    """
    registry = _get_registry()
    config = registry.get(name)
    if config is None:
        console.print(f"[red]LSP server not found:[/red] {name}")
        raise typer.Abort()

    if not yes:
        confirm = typer.confirm(f"Remove LSP server {name}?", default=False)
        if not confirm:
            console.print("[yellow]Aborted.[/yellow]")
            raise typer.Abort()

    registry.remove(name)

    if fmt != OutputFormat.RICH.value:
        typer.echo(format_output({"name": name, "removed": True}, fmt))
        return

    console.print(
        Panel(f"[bold]Name:[/bold] {name}", title="LSP Server Removed", expand=False)
    )
    console.print("[green]OK[/green] LSP server removed")


@app.command("list")
def list_servers(
    regex: Annotated[
        str | None,
        typer.Argument(
            help="Optional regex to filter server names",
        ),
    ] = None,
    namespace: Annotated[
        str | None,
        typer.Option("--namespace", "-n", help="Filter by namespace"),
    ] = None,
    language: Annotated[
        str | None,
        typer.Option("--language", "-l", help="Filter by language"),
    ] = None,
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """List registered LSP servers.

    An optional positional REGEX argument filters server names.

    Examples:
        agents lsp list
        agents lsp list "pyright.*"
        agents lsp list --namespace local
        agents lsp list --language python
        agents lsp list --format json
    """
    registry = _get_registry()
    servers = registry.list_servers(namespace=namespace, language=language)

    # Apply optional regex filter on server names.
    if regex is not None:
        try:
            pattern = _re.compile(regex)
        except _re.error as re_exc:
            console.print(f"[red]Invalid regex:[/red] {re_exc}")
            raise typer.Abort() from re_exc
        servers = [s for s in servers if pattern.search(s.name)]

    if not servers:
        console.print("[yellow]No LSP servers found.[/yellow]")
        console.print("Register one with 'agents lsp add --config <file>'")
        return

    if fmt != OutputFormat.RICH.value:
        data = [_config_dict(s) for s in servers]
        typer.echo(format_output(data, fmt))
        return

    table = Table(title=f"LSP Servers ({len(servers)} total)", show_header=True)
    table.add_column("Name", style="cyan")
    table.add_column("Command")
    table.add_column("Languages")
    table.add_column("Capabilities", justify="right")

    for server in servers:
        table.add_row(
            server.name,
            server.command,
            ", ".join(server.languages) or "(none)",
            str(len(server.capabilities)),
        )

    console.print(table)
    console.print(f"[green]OK[/green] {len(servers)} LSP server(s) listed")


@app.command("show")
def show(
    name: Annotated[
        str,
        typer.Argument(help="Namespaced LSP server name to show"),
    ],
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Show full details for a registered LSP server.

    Examples:
        agents lsp show local/pyright
        agents lsp show local/pyright --format json
    """
    registry = _get_registry()
    try:
        config = registry.get_or_raise(name)
    except LspServerNotFoundError as exc:
        console.print(f"[red]LSP server not found:[/red] {name}")
        raise typer.Abort() from exc

    if fmt != OutputFormat.RICH.value:
        typer.echo(format_output(_config_dict(config), fmt))
        return

    details = (
        f"[bold]Name:[/bold] {config.name}\n"
        f"[bold]Namespace:[/bold] {config.namespace}\n"
        f"[bold]Description:[/bold] {config.description or '(none)'}\n"
        f"[bold]Command:[/bold] {config.command}\n"
        f"[bold]Args:[/bold] {' '.join(config.args) or '(none)'}\n"
        f"[bold]Transport:[/bold] {config.transport.value}\n"
        f"[bold]Languages:[/bold] {', '.join(config.languages) or '(none)'}\n"
        f"[bold]Capabilities:[/bold] "
        f"{', '.join(c.value for c in config.capabilities) or '(none)'}"
    )
    console.print(Panel(details, title="LSP Server Details", expand=False))

    if config.initialization:
        init_str = json.dumps(config.initialization, indent=2)
        console.print(Panel(init_str, title="Initialization Options", expand=False))

    if config.workspace_settings:
        ws_str = json.dumps(config.workspace_settings, indent=2)
        console.print(Panel(ws_str, title="Workspace Settings", expand=False))

    if config.env:
        env_lines: list[str] = []
        for key, val in sorted(config.env.items()):
            env_lines.append(f"[blue]{key}[/blue] = {val}")
        console.print(Panel("\n".join(env_lines), title="Environment", expand=False))

    console.print("[green]OK[/green] LSP server loaded")


_VALID_LOG_LEVELS = ("debug", "info", "warning", "error")

# Dedicated stderr console for the serve command so that the
# startup banner does not corrupt the JSON-RPC transport on stdout.
_serve_console = Console(stderr=True)


@app.command("serve")
def serve(
    log_level: Annotated[
        str,
        typer.Option(
            "--log-level",
            help="Logging level: debug, info, warning, error (default: info)",
        ),
    ] = "info",
) -> None:
    """Launch the stub LSP server over stdin/stdout.

    The server speaks JSON-RPC with Content-Length header framing
    (the standard LSP base protocol transport).  It supports the
    ``initialize``, ``shutdown``, and ``exit`` lifecycle methods.
    All other LSP methods return a MethodNotFound error.

    Examples:
        agents lsp serve
        agents lsp serve --log-level debug
    """
    normalised = log_level.lower()
    if normalised not in _VALID_LOG_LEVELS:
        _serve_console.print(
            f"[red]ERROR[/red] Invalid log level '{log_level}'. "
            f"Valid levels: {', '.join(_VALID_LOG_LEVELS)}"
        )
        raise typer.Exit(code=1)

    level = getattr(logging, normalised.upper(), logging.INFO)
    previous_config: dict[str, Any] = dict(structlog.get_config())
    exit_code = 1
    try:
        structlog.configure(
            wrapper_class=structlog.make_filtering_bound_logger(level),
        )
        log = structlog.get_logger("cleveragents.lsp.cli")

        pid = os.getpid()
        log.info("lsp.serve.startup", pid=pid, log_level=normalised)
        _serve_console.print(
            "[bold cyan]CleverAgents LSP Server[/bold cyan]"
            f" (stub) starting — PID {pid}"
        )
        _serve_console.print(
            f"  Log level: {normalised}\n"
            "  Transport: stdin/stdout (JSON-RPC, Content-Length framing)\n"
            "  Supported methods: initialize, shutdown, exit\n"
            "  All other methods → MethodNotFound (-32601)"
        )

        # Lazy import: LspServer transitively imports A2aLocalFacade
        # which pulls the full A2A dependency chain (~200ms cold).
        # Non-serve subcommands (add, remove, list, show) do not
        # need LspServer, so deferring the import avoids that cost
        # for the common case.
        from cleveragents.lsp.server import LspServer

        server = LspServer()
        exit_code = server.run()
        log.info("lsp.serve.stopped", pid=pid, exit_code=exit_code)
    finally:
        # Restore the previous structlog configuration.  Note:
        # ``structlog.configure()`` unconditionally sets
        # ``_CONFIG.is_configured = True`` internally, and
        # ``get_config()`` does not capture this flag.  If structlog
        # was using defaults (not yet configured) before ``serve()``
        # was called, subsequent ``configure_once()`` invocations
        # will silently refuse to apply their configuration because
        # they see ``is_configured == True``.  There are currently
        # zero ``configure_once()`` calls in this codebase so the
        # impact is latent.  Accept this limitation rather than
        # calling ``reset_defaults()`` which would discard any
        # processor chain the caller set up.
        structlog.configure(**previous_config)

    raise typer.Exit(code=exit_code)
