"""Tool inspection CLI command implementations.

Spec source: plan step ``tools-command.list-cmd`` requires implementation
of `anima tools list` for displaying configured tool presets.
Spec source: plan step ``tools-command.init-cmd`` requires implementation
of `anima tools init` for scaffolding tool preset configurations.
Spec source: plan step ``tools-command.runtime-inspect`` requires implementation
of `anima tools inspect --agent <id>` for listing available tools from a running agent.
"""

# @invar:allow file_size_warning: tools command keeps init/list/inspect workflows co-located for shared option and rendering helpers

from __future__ import annotations

import sys
from pathlib import Path
from typing import Any

from rich.table import Table
from returns.result import Failure

from anima.shell.model_config import load_global_config, resolve_global_config_path


# Common tool preset templates (commented out by default)
TOOL_PRESET_TEMPLATES: dict[str, dict[str, Any]] = {
    "filesystem": {
        "command": "npx",
        "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
        "description": "Filesystem access tool",
        "sandbox": False,
        "sandbox_allow": [],
    },
    "shell": {
        "command": "uvx",
        "args": ["mcp-shell-server"],
        "env": {"ALLOW_COMMANDS": "ls,cat,echo"},
        "description": "Shell command execution",
        "sandbox": True,
        "sandbox_allow": ["fs:/tmp"],
    },
    "fetch": {
        "command": "uvx",
        "args": ["mcp-server-fetch"],
        "description": "Fetch web content",
        "sandbox": False,
        "sandbox_allow": [],
    },
    "github": {
        "command": "github-mcp-server",
        "args": ["stdio"],
        "env": {"GITHUB_PERSONAL_ACCESS_TOKEN": "YOUR_TOKEN_HERE"},
        "description": "GitHub API integration",
        "sandbox": False,
        "sandbox_allow": [],
    },
}


# @shell_complexity: Output branch (table/JSON) + empty-guidance branch + error branch
def list_tools_impl(*, json_output: bool, console) -> None:
    """Implementation for ``anima tools list``.

    Loads global config and displays configured tool presets.
    With --json, outputs as JSON array.
    If no presets, shows helpful guidance.

    Args:
        json_output: If True, output as JSON instead of table.
        console: Rich console for output.
    """
    config_result = load_global_config()

    if isinstance(config_result, Failure):
        error = config_result.failure()
        console.print(f"[red]Error loading config: {error.message}[/red]")
        return

    config = config_result.unwrap()

    # Prepare data
    presets = []
    for name, preset_config in sorted(config.tools.items()):
        presets.append(
            {
                "name": name,
                "command": preset_config.command,
                "args": preset_config.args,
                "description": preset_config.description or preset_config.command,
            }
        )

    # Handle empty case
    if not presets:
        if json_output:
            console.print_json(data=[])
        else:
            console.print("[dim]No tool presets configured.[/dim]")
            console.print()
            console.print("Add tool presets to [cyan]~/.anima/config.toml[/cyan]:")
            console.print()
            console.print("[dim]Example:[/dim]")
            console.print()
            console.print("  [tools]")
            console.print(
                '  filesystem = { command = "npx", args = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] }'
            )  # noqa: E501
            console.print('  shell = { command = "uvx", args = ["mcp-shell-server"] }')  # noqa: E501
        return

    # Output
    if json_output:
        console.print_json(data=presets)
    else:
        table = Table(title="Tool Presets")
        table.add_column("Name", style="cyan", no_wrap=True)
        table.add_column("Command", style="white")
        table.add_column("Description", style="dim")

        for preset in presets:
            # Build command display: show command + args if present
            if preset["args"]:
                cmd_display = f"{preset['command']} {' '.join(preset['args'])}"
            else:
                cmd_display = preset["command"]

            table.add_row(
                preset["name"],
                cmd_display,
                preset["description"],
            )

        console.print(table)


