"""Persona management CLI command implementations.

Spec source: plan step ``persona-list-show.list-command`` requires implementation
of `anima persona list` for listing registered personas from larva registry.
Spec source: plan step ``persona-list-show.show-command`` adds
`anima persona show <name>` for showing details of a specific persona.
"""

from __future__ import annotations

import asyncio
import json
import sys
from typing import Any

import click
from rich.console import Console
from rich.table import Table
from returns.result import Failure

from anima.shell.model_config import load_global_config


LARVA_LIST_TOOL = "larva_list"
LARVA_RESOLVE_TOOL = "larva_resolve"


_NO_REGISTRY_MESSAGE = """\
error: persona commands require a registry (larva)

  anima persona list --mcp "uvx larva mcp"

  Or configure a default:
    [tools.larva]
    cmd = "uvx larva mcp"\
"""


# @invar:allow shell_result: Pure config lookup helper returns optional string, not Result
def _get_larva_mcp_from_config() -> str | None:
    """Check ~/.anima/config.toml for [tools.larva] preset.

    Returns:
        MCP command string if configured, None otherwise.
    """
    config_result = load_global_config()
    if isinstance(config_result, Failure):
        return None

    config = config_result.unwrap()
    larva_preset = config.tools.get("larva")
    if larva_preset is None:
        return None

    # Reconstruct the command string from command + args
    parts = [larva_preset.command] + list(larva_preset.args)
    return " ".join(parts)


# @invar:allow shell_result: Internal MCP bridge helper, caller wraps errors
async def _call_larva_list(mcp_servers: list[Any]) -> list[dict[str, Any]]:
    """Connect to larva MCP and call larva_list tool.

    Args:
        mcp_servers: List of MCP server instances (pydantic_ai MCPServer).

    Returns:
        List of persona dicts from larva registry.

    Raises:
        RuntimeError: If larva_list tool not found or call fails.
    """
    for toolset in mcp_servers:
        try:
            tools = await toolset.get_tools(None)
            if LARVA_LIST_TOOL in tools:
                tool = tools[LARVA_LIST_TOOL]
                result = await toolset.call_tool(
                    LARVA_LIST_TOOL,
                    {},
                    None,  # ctx
                    tool,
                )
                return _parse_larva_list_result(result)
        except Exception as exc:
            raise RuntimeError(f"larva_list call failed: {exc}") from exc

    raise RuntimeError(f"No MCP server provides '{LARVA_LIST_TOOL}' tool")


# @invar:allow shell_result: Pure parsing helper returns parsed data, not Result
# @shell_complexity: MCP response format has multiple content shapes (list/str/dict with nested personas)
def _parse_larva_list_result(result: Any) -> list[dict[str, Any]]:
    """Parse the raw MCP result from larva_list into a list of persona dicts.

    Args:
        result: Raw MCP tool call result.

    Returns:
        List of persona dicts with at least 'id' field.
    """
    if isinstance(result, list) and result:
        # MCP response format: list of content items with .text
        first = result[0]
        text = getattr(first, "text", None) or (
            first.get("text") if isinstance(first, dict) else None
        )
        if isinstance(text, str):
            parsed = json.loads(text)
            if isinstance(parsed, list):
                return parsed
            if isinstance(parsed, dict) and "personas" in parsed:
                return parsed["personas"]
            return [parsed]

    if isinstance(result, str):
        parsed = json.loads(result)
        if isinstance(parsed, list):
            return parsed
        if isinstance(parsed, dict) and "personas" in parsed:
            return parsed["personas"]
        return [parsed]

    if isinstance(result, dict):
        if "personas" in result:
            return result["personas"]
        return [result]

    return []


