"""MCP API — manage MCP server and external connections."""

from __future__ import annotations

from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel

router = APIRouter(prefix="/mcp", tags=["mcp"])


class MCPServerConfig(BaseModel):
    name: str
    command: str
    args: list[str] = []
    env: dict[str, str] = {}
    auto_connect: bool = True


@router.get("/status")
async def mcp_status(request: Request) -> dict:
    """Get MCP server status and connected external servers."""
    mcp_client = request.app.state.mcp_client
    return {
        "server": "running",
        "external_servers": mcp_client.list_servers(),
        "external_tools": len(mcp_client.get_external_tool_definitions()),
    }


@router.post("/servers/add")
async def add_server(request: Request, body: MCPServerConfig) -> dict:
    """Register an external MCP server."""
    from agntspace.mcp.client import ExternalMCPServer

    mcp_client = request.app.state.mcp_client
    mcp_client.register_server(ExternalMCPServer(
        name=body.name, command=body.command,
        args=body.args, env=body.env,
        auto_connect=body.auto_connect,
    ))
    return {"registered": True, "name": body.name}


@router.post("/servers/{name}/connect")
async def connect_server(request: Request, name: str) -> dict:
    """Connect to an external MCP server."""
    mcp_client = request.app.state.mcp_client
    try:
        tools = await mcp_client.connect_server(name)
        return {"connected": True, "tools": len(tools), "tool_names": [t["name"] for t in tools]}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e)) from e


@router.post("/servers/{name}/disconnect")
async def disconnect_server(request: Request, name: str) -> dict:
    """Disconnect from an external MCP server."""
    mcp_client = request.app.state.mcp_client
    ok = await mcp_client.disconnect_server(name)
    return {"disconnected": ok, "name": name}


@router.delete("/servers/{name}")
async def remove_server(request: Request, name: str) -> dict:
    """Remove an external MCP server configuration."""
    mcp_client = request.app.state.mcp_client
    ok = mcp_client.remove_server(name)
    if not ok:
        raise HTTPException(status_code=404, detail=f"Server '{name}' not found")
    return {"removed": True, "name": name}


@router.post("/servers/{name}/reset-circuit")
async def reset_circuit(request: Request, name: str) -> dict:
    """Manually clear the circuit breaker for a server.

    Layer 4 — when the circuit is OPEN, ``connect_server`` rejects
    attempts immediately to avoid stampeding a known-broken upstream.
    The "Reset & retry" button in Settings → MCP Servers calls this
    endpoint to clear the failure counter and let the user force a
    fresh attempt without waiting for the auto-recovery window.
    Idempotent — safe to call when the circuit isn't actually open.
    """
    mcp_client = request.app.state.mcp_client
    if name not in mcp_client._servers:
        raise HTTPException(status_code=404, detail=f"Server '{name}' not found")
    state = mcp_client.reset_circuit(name)
    return {"reset": True, "name": name, "state": state}


@router.get("/tools")
async def list_external_tools(request: Request) -> list[dict]:
    """List tools from all connected external MCP servers."""
    mcp_client = request.app.state.mcp_client
    return [
        {"name": t["name"], "description": t.get("description", ""), "server": t.get("_mcp_server", "")}
        for t in mcp_client.get_external_tool_definitions()
    ]


# ───────────────────────────────────────────────────────────
# §9.4 Review-before-connect flow
# ───────────────────────────────────────────────────────────


@router.get("/servers/{name}/review")
async def get_server_review(request: Request, name: str) -> dict:
    """Return pending tools for a server, with hashes + injection flags.

    Pure read surface for the approval UI. Does NOT trigger a connect —
    the server must already be connected (`POST /servers/{name}/connect`)
    so its tools have been fetched into the pending queue.
    """
    mcp_client = request.app.state.mcp_client
    try:
        return mcp_client.get_pending_review(name)
    except ValueError as exc:
        raise HTTPException(status_code=404, detail=f"Server '{name}' not found") from exc


class _ReviewRejectBody(BaseModel):
    reason: str = ""


@router.post("/servers/{name}/review/approve")
async def approve_server(request: Request, name: str) -> dict:
    """Approve all pending tools for a server — they become visible to the agent.

    Records a high-importance user activity event (classified by the
    activity middleware as `mcp_server_reviewed`). The caller is trusted
    to have seen the tool list first; this endpoint does not re-render it.
    """
    mcp_client = request.app.state.mcp_client
    try:
        result = mcp_client.approve_server(name)
    except ValueError as exc:
        raise HTTPException(status_code=404, detail=f"Server '{name}' not found") from exc
    return {"approved": True, "name": name, **result}