# @shell_complexity: Config I/O + TOML template generation + idempotency checks
def init_tools_impl(*, preset_name: str | None, json_output: bool, console) -> None:
    """Implementation for ``anima tools init``.

    Creates or updates ~/.anima/config.toml with tool preset scaffolding.
    Idempotent: running twice is safe, never overwrites existing [tools.*].

    Behavior:
        1) If config doesn't exist -> create with defaults + commented templates
        2) If config exists and no [tools.*] -> append commented templates
        3) If config exists with [tools.*] -> skip (idempotent)
        4) Print guidance to uncomment presets

    Args:
        preset_name: If provided, scaffold only this preset. If None, scaffold all.
        json_output: If True, output as JSON instead of Rich text.
        console: Rich console for output.
    """
    config_path = resolve_global_config_path()

    # Map preset_name to template key
    if preset_name is not None:
        if preset_name not in TOOL_PRESET_TEMPLATES:
            if json_output:
                console.print_json(
                    data={
                        "error": {
                            "code": "INVALID_INPUT",
                            "numeric_code": 1,
                            "message": f"Unknown preset '{preset_name}'",
                            "details": {
                                "valid_presets": list(TOOL_PRESET_TEMPLATES.keys())
                            },
                        }
                    }
                )
            else:
                console.print(f"[red]Error: Unknown preset '{preset_name}'[/red]")
                console.print(
                    f"[dim]Valid presets: {', '.join(TOOL_PRESET_TEMPLATES.keys())}[/dim]"
                )
            return
        templates_to_scaffold = {preset_name: TOOL_PRESET_TEMPLATES[preset_name]}
    else:
        templates_to_scaffold = TOOL_PRESET_TEMPLATES

    # Check if config exists
    config_exists = config_path.exists()

    if not config_exists:
        # Create new config with defaults + templates
        _create_new_config_with_templates(
            config_path, templates_to_scaffold, json_output, console
        )
        return

    # Config exists - check for existing tools sections
    config_result = load_global_config()

    if isinstance(config_result, Failure):
        error = config_result.failure()
        if json_output:
            console.print_json(
                data={
                    "error": {
                        "code": error.code,
                        "numeric_code": getattr(error, "numeric_code", error.code),
                        "message": error.message,
                    }
                }
            )
        else:
            console.print(f"[red]Error loading config: {error.message}[/red]")
        return

    config = config_result.unwrap()

    # Check for existing tool presets (active)
    existing_tools = set(config.tools.keys())

    # Also check if commented templates already exist in file
    config_content = config_path.read_text(encoding="utf-8")
    already_has_commented_templates = any(
        f"# [tools.{name}]" in config_content for name in templates_to_scaffold.keys()
    )

    # Filter to templates that don't exist yet (either active or commented)
    templates_to_add = {
        name: template
        for name, template in templates_to_scaffold.items()
        if name not in existing_tools
    }

    if not templates_to_add or already_has_commented_templates:
        # All presets already exist (active or commented) - idempotent success
        if json_output:
            console.print_json(
                data={
                    "status": "unchanged",
                    "message": "All requested presets already exist in config",
                    "existing_presets": sorted(existing_tools)
                    if existing_tools
                    else [],
                }
            )
        else:
            console.print("[dim]All requested presets already exist in config.[/dim]")
            console.print()
            console.print(f"Config location: [cyan]{config_path}[/cyan]")
            if existing_tools:
                console.print(f"Existing presets: {', '.join(sorted(existing_tools))}")
        return

    # Append templates to existing config
    _append_templates_to_config(
        config_path, templates_to_add, existing_tools, json_output, console
    )


