from __future__ import annotations

from collections.abc import Awaitable, Callable
from typing import Any, TypeVar

from fastmcp import FastMCP

from .amqp import amqp_ack, amqp_consume, amqp_nack, amqp_publish, amqp_reject
from .cli import parse_cli
from .client import RabbitMQClient, RabbitMQError
from .config import RabbitMQConfig
from .logging_utils import get_logger, log_event
from .mcp_errors import MCPToolError
from .mcp_results import error as error_result
from .mcp_results import ok
from .pagination import normalize_pagination
from .registry import build_request, get_operation, operation_schema
from .safety import enforce_safety
from .semantic import semantic_search
from .validation import validate_call, validate_response

T = TypeVar("T")

mcp = FastMCP("RabbitMQ MCP")
LOGGER = get_logger("rabbitmq_mcp")
_CONFIG: RabbitMQConfig | None = None


def _set_config(config: RabbitMQConfig) -> None:
    global _CONFIG
    _CONFIG = config


def _get_config() -> RabbitMQConfig:
    return _CONFIG or RabbitMQConfig.from_env()


async def _with_client(action: Callable[[RabbitMQClient], Awaitable[T]]) -> T:
    config = _get_config()
    async with RabbitMQClient(config) as client:
        return await action(client)


@mcp.tool()
async def system_overview() -> dict[str, Any]:
    """Get RabbitMQ management overview."""

    async def _call(client: RabbitMQClient) -> dict[str, Any]:
        return await client.overview()

    return await _with_client(_call)


@mcp.tool()
async def queues_list(vhost: str | None = None) -> list[dict[str, Any]]:
    """List queues, optionally filtered by vhost."""

    async def _call(client: RabbitMQClient) -> list[dict[str, Any]]:
        return await client.list_queues(vhost=vhost)

    return await _with_client(_call)


@mcp.tool()
async def queues_get(vhost: str, name: str) -> dict[str, Any]:
    """Get details for a single queue."""

    async def _call(client: RabbitMQClient) -> dict[str, Any]:
        return await client.get_queue(vhost=vhost, name=name)

    return await _with_client(_call)


@mcp.tool()
async def exchanges_list(vhost: str | None = None) -> list[dict[str, Any]]:
    """List exchanges, optionally filtered by vhost."""

    async def _call(client: RabbitMQClient) -> list[dict[str, Any]]:
        return await client.list_exchanges(vhost=vhost)

    return await _with_client(_call)


@mcp.tool()
async def bindings_list(vhost: str | None = None) -> list[dict[str, Any]]:
    """List bindings, optionally filtered by vhost."""

    async def _call(client: RabbitMQClient) -> list[dict[str, Any]]:
        return await client.list_bindings(vhost=vhost)

    return await _with_client(_call)


@mcp.tool()
async def vhosts_list() -> list[dict[str, Any]]:
    """List vhosts."""

    async def _call(client: RabbitMQClient) -> list[dict[str, Any]]:
        return await client.list_vhosts()

    return await _with_client(_call)


@mcp.tool()
async def users_list() -> list[dict[str, Any]]:
    """List users."""

    async def _call(client: RabbitMQClient) -> list[dict[str, Any]]:
        return await client.list_users()

    return await _with_client(_call)


