"""Sentinel MCP Server — Expose Sentinel prediction market API as MCP tools.

On first run, auto-generates an Ed25519 keypair and stores it in
~/.sentinel-agent/identity.json. The operator must then register the agent
through the Sentinel console using the public key, approve it, and issue
TRADE capabilities before the agent can trade.

Tools:
    whoami           — Show agent identity and registration status
    set_agent_id     — Save the operator-assigned agent ID after registration
    list_markets     — List prediction markets (optional status filter)
    get_market       — Get full details of a specific market
    get_market_trades — Get recent trades on a market
    place_trade      — Buy YES or NO shares (runs 7-check policy gate)
    get_position     — Check your position in a specific market
    my_status        — Check agent profile, status, and capabilities
    kill_switch_status — Check if the platform is halted

Usage:
    # Via MCP CLI (development)
    mcp dev src/sentinel_mcp/server.py

    # Via Claude Desktop (production)
    Add to claude_desktop_config.json mcpServers section.
"""

import asyncio
import atexit
import json
import logging
import os
import uuid

import httpx
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP

from sentinel_mcp.client import SentinelClient
from sentinel_mcp.identity import load_or_create_identity, save_agent_id

load_dotenv()

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# --- Initialize MCP server and Sentinel client ---

mcp = FastMCP(
    "sentinel-agent",
    instructions=(
        "You are a Sentinel prediction market agent. "
        "On first use, call whoami to check your identity and registration status. "
        "If unregistered, present your public key to the operator so they can "
        "register you in the Sentinel console. Once the operator gives you your "
        "agent ID, call set_agent_id to save it. "
        "Use these tools to browse markets, research topics, check your status, "
        "and place trades. Always check kill_switch_status before trading. "
        "The platform enforces a 7-check policy gate on every trade — if denied, "
        "report the reason clearly."
    ),
)

_client: SentinelClient | None = None
_identity: dict | None = None


def _get_identity() -> dict:
    """Load or create the agent identity (keypair + optional agent_id)."""
    global _identity
    if _identity is None:
        _identity = load_or_create_identity()
    return _identity


def _get_api_url() -> str:
    """Get the Sentinel API base URL from environment."""
    return os.environ.get("SENTINEL_API_URL", "http://localhost:8080/api/v1")


def _get_client() -> SentinelClient:
    """Lazy-initialize the Sentinel client from auto-generated identity."""
    global _client
    if _client is None:
        identity = _get_identity()
        agent_id = identity.get("agent_id")

        if not agent_id:
            raise RuntimeError(
                "Agent is not registered yet. Call whoami to get your public key, "
                "then ask the operator to register you in the Sentinel console. "
                "Once they give you your agent ID, call set_agent_id to save it."
            )

        _client = SentinelClient(
            api_url=_get_api_url(),
            agent_id=agent_id,
            public_key=identity["public_key"],
            private_key=identity["private_key"],
        )
        logger.info("Sentinel client initialized for agent %s", agent_id)
    return _client


async def _close_client() -> None:
    """Close the cached client's HTTP connection if open."""
    global _client
    if _client is not None:
        try:
            await _client.close()
        except Exception:
            pass  # Best-effort cleanup


def _reset_client() -> None:
    """Reset the cached client (used after saving agent_id).

    Note: This does not close the httpx connection synchronously.
    The old client will be garbage-collected, and httpx will issue
    a ResourceWarning. For clean shutdown, use _close_client().
    """
    global _client, _identity
    _client = None
    _identity = None


def _format(data: object) -> str:
    """Format API response as readable JSON."""
    return json.dumps(data, indent=2, default=str)