# @invar:allow shell_result: CLI command prints errors and exits
# @invar:allow entry_point_too_thick: Click command with MCP connection and output formatting
# @shell_complexity: MCP connection with config fallback and output formatting
def persona_list_impl(
    *,
    mcp: tuple[str, ...],
    json_output: bool,
    console: Console,
) -> None:
    """Implementation for ``anima persona list``.

    Connects to larva MCP and lists registered personas.
    Supports --mcp flag and config.toml [tools.larva] fallback.

    Args:
        mcp: Tuple of MCP server command strings from --mcp flag.
        json_output: If True, output as JSON instead of table.
        console: Rich console for output.
    """
    from anima.shell.mcp_servers import parse_mcp_servers

    # Determine MCP sources: --mcp flags take priority, then config.toml
    mcp_values: list[str] = list(mcp)
    if not mcp_values:
        config_larva = _get_larva_mcp_from_config()
        if config_larva:
            mcp_values = [config_larva]

    if not mcp_values:
        if json_output:
            console.print_json(
                data={
                    "error": {
                        "code": "NO_REGISTRY",
                        "numeric_code": 1,
                        "message": "persona commands require a registry (larva)",
                    }
                }
            )
        else:
            console.print(_NO_REGISTRY_MESSAGE)
        sys.exit(1)

    servers = parse_mcp_servers(mcp_values)
    if not servers:
        if json_output:
            console.print_json(
                data={
                    "error": {
                        "code": "NO_REGISTRY",
                        "numeric_code": 1,
                        "message": "Failed to parse MCP server configuration",
                    }
                }
            )
        else:
            console.print(_NO_REGISTRY_MESSAGE)
        sys.exit(1)

    try:
        personas = asyncio.run(_call_larva_list(servers))
    except Exception as exc:
        if json_output:
            console.print_json(
                data={
                    "error": {
                        "code": "LARVA_ERROR",
                        "numeric_code": 1,
                        "message": str(exc),
                    }
                }
            )
        else:
            console.print(f"[red]Error: {exc}[/red]")
        sys.exit(1)

    if json_output:
        console.print_json(data=personas)
        return

    if not personas:
        console.print("[dim]No personas registered.[/dim]")
        return

    table = Table(title="Personas")
    table.add_column("ID", style="cyan", no_wrap=True)
    table.add_column("Model", style="white")
    table.add_column("Description", style="dim")

    for persona in personas:
        table.add_row(
            str(persona.get("id", "")),
            str(persona.get("model", "")),
            str(persona.get("description", "")),
        )

    console.print(table)


# @invar:allow shell_result: Internal MCP bridge helper, caller wraps errors
async def _call_larva_resolve_raw(mcp_servers: list[Any], name: str) -> dict[str, Any]:
    """Connect to larva MCP and call larva_resolve tool.

    Args:
        mcp_servers: List of MCP server instances (pydantic_ai MCPServer).
        name: Persona ID to resolve.

    Returns:
        Parsed persona dict from larva registry.

    Raises:
        RuntimeError: If larva_resolve tool not found or call fails.
    """
    for toolset in mcp_servers:
        try:
            tools = await toolset.get_tools(None)
            if LARVA_RESOLVE_TOOL in tools:
                tool = tools[LARVA_RESOLVE_TOOL]
                result = await toolset.call_tool(
                    LARVA_RESOLVE_TOOL,
                    {"id": name},
                    None,  # ctx
                    tool,
                )
                return _parse_resolve_result(result, name)
        except RuntimeError:
            raise
        except Exception as exc:
            raise RuntimeError(f"larva_resolve call failed: {exc}") from exc

    raise RuntimeError(f"No MCP server provides '{LARVA_RESOLVE_TOOL}' tool")


# @invar:allow shell_result: Pure parsing helper returns parsed data, not Result
def _parse_resolve_result(result: Any, name: str) -> dict[str, Any]:
    """Parse the raw MCP result from larva_resolve into a persona dict.

    Args:
        result: Raw MCP tool call result.
        name: Persona name for error messages.

    Returns:
        Persona dict with persona fields.

    Raises:
        RuntimeError: If the result indicates persona not found or is unparseable.
    """
    data: dict[str, Any] | None = None

    if isinstance(result, list) and result:
        first = result[0]
        text = getattr(first, "text", None) or (
            first.get("text") if isinstance(first, dict) else None
        )
        if isinstance(text, str):
            data = json.loads(text)
    elif isinstance(result, str):
        data = json.loads(result)
    elif isinstance(result, dict):
        data = result

    if data is None or not isinstance(data, dict):
        raise RuntimeError(
            f"Unexpected response type from larva_resolve: {type(result).__name__}"
        )

    if "error" in data:
        error_info = data["error"]
        message = (
            error_info.get("message", str(error_info))
            if isinstance(error_info, dict)
            else str(error_info)
        )
        raise RuntimeError(message)

    return data