@mcp.tool()
async def exchange_publish(
    vhost: str,
    exchange: str,
    routing_key: str,
    payload: str,
    payload_encoding: str = "string",
    properties: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Publish a message to an exchange via the management API."""

    async def _call(client: RabbitMQClient) -> dict[str, Any]:
        return await client.publish(
            vhost=vhost,
            exchange=exchange,
            routing_key=routing_key,
            payload=payload,
            payload_encoding=payload_encoding,
            properties=properties,
        )

    return await _with_client(_call)


def main() -> None:
    config = parse_cli()
    _set_config(config)
    mcp.run()


@mcp.tool()
async def search_ids(
    query: str, page: int = 1, page_size: int = 50
) -> dict[str, Any]:
    """Search available operation IDs by keyword."""

    try:
        if page < 1 or page_size < 1:
            raise MCPToolError(
                code="INVALID_PAGINATION",
                message="page and page_size must be positive",
            )
        results = semantic_search(query=query, limit=None)
        total = len(results)
        start = (page - 1) * page_size
        end = start + page_size
        items: list[dict[str, Any]] = []
        for entry in results[start:end]:
            op = get_operation(entry["operation_id"])
            items.append(
                {
                    "operation_id": op.operation_id,
                    "score": entry.get("score", 0.0),
                    "metadata": {
                        "method": op.method.upper(),
                        "path": op.path,
                        "protocol": op.protocol,
                        "namespace": op.namespace,
                        "deprecated": op.deprecated,
                    },
                }
            )
        return ok(
            {
                "items": items,
                "pagination": {"page": page, "page_size": page_size, "total": total},
            }
        )
    except MCPToolError as exc:
        log_event(LOGGER, "search_ids.error", code=exc.code, message=exc.message)
        return error_result(exc.code, exc.message, exc.details)
    except Exception as exc:  # pragma: no cover - safety net
        log_event(LOGGER, "search_ids.error", code="INTERNAL_ERROR", error=str(exc))
        return error_result("INTERNAL_ERROR", "Unexpected error", {"error": str(exc)})


@mcp.tool()
async def get_id(operation_id: str) -> dict[str, Any]:
    """Get metadata for a specific operation ID."""

    try:
        operation = get_operation(operation_id)
        return ok(operation_schema(operation))
    except KeyError:
        log_event(LOGGER, "get_id.error", code="OPERATION_NOT_FOUND")
        return error_result("OPERATION_NOT_FOUND", "Operation ID not found")
    except Exception as exc:  # pragma: no cover - safety net
        log_event(LOGGER, "get_id.error", code="INTERNAL_ERROR", error=str(exc))
        return error_result("INTERNAL_ERROR", "Unexpected error", {"error": str(exc)})


@mcp.tool()
async def call_id(
    operation_id: str,
    path_params: dict[str, Any] | None = None,
    query_params: dict[str, Any] | None = None,
    body: dict[str, Any] | None = None,
    pagination: dict[str, Any] | None = None,
    allow_destructive: bool | None = None,
) -> Any:
    """Call any HTTP API operation by operation ID."""

    try:
        operation = get_operation(operation_id)
        config = _get_config()
        effective_allow = config.allow_destructive if allow_destructive is None else allow_destructive
        merged_query = dict(query_params or {})
        merged_query.update(normalize_pagination(pagination))
        validate_call(operation, path_params, merged_query, body)
        if operation.protocol == "amqp":
            if body is None:
                raise MCPToolError(
                    code="MISSING_BODY",
                    message="AMQP operation requires body parameters",
                )
            if operation.operation_id == "amqp.publish":
                result = await amqp_publish(
                    config,
                    exchange=str(body.get("exchange", "")),
                    routing_key=str(body.get("routing_key", "")),
                    payload=str(body.get("payload", "")),
                    payload_encoding=str(body.get("payload_encoding", "string")),
                    properties=body.get("properties"),
                )
            elif operation.operation_id == "amqp.consume":
                result = await amqp_consume(
                    config,
                    queue=str(body.get("queue", "")),
                    max_messages=int(body.get("max_messages", 1)),
                    auto_ack=bool(body.get("auto_ack", True)),
                )
            elif operation.operation_id == "amqp.ack":
                result = await amqp_ack(
                    config,
                    receipt_handle=str(body.get("receipt_handle", "")),
                )
            elif operation.operation_id == "amqp.nack":
                result = await amqp_nack(
                    config,
                    receipt_handle=str(body.get("receipt_handle", "")),
                    requeue=bool(body.get("requeue", True)),
                )
            elif operation.operation_id == "amqp.reject":
                result = await amqp_reject(
                    config,
                    receipt_handle=str(body.get("receipt_handle", "")),
                    requeue=bool(body.get("requeue", False)),
                )
            else:
                raise MCPToolError(
                    code="UNSUPPORTED_AMQP",
                    message="Unsupported AMQP operation",
                )
            return ok(result)

        path, query = build_request(operation, path_params, merged_query)

        async def _call(client: RabbitMQClient) -> Any:
            await enforce_safety(operation, client, path_params, effective_allow)
            payload, status_code = await client.request(
                operation.method, path, params=query, json_body=body, return_meta=True
            )
            validate_response(operation, status_code, payload)
            return payload

        return ok(await _with_client(_call))
    except MCPToolError as exc:
        log_event(LOGGER, "call_id.error", code=exc.code, message=exc.message)
        return error_result(exc.code, exc.message, exc.details)
    except RabbitMQError as exc:
        log_event(LOGGER, "call_id.error", code="API_ERROR", error=str(exc))
        return error_result("API_ERROR", "RabbitMQ API request failed", {"error": str(exc)})
    except KeyError:
        log_event(LOGGER, "call_id.error", code="OPERATION_NOT_FOUND")
        return error_result("OPERATION_NOT_FOUND", "Operation ID not found")
    except Exception as exc:  # pragma: no cover - safety net
        log_event(LOGGER, "call_id.error", code="INTERNAL_ERROR", error=str(exc))
        return error_result("INTERNAL_ERROR", "Unexpected error", {"error": str(exc)})


__all__ = [
    "mcp",
    "main",
    "search_ids",
    "get_id",
    "call_id",
]
