"""MCP server configuration parsing.

Parses --mcp CLI arguments into MCPServer instances.

Supports two formats:
- HTTP URLs: http(s)://... → MCPServerSSE
- Command strings: 'command args' → MCPServerStdio

This module is in the Shell layer because it imports pydantic_ai.mcp.
"""

# @invar:allow shell_result: Factory functions return MCPServer objects, not Result
# @shell_orchestration: MCP server parsing orchestrates server configuration

from __future__ import annotations

import shlex
from contextlib import asynccontextmanager
from pathlib import Path
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from mcp.types import Implementation

    from pydantic_ai.mcp import MCPServer

    from anima.core.schemas import CapabilityToken
else:
    MCPServer = Any
    Implementation = Any
    CapabilityToken = Any


# @invar:allow shell_result: Helper returns bool classification for server command
def _is_mcp_shell_server(command: str, args: list[str]) -> bool:
    """Return True when command launches mcp-shell-server."""
    if command == "mcp-shell-server":
        return True
    return any(arg == "mcp-shell-server" for arg in args)


# @invar:allow shell_result: Argument normalizer returns transformed dict, not Result
def _normalize_shell_directory_arg(tool_args: dict[str, Any]) -> dict[str, Any]:
    """Normalize shell_execute.directory to an absolute path.

    mcp-shell-server validates that ``directory`` is absolute. Some model/tool
    calls provide relative values like "." (as seen in runtime logs), which are
    rejected before command execution.
    """
    if not isinstance(tool_args, dict):
        return tool_args

    normalized = dict(tool_args)
    directory = normalized.get("directory")
    if not isinstance(directory, str) or directory.strip() == "":
        normalized["directory"] = str(Path.cwd().resolve())
        return normalized

    if not Path(directory).is_absolute():
        normalized["directory"] = str((Path.cwd() / directory).resolve())

    return normalized


# @invar:allow shell_result: MCP callback returns ToolResult, not Result[T,E]
# @shell_orchestration: Callback orchestration for MCP tool call preprocessing
async def _process_shell_tool_call(
    ctx: Any,
    call_tool: Any,
    name: str,
    args: dict[str, Any],
) -> Any:
    """Preprocess shell tool calls so directory is always absolute."""
    _ = ctx
    normalized_args = args
    if name == "shell_execute":
        normalized_args = _normalize_shell_directory_arg(args)
    return await call_tool(name, normalized_args)


# @invar:allow shell_result: Factory returns MCPServerStdio with patched client_streams
def _make_quiet_mcp_server(**kwargs: Any) -> MCPServer:
    """Create MCPServerStdio with subprocess stderr suppressed.

    MCP tool servers (e.g. mcp-shell-server) emit INFO-level Python logging
    to stderr, which leaks into the parent terminal.  This patches
    ``client_streams`` to redirect subprocess stderr to ``os.devnull``.
    """
    from pydantic_ai.mcp import MCPServerStdio

    instance = MCPServerStdio(**kwargs)

    @asynccontextmanager
    async def quiet_client_streams():  # type: ignore[no-untyped-def]
        import os

        from mcp import StdioServerParameters
        from mcp.client.stdio import stdio_client

        server = StdioServerParameters(
            command=instance.command,
            args=list(instance.args),
            env={**os.environ, **(instance.env or {})},
            cwd=instance.cwd,
        )
        with open(os.devnull, "w") as devnull:
            async with stdio_client(server=server, errlog=devnull) as streams:
                yield streams

    instance.client_streams = quiet_client_streams  # type: ignore[assignment]
    return instance


# @invar:allow shell_result: Factory returns MCPServerStdio object, not Result
# @shell_orchestration: Centralizes MCP stdio server wiring and callback attachment
def build_mcp_server_stdio(
    *,
    command: str,
    args: list[str],
    env: dict[str, str] | None = None,
    client_info: Implementation | None = None,
) -> MCPServer:
    """Build MCPServerStdio with shell-server argument normalization hook.

    When launching ``mcp-shell-server``, attach a process_tool_call callback that
    normalizes ``shell_execute.directory`` to an absolute path.

    Subprocess stderr is suppressed to avoid INFO-level log noise from MCP
    tool servers (e.g. mcp-shell-server) leaking into the parent terminal.
    """
    process_tool_call = (
        _process_shell_tool_call if _is_mcp_shell_server(command, args) else None
    )

    kwargs: dict[str, Any] = {
        "command": command,
        "args": args,
        "env": env,
        "process_tool_call": process_tool_call,
    }
    if client_info is not None:
        kwargs["client_info"] = client_info

    return _make_quiet_mcp_server(**kwargs)


# @invar:allow shell_result: Constructs MCP Implementation object, not Result
# @shell_orchestration: Constructs MCP client_info object for tela integration
def _build_client_info(
    capability_token: CapabilityToken | None,
) -> Implementation | None:
    """Build MCP client_info with capability_token if provided.

    Spec source: docs/INTERFACES.md Section H.5 (Token Transport)
    The token is presented to tela in the MCP initialize request,
    inside clientInfo.capability_token.

    The MCP Implementation type has additionalProperties: true,
    allowing us to embed the capability_token as an extra field.

    Args:
        capability_token: Optional CapabilityToken to embed in client_info.

    Returns:
        Implementation with capability_token, or None if no token.
    """
    if capability_token is None:
        return None

    # Build client_info with capability_token embedded
    # Implementation has additionalProperties: true per MCP spec
    # Name and version are required by MCP
    from mcp.types import Implementation

    return Implementation(
        name="anima",
        version="1.0.0",
        capability_token=capability_token.model_dump(),  # type: ignore[call-arg]
    )