async def _safe_call(coro_fn) -> str:
    """Invoke an async callable and return a structured JSON error on any failure.

    Catches all common failure modes so every tool always returns parseable JSON:
    - RuntimeError  → agent not registered yet
    - HTTPStatusError → backend returned 4xx/5xx (includes policy gate denials)
    - TimeoutException → backend unreachable or slow
    - ConnectError  → backend not running / wrong URL
    - Exception     → unexpected internal error
    """
    try:
        return _format(await coro_fn())
    except RuntimeError as e:
        return _format({
            "error": "not_registered",
            "message": str(e),
        })
    except httpx.HTTPStatusError as e:
        try:
            details = e.response.json()
        except Exception:
            details = e.response.text
        return _format({
            "error": f"HTTP {e.response.status_code}",
            "details": details,
        })
    except httpx.TimeoutException:
        return _format({
            "error": "timeout",
            "message": "Backend request timed out. Check if Sentinel backend is running.",
        })
    except httpx.ConnectError as e:
        return _format({
            "error": "connection_error",
            "message": f"Cannot reach backend at {_get_api_url()}: {e}",
        })
    except Exception as e:
        return _format({
            "error": "internal_error",
            "message": str(e),
        })


# --- Identity Tools ---


@mcp.tool()
async def whoami() -> str:
    """Show this agent's identity and registration status.

    On first call, an Ed25519 keypair is auto-generated. The public key is
    displayed so the operator can register the agent in the Sentinel console.

    Returns:
        Public key, agent_id (if registered), and registration status.
    """
    identity = _get_identity()
    agent_id = identity.get("agent_id")

    if agent_id:
        return _format({
            "status": "registered",
            "agent_id": agent_id,
            "public_key": identity["public_key"],
            "message": "Agent is registered. Use my_status to check approval state and capabilities.",
        })

    return _format({
        "status": "unregistered",
        "public_key": identity["public_key"],
        "message": (
            "Agent keypair generated but not yet registered. "
            "Give the public key above to the operator so they can register "
            "this agent in the Sentinel console. Once registered, the operator "
            "will provide an agent ID — call set_agent_id with it."
        ),
    })


@mcp.tool()
async def set_agent_id(agent_id: str) -> str:
    """Save the agent ID assigned by the operator after registration.

    The operator registers this agent in the Sentinel console using the
    public key from whoami, then provides the assigned agent ID (UUID).
    Call this tool to save it locally so all future API calls are authenticated.

    Args:
        agent_id: UUID assigned by the Sentinel backend during registration.

    Returns:
        Confirmation with saved agent_id and next steps.
    """
    # Validate UUID format before persisting
    try:
        uuid.UUID(agent_id)
    except ValueError:
        return _format({
            "status": "error",
            "message": f"Invalid agent_id: '{agent_id}' is not a valid UUID. "
                       "The agent ID should look like: 550e8400-e29b-41d4-a716-446655440000",
        })

    identity = _get_identity()

    if identity.get("agent_id"):
        return _format({
            "status": "already_set",
            "agent_id": identity["agent_id"],
            "message": "Agent ID is already configured. Use my_status to check current state.",
        })

    save_agent_id(agent_id)
    _reset_client()

    return _format({
        "status": "saved",
        "agent_id": agent_id,
        "public_key": identity["public_key"],
        "message": (
            "Agent ID saved. The operator must still APPROVE this agent and "
            "issue TRADE capabilities before it can trade. "
            "Use my_status to check approval state."
        ),
    })


# --- Market Tools ---


@mcp.tool()
async def list_markets(status: str | None = None) -> str:
    """List prediction markets on Sentinel, optionally filtered by status.

    Args:
        status: Optional filter — PROPOSED, ACTIVE, HALTED, or RESOLVED.
                Omit to list all markets.

    Returns:
        Paginated list of markets with id, title, prices, status, and close time.
    """
    async def call():
        client = _get_client()
        params = {"size": "50"}
        if status:
            params["status"] = status
        return await client.get("markets", params=params)

    return await _safe_call(call)


@mcp.tool()
async def get_market(market_id: str) -> str:
    """Get full details of a specific market including prices and recent trades.

    Args:
        market_id: UUID of the market.

    Returns:
        Market detail: title, description, resolution criteria, current YES/NO
        prices, total shares, recent trades, and status.
    """
    async def call():
        client = _get_client()
        return await client.get(f"markets/{market_id}")

    return await _safe_call(call)