# @invar:allow shell_result: Helper exits process on failure, caller gets servers or program exits
def _resolve_mcp_servers(
    mcp: tuple[str, ...],
    json_output: bool,
    console: Console,
) -> list[Any]:
    """Resolve MCP servers from flags or config, exit on failure.

    Args:
        mcp: Tuple of MCP server command strings from --mcp flag.
        json_output: If True, errors are formatted as JSON.
        console: Rich console for output.

    Returns:
        List of parsed MCP server instances.
    """
    from anima.shell.mcp_servers import parse_mcp_servers

    mcp_values: list[str] = list(mcp)
    if not mcp_values:
        config_larva = _get_larva_mcp_from_config()
        if config_larva:
            mcp_values = [config_larva]

    if not mcp_values:
        if json_output:
            console.print_json(
                data={
                    "error": {
                        "code": "NO_REGISTRY",
                        "numeric_code": 1,
                        "message": "persona commands require a registry (larva)",
                    }
                }
            )
        else:
            console.print(_NO_REGISTRY_MESSAGE)
        sys.exit(1)

    servers = parse_mcp_servers(mcp_values)
    if not servers:
        if json_output:
            console.print_json(
                data={
                    "error": {
                        "code": "NO_REGISTRY",
                        "numeric_code": 1,
                        "message": "Failed to parse MCP server configuration",
                    }
                }
            )
        else:
            console.print(_NO_REGISTRY_MESSAGE)
        sys.exit(1)

    return servers


# @invar:allow shell_result: Pure formatting helper returns display string, not Result
def _format_tools(tools: dict[str, Any] | list[str] | Any) -> str:
    """Format tools field for display.

    Args:
        tools: Tools from persona spec (dict of postures or list of names).

    Returns:
        Comma-separated tool names string, or empty string.
    """
    if isinstance(tools, dict):
        return ", ".join(sorted(tools.keys())) if tools else ""
    if isinstance(tools, list):
        return ", ".join(str(t) for t in tools)
    return str(tools) if tools else ""


# @invar:allow shell_result: CLI command prints errors and exits
# @invar:allow entry_point_too_thick: Click command with MCP connection and output formatting
# @shell_complexity: MCP connection with config fallback and output formatting
def persona_show_impl(
    *,
    name: str,
    mcp: tuple[str, ...],
    json_output: bool,
    console: Console,
) -> None:
    """Implementation for ``anima persona show``.

    Connects to larva MCP and shows details of a specific persona.
    If persona not found, suggests available names from larva_list.

    Args:
        name: Persona ID to look up.
        mcp: Tuple of MCP server command strings from --mcp flag.
        json_output: If True, output as JSON instead of formatted text.
        console: Rich console for output.
    """
    servers = _resolve_mcp_servers(mcp=mcp, json_output=json_output, console=console)

    try:
        persona_data = asyncio.run(_call_larva_resolve_raw(servers, name))
    except RuntimeError as exc:
        # Attempt to list available personas for suggestions
        suggestions: list[str] = []
        try:
            all_personas = asyncio.run(_call_larva_list(servers))
            suggestions = [str(p.get("id", "")) for p in all_personas if p.get("id")]
        except Exception:
            pass

        if json_output:
            error_data: dict[str, Any] = {
                "error": {
                    "code": "NOT_FOUND",
                    "numeric_code": 1,
                    "message": f"Persona '{name}' not found: {exc}",
                }
            }
            if suggestions:
                error_data["error"]["available"] = suggestions
            console.print_json(data=error_data)
        else:
            console.print(f"[red]Error: Persona '{name}' not found: {exc}[/red]")
            if suggestions:
                console.print(f"\nAvailable personas: {', '.join(suggestions)}")
        sys.exit(1)
    except Exception as exc:
        if json_output:
            console.print_json(
                data={
                    "error": {
                        "code": "LARVA_ERROR",
                        "numeric_code": 1,
                        "message": str(exc),
                    }
                }
            )
        else:
            console.print(f"[red]Error: {exc}[/red]")
        sys.exit(1)

    if json_output:
        console.print_json(data=persona_data)
        return

    # Formatted text output
    console.print(f"[cyan bold]Name:[/cyan bold]        {persona_data.get('id', '')}")
    console.print(
        f"[cyan bold]Model:[/cyan bold]       {persona_data.get('model', '')}"
    )
    console.print(
        f"[cyan bold]Description:[/cyan bold] {persona_data.get('description', '')}"
    )
    console.print(
        f"[cyan bold]Prompt:[/cyan bold]      {persona_data.get('prompt', '')}"
    )

    tools_str = _format_tools(persona_data.get("tools", {}))
    if tools_str:
        console.print(f"[cyan bold]Tools:[/cyan bold]       {tools_str}")
