"""MCP bridge for agent tool invocation and state fallback.

This module provides bridge functions for calling tools on agent MCP servers
and reading agent state files as a fallback when agents are dead or missing.

Bridge behavior:
- Alive agent -> MCP socket tool call
- Dead/missing -> Fallback to state file read

docs/INTERFACES.md Section B defines per-agent MCP tools:
- anima.status() - live agent state and metrics
- anima.interact(message) - send message to agent
- anima.interrupt() - request graceful shutdown
- anima.logs() - get agent log entries
"""

# @shell_complexity: MCP connection with Unix socket transport requires multiple error handling paths
# @shell_orchestration: Orchestrates MCP socket calls and fallback state reads

import json
from pathlib import Path
from typing import Any

import httpx
from returns.result import Failure, Result, Success

from anima.core.schemas import (
    AgentNotFoundError,
    AnimaError,
    InternalError,
    MCPSocketNotFoundError,
    MCPConnectionError,
)
from anima.shell.state import (
    get_agent_dir,
    get_state_dir,
    read_state,
)


# @shell_orchestration: Path construction for MCP socket used by bridge orchestration
# @invar:allow shell_result: Path construction never fails
def get_mcp_socket_path(instance_id: str) -> Path:
    """Get the MCP socket path for an agent.

    Args:
        instance_id: The agent's instance identifier.

    Returns:
        Path to the Unix domain socket.
    """
    return get_state_dir() / "agents" / instance_id / "mcp.sock"


# @invar:allow shell_result: Boolean liveness check never fails
def is_agent_alive(instance_id: str) -> bool:
    """Check if agent is alive (has running MCP socket).

    Args:
        instance_id: The agent's instance identifier.

    Returns:
        True if agent's MCP socket exists, False otherwise.
    """
    socket_path = get_mcp_socket_path(instance_id)
    return socket_path.exists()


def call_agent_mcp_tool(
    instance_id: str,
    tool_name: str,
    arguments: dict[str, Any] | None = None,
    timeout: float = 30.0,
) -> Result[dict[str, Any], AnimaError]:
    """Call an MCP tool on a live agent via Unix domain socket.

    For alive agents, bridges to the agent's MCP socket and calls the tool.
    For dead/missing agents, returns Failure with appropriate error.

    Args:
        instance_id: The agent's instance identifier.
        tool_name: Name of the MCP tool to call (e.g., "anima.status").
        arguments: Optional dict of tool arguments.
        timeout: Request timeout in seconds (default 30.0).

    Returns:
        Success(dict) with tool result on success.
        Failure(AnimaError) on failure:
        - AgentNotFoundError: Agent state not found
        - MCPSocketNotFoundError: Agent MCP socket not found (agent not running)
        - MCPConnectionError: Failed to connect or call tool
    """
    socket_path = get_mcp_socket_path(instance_id)

    # Check if socket exists - if not, agent is dead/missing
    if not socket_path.exists():
        return Failure(
            MCPSocketNotFoundError(
                f"Agent MCP socket not found: {socket_path}",
                details={"instance_id": instance_id, "socket_path": str(socket_path)},
            )
        )

    # Verify agent state exists
    state_result = read_state(instance_id)
    if isinstance(state_result, Failure):
        return Failure(
            AgentNotFoundError(
                f"Agent not found: {instance_id}",
                details={"instance_id": instance_id},
            )
        )

    arguments = arguments or {}

    try:
        return _call_tool_sync(
            socket_path=socket_path,
            tool_name=tool_name,
            arguments=arguments,
            timeout=timeout,
        )
    except Exception as e:
        return Failure(
            MCPConnectionError(
                f"Failed to call MCP tool {tool_name}: {e}",
                details={"instance_id": instance_id, "tool_name": tool_name},
            )
        )