# @invar:allow shell_complexity: Config file generation has 5 branches for multi-field templating (command/args/sandbox/allow/description)
# @shell_complexity: Spec-source tools.init; TOML template generation requires conditional field branches
def _create_new_config_with_templates(
    config_path: Path,
    templates: dict[str, dict[str, Any]],
    json_output: bool,
    console,
) -> None:
    """Create new config file with defaults and commented templates."""
    config_path.parent.mkdir(parents=True, exist_ok=True)

    # Default config content
    lines = [
        "# anima configuration file",
        "# See https://github.com/tfx/anima for documentation",
        "",
        "[defaults]",
        '# model = "gpt-5.2-mini"',
        "",
        "[env]",
        "# Set environment variables for tool servers",
        '# OPENAI_API_KEY = "sk-..."',
        "",
        "[tools]",
        "# Tool presets - uncomment to enable",
        "",
    ]

    # Add commented templates
    for name, template in templates.items():
        lines.append(f"# [tools.{name}]")
        lines.append(f'# command = "{template["command"]}"')
        if template["args"]:
            args_str = ", ".join(f'"{arg}"' for arg in template["args"])
            lines.append(f"# args = [{args_str}]")
        else:
            lines.append("# args = []")
        # env is dict
        env = template.get("env", {})
        if env:
            for env_key, env_value in env.items():
                lines.append(f'# env."{env_key}" = "{env_value}"')
        lines.append(f'# description = "{template["description"]}"')
        # sandbox is bool
        lines.append(f"# sandbox = {str(template.get('sandbox', False)).lower()}")
        # sandbox_allow is list
        sandbox_allow = template.get("sandbox_allow", [])
        if sandbox_allow:
            allow_str = ", ".join(f'"{a}"' for a in sandbox_allow)
            lines.append(f"# sandbox_allow = [{allow_str}]")
        else:
            lines.append("# sandbox_allow = []")
        lines.append("")

    content = "\n".join(lines)
    config_path.write_text(content, encoding="utf-8")

    if json_output:
        console.print_json(
            data={
                "status": "created",
                "path": str(config_path),
                "presets": list(templates.keys()),
            }
        )
    else:
        console.print(
            f"[green]Created new config at[/green] [cyan]{config_path}[/cyan]"
        )
        console.print()
        console.print("Added presets (commented):")
        for name in sorted(templates.keys()):
            console.print(f"  • {name}")
        console.print()
        console.print(
            f"[dim]Edit {config_path} to uncomment and enable tool presets.[/dim]"
        )


# @invar:allow shell_complexity: Config append has 8 branches for conditional field handling (command/args/description/sandbox/allow/json vs plain output)
# @shell_complexity: Spec-source tools.init; append mode requires conditional output and field presence checks
def _append_templates_to_config(
    config_path: Path,
    templates: dict[str, dict[str, Any]],
    existing_presets: set[str],
    json_output: bool,
    console,
) -> None:
    """Append commented templates to existing config file."""
    existing_content = config_path.read_text(encoding="utf-8")

    # Build template lines
    template_lines = ["\n"]

    for name, template in sorted(templates.items()):
        if name in existing_presets:
            continue

        template_lines.append(f"# [tools.{name}]")
        template_lines.append(f'# command = "{template["command"]}"')
        if template["args"]:
            args_str = ", ".join(f'"{arg}"' for arg in template["args"])
            template_lines.append(f"# args = [{args_str}]")
        else:
            template_lines.append("# args = []")
        # env is dict
        env = template.get("env", {})
        if env:
            for env_key, env_value in env.items():
                template_lines.append(f'# env."{env_key}" = "{env_value}"')
        template_lines.append(f'# description = "{template["description"]}"')
        # sandbox is bool
        template_lines.append(
            f"# sandbox = {str(template.get('sandbox', False)).lower()}"
        )
        # sandbox_allow is list
        sandbox_allow = template.get("sandbox_allow", [])
        if sandbox_allow:
            allow_str = ", ".join(f'"{a}"' for a in sandbox_allow)
            template_lines.append(f"# sandbox_allow = [{allow_str}]")
        else:
            template_lines.append("# sandbox_allow = []")
        template_lines.append("")

    # Append to existing config
    new_content = existing_content + "\n".join(template_lines)
    config_path.write_text(new_content, encoding="utf-8")

    if json_output:
        console.print_json(
            data={
                "status": "updated",
                "path": str(config_path),
                "added_presets": list(templates.keys()),
                "existing_presets": sorted(existing_presets)
                if existing_presets
                else [],
            }
        )
    else:
        console.print(f"[green]Updated config at[/green] [cyan]{config_path}[/cyan]")
        console.print()
        console.print("Added presets (commented):")
        for name in sorted(templates.keys()):
            console.print(f"  • {name}")
        if existing_presets:
            console.print()
            console.print(f"Existing presets: {', '.join(sorted(existing_presets))}")
        console.print()
        console.print(
            f"[dim]Edit {config_path} to uncomment and enable tool presets.[/dim]"
        )