@router.post("/servers/{name}/review/reject")
async def reject_server(request: Request, name: str, body: _ReviewRejectBody) -> dict:
    """Reject a server — marks it rejected + disconnects + clears pending tools.

    The server stays registered (so the user can see what they rejected) but
    will refuse to connect until explicitly removed + re-registered. Logged
    as `user_override` at high importance.
    """
    mcp_client = request.app.state.mcp_client
    try:
        result = await mcp_client.reject_server(name, reason=body.reason)
    except ValueError as exc:
        raise HTTPException(status_code=404, detail=f"Server '{name}' not found") from exc
    return {"name": name, **result}


# ───────────────────────────────────────────────────────────
# Tokens — bearer auth for the SSE transport (remote access)
# ───────────────────────────────────────────────────────────


class _MintTokenBody(BaseModel):
    name: str = ""
    # ``scope`` is user input — the store normalizes unknown values to
    # ``full``, but we declare a Literal so the OpenAPI schema shows the
    # allowed values and the UI can render proper controls.
    scope: str = "full"


@router.get("/tokens")
async def list_mcp_tokens(request: Request) -> list[dict]:
    """List MCP bearer tokens (no secrets, never raw).

    Returned fields: id, name, scope, created_at, last_used_at, revoked,
    revoked_at, prefix (``agnt_mcp_<id>...`` for recognition).
    """
    store = getattr(request.app.state, "mcp_tokens", None)
    if store is None:
        raise HTTPException(status_code=503, detail="Token store not initialized")
    return await store.list()


@router.post("/tokens")
async def mint_mcp_token(request: Request, body: _MintTokenBody) -> dict:
    """Mint a new MCP bearer token.

    The raw token is returned EXACTLY ONCE in this response — the server
    stores only a sha256 hash. If the user loses the raw value, they must
    revoke and mint a new one.

    ``scope`` can be ``full`` (default — every tool the contract allows)
    or ``read_only`` (pure-read tools only: memory recall, graph queries,
    vault search, list endpoints). Unknown values coerce to ``full``.
    """
    store = getattr(request.app.state, "mcp_tokens", None)
    if store is None:
        raise HTTPException(status_code=503, detail="Token store not initialized")
    minted = await store.create(name=body.name or "", scope=body.scope or "full")
    return {
        "id": minted.token_id,
        "name": minted.name,
        "scope": minted.scope,
        "created_at": minted.created_at,
        "raw_token": minted.raw_token,
        "note": "Save this token now — it is NOT retrievable later.",
    }


@router.delete("/tokens/{token_id}")
async def revoke_mcp_token(request: Request, token_id: str) -> dict:
    """Revoke a token by id. Idempotent — revoking already-revoked tokens succeeds silently.

    Force-closes any live SSE sessions holding this token. The closed
    count is returned in ``sessions_closed`` so the UI can confirm the
    revoke had real effect, not just a paper one.
    """
    store = getattr(request.app.state, "mcp_tokens", None)
    if store is None:
        raise HTTPException(status_code=503, detail="Token store not initialized")
    changed = await store.revoke(token_id)
    # Even if changed=False (already revoked), still try to close active
    # sessions — they could be lingering from before a previous revoke
    # attempt failed to reach the tracker.
    sessions_closed = 0
    tracker = getattr(request.app.state, "mcp_sse_tracker", None)
    if tracker is not None:
        sessions_closed = await tracker.revoke(token_id)
    return {"revoked": changed, "id": token_id, "sessions_closed": sessions_closed}


@router.get("/sessions")
async def list_mcp_sessions(request: Request) -> dict:
    """Live SSE sessions, keyed by token id.

    Returns ``{active, by_token: {token_id: {count, opened_at: [...]}}}``
    for every token with at least one active stream. Used by the
    Settings UI to show a "currently streaming" indicator next to each
    token row, and by operators to confirm revocation actually killed
    connections.

    Empty when no SSE sessions are live (or when the tracker isn't
    wired — setup mode).
    """
    tracker = getattr(request.app.state, "mcp_sse_tracker", None)
    if tracker is None:
        return {"active": 0, "by_token": {}}
    return {"active": tracker.active_count(), "by_token": tracker.snapshot()}