async def call_agent_mcp_tool_async(
    instance_id: str,
    tool_name: str,
    arguments: dict[str, Any] | None = None,
    timeout: float = 30.0,
) -> Result[dict[str, Any], AnimaError]:
    """Async MCP tool call for use from async handlers.

    Source: bridge-and-fanout.fix-gate-blockers (B1)
    avoids nested-event-loop hazards from ``asyncio.run`` in active loops.
    """
    socket_path = get_mcp_socket_path(instance_id)

    if not socket_path.exists():
        return Failure(
            MCPSocketNotFoundError(
                f"Agent MCP socket not found: {socket_path}",
                details={"instance_id": instance_id, "socket_path": str(socket_path)},
            )
        )

    state_result = read_state(instance_id)
    if isinstance(state_result, Failure):
        return Failure(
            AgentNotFoundError(
                f"Agent not found: {instance_id}",
                details={"instance_id": instance_id},
            )
        )

    try:
        return await _call_tool_async(
            socket_path=socket_path,
            tool_name=tool_name,
            arguments=arguments or {},
            timeout=timeout,
        )
    except Exception as e:
        return Failure(
            MCPConnectionError(
                f"Failed to call MCP tool {tool_name}: {e}",
                details={"instance_id": instance_id, "tool_name": tool_name},
            )
        )


# @shell_complexity: MCP error handling requires multiple exception branches
def _call_tool_sync(
    socket_path: Path,
    tool_name: str,
    arguments: dict[str, Any],
    timeout: float,
) -> Result[dict[str, Any], AnimaError]:
    """Synchronous wrapper for MCP tool call.

    Uses asyncio.run() to execute the async MCP call.
    """
    import asyncio

    try:
        return asyncio.run(
            _call_tool_async(
                socket_path=socket_path,
                tool_name=tool_name,
                arguments=arguments,
                timeout=timeout,
            )
        )
    except Exception as e:
        return Failure(
            MCPConnectionError(
                f"Failed to call MCP tool: {e}",
                details={"socket_path": str(socket_path)},
            )
        )


async def _call_tool_async(
    socket_path: Path,
    tool_name: str,
    arguments: dict[str, Any],
    timeout: float,
) -> Result[dict[str, Any], AnimaError]:
    """Async MCP tool call implementation shared by sync/async wrappers."""
    from mcp import ClientSession
    from mcp.client.streamable_http import streamable_http_client
    from mcp.types import TextContent

    try:
        async with httpx.AsyncClient(
            transport=httpx.AsyncHTTPTransport(uds=str(socket_path)),
            timeout=httpx.Timeout(timeout, read=timeout * 2),
        ) 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()
                    mcp_result = await session.call_tool(tool_name, arguments)
                    if mcp_result.content:
                        first = mcp_result.content[0]
                        if isinstance(first, TextContent):
                            return Success(json.loads(first.text))
                    return Success({})
    except json.JSONDecodeError as e:
        return Failure(
            MCPConnectionError(
                f"Invalid JSON response from MCP tool: {e}",
                details={"socket_path": str(socket_path)},
            )
        )
    except OSError as e:
        return Failure(
            MCPConnectionError(
                f"OS error connecting to MCP socket: {e}",
                details={"socket_path": str(socket_path)},
            )
        )
    except Exception as e:
        return Failure(
            MCPConnectionError(
                f"Failed to call MCP tool: {e}",
                details={"socket_path": str(socket_path)},
            )
        )


def read_agent_state_file(
    instance_id: str,
) -> Result[dict[str, Any], AnimaError]:
    """Read agent state from state file as fallback for dead/missing agents.

    This is the fallback path when the agent's MCP socket is not available.
    Returns the agent's state.json content.

    Args:
        instance_id: The agent's instance identifier.

    Returns:
        Success(dict) with parsed state.json content on success.
        Failure(AnimaError) on failure:
        - AgentNotFoundError: Agent state file not found
        - InternalError: Failed to read or parse state file
    """
    # Try to read state file directly from disk
    agent_dir = get_agent_dir(instance_id)
    state_file = agent_dir / "state.json"

    if not state_file.exists():
        return Failure(
            AgentNotFoundError(
                f"Agent state not found: {instance_id}",
                details={"instance_id": instance_id},
            )
        )

    try:
        content = state_file.read_text()
        data = json.loads(content)
        return Success(data)
    except json.JSONDecodeError as e:
        return Failure(
            InternalError(
                f"Invalid state file for {instance_id}: {e}",
                details={"instance_id": instance_id},
            )
        )
    except OSError as e:
        return Failure(
            InternalError(
                f"Failed to read state file: {e}",
                details={"instance_id": instance_id},
            )
        )