# =============================================================================
# Runtime Tool Inspection
# =============================================================================


# @invar:allow shell_result: Pure path builder helper for CLI
def _get_agent_socket_path(agent_id: str) -> Path:
    """Get the MCP socket path for a running agent.

    Args:
        agent_id: The agent identifier (directory key).

    Returns:
        Path to the agent's MCP socket.

    Examples:
        >>> _get_agent_socket_path('ag_abc123')
        PosixPath('/.../agents/ag_abc123/mcp.sock')
    """
    from anima.shell.state import get_state_dir

    return get_state_dir() / "agents" / agent_id / "mcp.sock"


# @invar:allow shell_result: Pure formatting helper for CLI output
# @shell_orchestration: Tool summary extraction scoped to CLI command module
def _format_tool_summary(tool_info: dict[str, Any]) -> dict[str, str | None]:
    """Extract summary fields from MCP tool info.

    Args:
        tool_info: Tool info dict from MCP list_tools.

    Returns:
        Summary dict with name, description, and schema hint.
    """
    name = tool_info.get("name", "unknown")
    description = tool_info.get("description") or None

    # Schema summary: show input key count if schema exists
    input_schema = tool_info.get("inputSchema") or tool_info.get("input_schema")
    schema_hint = None
    if isinstance(input_schema, dict):
        props = input_schema.get("properties", {})
        required = input_schema.get("required", [])
        if props:
            schema_hint = f"{len(props)} params"
            if required:
                schema_hint += f" ({len(required)} required)"

    return {
        "name": name,
        "description": description,
        "schema_hint": schema_hint,
    }