@mcp.tool()
async def get_market_trades(market_id: str) -> str:
    """Get recent trades on a market to see what other agents are doing.

    Use this to gauge market activity, volume, and price movement direction
    before deciding whether to trade.

    Args:
        market_id: UUID of the market.

    Returns:
        Paginated list of recent trades with agent, direction, shares, and price impact.
    """
    async def call():
        client = _get_client()
        return await client.get(f"markets/{market_id}/trades", params={"size": "20"})

    return await _safe_call(call)


# --- Trading Tools ---


@mcp.tool()
async def place_trade(market_id: str, direction: str, shares: float) -> str:
    """Buy YES or NO shares in a prediction market.

    This runs through the 7-check policy gate:
    0. Kill switch check
    1. Agent status (must be ACTIVE)
    2. Capability check (must have TRADE for this market)
    3. Rate limit check
    4. Position limit check
    5. Trade size check
    6. Market state check (must be ACTIVE)

    If any check fails, the trade is denied and the reason is returned.
    Denied trades are still logged in the audit trail.

    Args:
        market_id: UUID of the market to trade in.
        direction: BUY_YES or BUY_NO.
        shares: Number of shares to buy (must be >= 0.01).

    Returns:
        On success: trade ID, cost, new prices, updated position.
        On denial: error with policy gate reason code.
    """
    if shares < 0.01:
        return _format({
            "error": "validation_error",
            "message": "Shares must be at least 0.01",
        })

    async def call():
        client = _get_client()
        body = {
            "market_id": market_id,
            "direction": direction,
            "shares": shares,
        }
        return await client.post("trade", body)

    return await _safe_call(call)


@mcp.tool()
async def get_position(market_id: str) -> str:
    """Check your current position (YES/NO shares held) in a specific market.

    Args:
        market_id: UUID of the market.

    Returns:
        Position: sharesYes, sharesNo counts. Empty position if no trades yet.
    """
    async def call():
        client = _get_client()
        return await client.get(f"markets/{market_id}/position")

    return await _safe_call(call)


# --- Status Tools ---


@mcp.tool()
async def my_status() -> str:
    """Check your agent profile, status, risk tier, and capabilities.

    Returns:
        Agent details (name, status, risk tier) and list of capabilities
        with their limits (max trade size, position size, rate limit, expiration).
    """
    async def call():
        client = _get_client()
        identity = _get_identity()
        agent_id = identity["agent_id"]
        agent = await client.get(f"agents/{agent_id}")
        capabilities = await client.get(f"agents/{agent_id}/capabilities")
        return {"agent": agent, "capabilities": capabilities}

    return await _safe_call(call)


@mcp.tool()
async def kill_switch_status() -> str:
    """Check if the Sentinel platform is halted (kill switch active).

    Agents should check this before attempting to trade. When the kill switch
    is active, all trades are denied with reason KILL_SWITCH_ACTIVE.

    Returns:
        Kill switch state: active/inactive, reason if active, timestamps.
    """
    async def call():
        async with httpx.AsyncClient(timeout=15.0) as http:
            resp = await http.get(f"{_get_api_url()}/kill-switch/status")
            resp.raise_for_status()
            return resp.json()

    return await _safe_call(call)


# --- Entry point ---


def _cleanup() -> None:
    """Best-effort cleanup: close httpx client on process exit."""
    if _client is not None:
        try:
            # Use asyncio.run() instead of deprecated get_event_loop() (Python 3.10+)
            asyncio.run(_close_client())
        except RuntimeError:
            # Event loop may already be running or closed — skip gracefully
            pass


def main() -> None:
    """Run the MCP server via stdio transport."""
    atexit.register(_cleanup)

    # Eagerly load/create identity on startup so keys are ready
    identity = _get_identity()
    if identity.get("agent_id"):
        logger.info("Starting Sentinel MCP Server (agent_id=%s)", identity["agent_id"])
    else:
        logger.info(
            "Starting Sentinel MCP Server (UNREGISTERED — public_key=%s...). "
            "Call whoami to get the public key for operator registration.",
            identity["public_key"][:16],
        )
    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()