# @invar:allow dead_export: Fallback function for fanout pattern implementation
def read_agent_state_with_fallback(
    instance_id: str,
    timeout: float = 30.0,
) -> Result[dict[str, Any], AnimaError]:
    """Read agent state, trying MCP socket first then falling back to state file.

    Bridge behavior:
    1. If agent is alive (MCP socket exists), call anima.status via MCP
    2. If agent is dead/missing (no socket), fall back to reading state.json

    Args:
        instance_id: The agent's instance identifier.
        timeout: Timeout for MCP call in seconds (default 30.0).

    Returns:
        Success(dict) with agent state on success.
        Failure(AnimaError) on failure.
    """
    # Try MCP socket first if agent is alive
    if is_agent_alive(instance_id):
        result = call_agent_mcp_tool(
            instance_id,
            "anima.status",
            timeout=timeout,
        )
        if isinstance(result, Success):
            return result

    # Fallback to reading state file
    return read_agent_state_file(instance_id)


async def read_agent_state_with_fallback_async(
    instance_id: str,
    timeout: float = 30.0,
) -> Result[dict[str, Any], AnimaError]:
    """Async fallback helper for status handlers running in an event loop."""
    if is_agent_alive(instance_id):
        result = await call_agent_mcp_tool_async(
            instance_id,
            "anima.status",
            timeout=timeout,
        )
        if isinstance(result, Success):
            return result

    return read_agent_state_file(instance_id)


def try_mcp_with_fallback(
    instance_id: str,
    tool_name: str,
    arguments: dict[str, Any] | None = None,
    timeout: float = 30.0,
) -> Result[dict[str, Any], AnimaError]:
    """Try MCP tool call first, fall back to state file if agent not alive.

    MCP-first pattern for lifecycle commands (approve/reject/resume):
    1. Check if agent is alive (socket exists)
    2. If alive, try MCP tool call
    3. If MCP fails or agent not alive, return Failure (caller handles fallback)

    Args:
        instance_id: The agent's instance identifier.
        tool_name: Name of MCP tool to call (e.g., "anima.approve").
        arguments: Optional dict of tool arguments.
        timeout: Timeout for MCP call in seconds (default 30.0).

    Returns:
        Success(dict) with tool result if MCP call succeeded.
        Failure(AnimaError) if MCP call failed or agent not alive.
    """
    if not is_agent_alive(instance_id):
        return Failure(
            MCPSocketNotFoundError(
                f"Agent not alive (no MCP socket): {instance_id}",
                details={"instance_id": instance_id},
            )
        )

    return call_agent_mcp_tool(
        instance_id,
        tool_name,
        arguments=arguments,
        timeout=timeout,
    )


async def try_mcp_with_fallback_async(
    instance_id: str,
    tool_name: str,
    arguments: dict[str, Any] | None = None,
    timeout: float = 30.0,
) -> Result[dict[str, Any], AnimaError]:
    """Async version of try_mcp_with_fallback for use in async handlers.

    MCP-first pattern for lifecycle commands (approve/reject/resume):
    1. Check if agent is alive (socket exists)
    2. If alive, try MCP tool call
    3. If MCP fails or agent not alive, return Failure (caller handles fallback)

    Args:
        instance_id: The agent's instance identifier.
        tool_name: Name of MCP tool to call (e.g., "anima.approve").
        arguments: Optional dict of tool arguments.
        timeout: Timeout for MCP call in seconds (default 30.0).

    Returns:
        Success(dict) with tool result if MCP call succeeded.
        Failure(AnimaError) if MCP call failed or agent not alive.
    """
    if not is_agent_alive(instance_id):
        return Failure(
            MCPSocketNotFoundError(
                f"Agent not alive (no MCP socket): {instance_id}",
                details={"instance_id": instance_id},
            )
        )

    return await call_agent_mcp_tool_async(
        instance_id,
        tool_name,
        arguments=arguments,
        timeout=timeout,
    )