# @invar:allow shell_result: Factory function returns MCPServer objects, not Result
# @shell_orchestration: Parsing is pure logic but MCPServer types import pydantic_ai
# @shell_complexity: URL detection with fallback to command parsing requires multiple branches
def parse_mcp_server(
    value: str,
    capability_token: CapabilityToken | None = None,
) -> MCPServer:
    """Parse a single --mcp argument into an MCPServer instance.

    @pre: lambda value: len(value.strip()) > 0

    Args:
        value: Either an HTTP/HTTPS URL or a command string.
        capability_token: Optional CapabilityToken to pass to MCP server
            via client_info.capability_token (docs/INTERFACES.md H.5).

    Returns:
        MCPServerSSE for URLs, MCPServerStdio for commands.

    Raises:
        ValueError: If the value cannot be parsed.

    Examples:
        >>> from anima.shell.mcp_servers import parse_mcp_server
        >>> # HTTP URL
        >>> server = parse_mcp_server("https://api.example.com/mcp")
        >>> server.__class__.__name__
        'MCPServerSSE'

        >>> # Stdio command with args
        >>> server = parse_mcp_server("npx @modelcontextprotocol/server-filesystem /path")
        >>> server.__class__.__name__
        'MCPServerStdio'
    """
    from pydantic_ai.mcp import MCPServerSSE, MCPServerStdio

    value = value.strip()
    client_info = _build_client_info(capability_token)

    # HTTP/HTTPS URLs → MCPServerSSE
    if value.startswith(("http://", "https://")):
        if client_info:
            return MCPServerSSE(url=value, client_info=client_info)
        return MCPServerSSE(url=value)

    # Command string → MCPServerStdio
    # Split command string with shell-aware quoting support.
    # Spec source: vectl step wire-cli-params.wire-mcp-servers requires
    # command strings to map to MCPServerStdio.
    # Supports env passthrough format: "command args |env: KEY1=VAL1,KEY2=VAL2"

    # Extract env section if present
    env_dict: dict[str, str] | None = None
    env_prefix = " |env: "
    if env_prefix in value:
        # Split on the env marker
        cmd_part, env_part = value.split(env_prefix, 1)
        # Parse env variables: "KEY1=VALUE1,KEY2=VALUE2"
        env_dict = {}
        if env_part.strip():
            # Parse comma-separated KEY=VALUE pairs
            # Handle escaped commas (\\,) by temporarily replacing them
            env_part = env_part.strip()
            current_pair = ""
            i = 0
            while i < len(env_part):
                if env_part[i : i + 2] == "\\,":
                    # Escaped comma, keep literal comma
                    current_pair += ","
                    i += 2
                elif env_part[i] == ",":
                    # Unescaped comma splits pairs
                    if current_pair and "=" in current_pair:
                        k, v = current_pair.split("=", 1)
                        env_dict[k.strip()] = v
                    current_pair = ""
                    i += 1
                else:
                    current_pair += env_part[i]
                    i += 1
            # Don't forget the last pair
            if current_pair and "=" in current_pair:
                k, v = current_pair.split("=", 1)
                env_dict[k.strip()] = v
        value = cmd_part  # Update value to be just the command part

    try:
        parts = shlex.split(value)
    except ValueError as exc:
        raise ValueError(f"Invalid MCP command specification: {value!r}") from exc
    if not parts:
        raise ValueError(f"Empty MCP server specification: {value!r}")

    command = parts[0]
    args = parts[1:] if len(parts) > 1 else []

    return build_mcp_server_stdio(
        command=command,
        args=args,
        env=env_dict,
        client_info=client_info,
    )


# @invar:allow shell_result: Factory function returns list of MCPServer objects, not Result
# @shell_orchestration: Transforms strings to objects, no I/O
def parse_mcp_servers(
    values: list[str] | None,
    capability_token: CapabilityToken | None = None,
) -> list[MCPServer] | None:
    """Parse multiple --mcp arguments into MCPServer instances.

    @pre: lambda values: values is None or len(values) >= 0
    @post: lambda values, result: result is None if not values else len(result) == len(values)

    Args:
        values: List of --mcp argument values (URLs or commands).
        capability_token: Optional CapabilityToken to pass to MCP server
            via client_info.capability_token (docs/INTERFACES.md H.5).

    Returns:
        List of MCPServer instances, or None if empty.

    Raises:
        ValueError: If any value cannot be parsed.

    Examples:
        >>> from anima.shell.mcp_servers import parse_mcp_servers
        >>> servers = parse_mcp_servers(["https://api.example.com/mcp", "npx mcp-server /path"])
        >>> len(servers)
        2
        >>> servers[0].__class__.__name__
        'MCPServerSSE'
        >>> servers[1].__class__.__name__
        'MCPServerStdio'

        >>> # Empty list → None
        >>> parse_mcp_servers([]) is None
        True

        >>> # None → None
        >>> parse_mcp_servers(None) is None
        True
    """
    if not values:
        return None

    return [parse_mcp_server(v, capability_token) for v in values]


__all__ = ["build_mcp_server_stdio", "parse_mcp_server", "parse_mcp_servers"]