# @invar:allow entry_point_too_thick: CLI command with async MCP connection
# @invar:allow function_size: CLI command with async MCP connection
# @invar:allow shell_result: CLI command prints errors and exits
# @shell_complexity: MCP connection with state validation, socket check, and output formatting
def inspect_runtime_tools_impl(
    agent_id: str,
    json_output: bool,
    console,
) -> None:
    """Implementation for ``anima tools inspect --agent <id>``.

    Connects to a running agent's MCP socket and lists available tools.
    Requires agent to be in a running state.

    Exit codes: 0 success, 1 error (not running, connection error), 2 not found.
    """
    import asyncio

    import httpx
    from mcp import ClientSession
    from mcp.client.streamable_http import streamable_http_client

    from anima.shell.cli_commands._shared import resolve_agent
    from anima.core.schemas import AgentState

    EXIT_AGENT_NOT_FOUND = 2

    # 1. Resolve agent ID
    state = resolve_agent(
        agent_id,
        json_output=json_output,
        console=console,
        exit_agent_not_found=EXIT_AGENT_NOT_FOUND,
    )
    instance_id = state.agent_id

    # 2. Check agent is running
    running_states = {
        AgentState.running,
        AgentState.suspended,
        AgentState.awaiting_approval,
    }
    if state.state not in running_states:
        if json_output:
            console.print_json(
                data={
                    "error": {
                        "code": "AGENT_NOT_RUNNING",
                        "numeric_code": 316,
                        "message": f"Agent is not running (state: {state.state.value})",
                        "details": {
                            "agent_id": instance_id,
                            "state": state.state.value,
                        },
                    }
                }
            )
        else:
            console.print(
                f"[red]Error: Agent {instance_id[:8]} is not running "
                f"(state: {state.state.value})[/red]"
            )
            console.print(
                "[dim]Only running agents can be inspected. "
                "Use 'anima run' to start an agent first.[/dim]"
            )
        sys.exit(1)

    # 3. Get socket path
    socket_path = _get_agent_socket_path(instance_id)

    if not socket_path.exists():
        if json_output:
            console.print_json(
                data={
                    "error": {
                        "code": "MCP_SOCKET_NOT_FOUND",
                        "numeric_code": 321,
                        "message": "Agent MCP socket not found. Is the agent running?",
                        "details": {
                            "agent_id": instance_id,
                            "socket_path": str(socket_path),
                        },
                    }
                }
            )
        else:
            console.print(
                f"[red]Error: Agent MCP socket not found at {socket_path}[/red]"
            )
            console.print("[dim]Is the agent running?[/dim]")
        sys.exit(1)

    # 4. Connect and list tools
    async def _list_tools_async() -> list[dict[str, Any]]:
        """Connect to agent and list available tools."""
        tools_list: list[dict[str, Any]] = []

        # Connect via Unix socket using httpx transport
        async with httpx.AsyncClient(
            transport=httpx.AsyncHTTPTransport(uds=str(socket_path)),
            timeout=httpx.Timeout(30.0, read=60.0),
        ) as client:
            async with streamable_http_client(
                "http://localhost/mcp", http_client=client
            ) as (
                read_stream,
                write_stream,
                _get_session_id,
            ):
                async with ClientSession(read_stream, write_stream) as session:
                    await session.initialize()

                    # list_tools returns ListToolsResult
                    result = await session.list_tools()

                    for tool in result.tools:
                        tool_info: dict[str, Any] = {
                            "name": tool.name,
                        }
                        if hasattr(tool, "description") and tool.description:
                            tool_info["description"] = tool.description
                        # inputSchema is part of Tool object
                        if hasattr(tool, "inputSchema") and tool.inputSchema:
                            tool_info["inputSchema"] = tool.inputSchema
                        tools_list.append(tool_info)

        return tools_list

    try:
        tools = asyncio.run(_list_tools_async())
    except FileNotFoundError:
        if json_output:
            console.print_json(
                data={
                    "error": {
                        "code": "MCP_SOCKET_NOT_FOUND",
                        "numeric_code": 321,
                        "message": "Agent MCP socket not found during connection",
                        "details": {"agent_id": instance_id},
                    }
                }
            )
        else:
            console.print("[red]Error: Agent MCP socket not found.[/red]")
        sys.exit(1)
    except ConnectionRefusedError:
        if json_output:
            console.print_json(
                data={
                    "error": {
                        "code": "MCP_CONNECTION_REFUSED",
                        "numeric_code": 322,
                        "message": "Agent refused MCP connection. Agent may be overloaded.",
                        "details": {"agent_id": instance_id},
                    }
                }
            )
        else:
            console.print(
                "[red]Error: Agent refused connection. "
                "The agent may be overloaded or terminating.[/red]"
            )
        sys.exit(1)
    except (OSError, ConnectionError, RuntimeError, ValueError, TypeError) as e:
        if json_output:
            console.print_json(
                data={
                    "error": {
                        "code": "MCP_CONNECTION_ERROR",
                        "numeric_code": 323,
                        "message": f"Connection error: {e}",
                        "details": {"agent_id": instance_id},
                    }
                }
            )
        else:
            console.print(f"[red]Connection error: {e}[/red]")
        sys.exit(1)

    # 5. Output results
    if json_output:
        console.print_json(
            data={
                "agent_id": instance_id,
                "tool_count": len(tools),
                "tools": tools,
            }
        )
    else:
        if not tools:
            console.print(f"[dim]No tools available for agent {instance_id[:8]}[/dim]")
            sys.exit(0)

        console.print(f"[bold]Available tools for agent {instance_id[:8]}:[/bold]")
        console.print(f"[dim]Total: {len(tools)} tools[/dim]")
        console.print()

        table = Table(show_header=True, header_style="bold")
        table.add_column("Tool", style="cyan", no_wrap=True)
        table.add_column("Description", style="white")
        table.add_column("Schema", style="dim")

        for tool in tools:
            summary = _format_tool_summary(tool)
            table.add_row(
                summary["name"] or "unknown",
                (summary["description"] or "")[:60]
                + (
                    "..."
                    if summary["description"] and len(summary["description"]) > 60
                    else ""
                ),
                summary["schema_hint"] or "-",
            )

        console.print(table)

    sys.exit(0)
