from __future__ import annotations

import json
from dataclasses import dataclass
from difflib import SequenceMatcher
from functools import lru_cache
from importlib import resources
from typing import Any, Iterable, cast
from urllib.parse import quote

import yaml


@dataclass(frozen=True)
class Parameter:
    name: str
    location: str
    required: bool
    schema: dict[str, Any] | None


@dataclass(frozen=True)
class Operation:
    operation_id: str
    method: str
    path: str
    summary: str | None
    description: str | None
    parameters: tuple[Parameter, ...]
    request_body: dict[str, Any] | None
    responses: dict[str, dict[str, Any]] | None
    deprecated: bool
    namespace: str
    protocol: str
    safety_validation_required: bool


@dataclass(frozen=True)
class Registry:
    operations: dict[str, Operation]


HTTP_METHODS = {"get", "post", "put", "delete", "patch", "head", "options"}
METHOD_OVERRIDES = {
    "exchanges_publish.update_by_params": "post",
}


def _resolve_ref(spec: dict[str, Any], ref: str) -> dict[str, Any]:
    if not ref.startswith("#/"):
        return {"$ref": ref}
    current: Any = spec
    for part in ref.lstrip("#/").split("/"):
        if not isinstance(current, dict) or part not in current:
            return {"$ref": ref}
        current = current[part]
    if not isinstance(current, dict):
        return {"$ref": ref}
    return dict(current)


def _resolve_parameter(spec: dict[str, Any], raw: dict[str, Any]) -> dict[str, Any]:
    if "$ref" in raw:
        return _resolve_ref(spec, raw["$ref"])
    return raw


def _resolve_schema(spec: dict[str, Any], schema: dict[str, Any]) -> dict[str, Any]:
    if "$ref" in schema:
        return _resolve_ref(spec, schema["$ref"])
    return schema


def _resolve_request_body(spec: dict[str, Any], raw: dict[str, Any]) -> dict[str, Any]:
    if "$ref" in raw:
        return _resolve_ref(spec, raw["$ref"])
    content = raw.get("content")
    if isinstance(content, dict) and "application/json" in content:
        schema = content.get("application/json", {}).get("schema")
        if isinstance(schema, dict):
            raw = dict(raw)
            raw["content"] = dict(content)
            raw["content"]["application/json"] = dict(content["application/json"])
            raw["content"]["application/json"]["schema"] = _resolve_schema(
                spec, schema
            )
    return raw


def _resolve_responses(
    spec: dict[str, Any], responses: dict[str, Any]
) -> dict[str, dict[str, Any]]:
    resolved: dict[str, dict[str, Any]] = {}
    for status, response in responses.items():
        if not isinstance(response, dict):
            continue
        content = response.get("content")
        if not isinstance(content, dict):
            continue
        app_json = content.get("application/json")
        if not isinstance(app_json, dict):
            continue
        schema = app_json.get("schema")
        if not isinstance(schema, dict):
            continue
        resolved[status] = _resolve_schema(spec, schema)
    return resolved


def _iter_operations(spec: dict[str, Any]) -> Iterable[Operation]:
    paths: dict[str, Any] = spec.get("paths", {})
    for path, item in paths.items():
        if not isinstance(item, dict):
            continue
        path_params = [_resolve_parameter(spec, p) for p in item.get("parameters", [])]
        for method, operation in item.items():
            if method not in HTTP_METHODS or not isinstance(operation, dict):
                continue
            operation_id = operation.get("operationId")
            if not operation_id:
                continue
            op_params = [_resolve_parameter(spec, p) for p in operation.get("parameters", [])]
            parameters = path_params + op_params
            resolved_params: list[Parameter] = []
            for param in parameters:
                resolved_params.append(
                    Parameter(
                        name=str(param.get("name", "")),
                        location=str(param.get("in", "")),
                        required=bool(param.get("required", False)),
                        schema=param.get("schema"),
                    )
                )
            request_body = operation.get("requestBody")
            if request_body:
                request_body = _resolve_request_body(spec, request_body)
            responses = _resolve_responses(spec, operation.get("responses", {}))
            normalized_method = METHOD_OVERRIDES.get(str(operation_id))
            if normalized_method is None:
                normalized_method = method
            yield Operation(
                operation_id=str(operation_id),
                method=normalized_method,
                path=path,
                summary=operation.get("summary"),
                description=operation.get("description"),
                parameters=tuple(resolved_params),
                request_body=request_body,
                responses=responses or None,
                deprecated=bool(operation.get("deprecated", False)),
                namespace=str(operation_id).split(".", 1)[0]
                if "." in str(operation_id)
                else str(operation_id),
                protocol="http",
                safety_validation_required=normalized_method == "delete",
            )


def _load_spec() -> dict[str, Any]:
    data_file = resources.files("rabbitmq_mcp.data").joinpath(
        "rabbitmq-http-api-openapi.yaml"
    )
    return cast(dict[str, Any], yaml.safe_load(data_file.read_text(encoding="utf-8")))


def _load_operations_json() -> dict[str, Any] | None:
    try:
        data_file = resources.files("rabbitmq_mcp.data").joinpath("operations.json")
        if not data_file.is_file():
            return None
        return cast(dict[str, Any], json.loads(data_file.read_text(encoding="utf-8")))
    except Exception:
        return None


@lru_cache
def get_registry() -> Registry:
    json_registry = _load_operations_json()
    if json_registry is not None:
        operations_payload = json_registry.get("operations", {})
        operations: dict[str, Operation] = {}
        for operation_id, raw in operations_payload.items():
            parameters = tuple(
                Parameter(
                    name=str(param.get("name", "")),
                    location=str(param.get("in", "")),
                    required=bool(param.get("required", False)),
                    schema=cast(dict[str, Any] | None, param.get("schema")),
                )
                for param in raw.get("parameters", [])
            )
            operations[operation_id] = Operation(
                operation_id=operation_id,
                method=str(raw.get("method", "")),
                path=str(raw.get("path", "")),
                summary=raw.get("summary"),
                description=raw.get("description"),
                parameters=parameters,
                request_body=cast(dict[str, Any] | None, raw.get("request_body")),
                responses=cast(dict[str, dict[str, Any]] | None, raw.get("responses")),
                deprecated=bool(raw.get("deprecated", False)),
                namespace=str(raw.get("namespace", operation_id.split(".", 1)[0])),
                protocol=str(raw.get("protocol", "http")),
                safety_validation_required=bool(raw.get("safety_validation_required", False)),
            )
        return Registry(operations=operations)
    spec = _load_spec()
    operations = {op.operation_id: op for op in _iter_operations(spec)}
    return Registry(operations=operations)


def get_operation(operation_id: str) -> Operation:
    registry = get_registry()
    if operation_id not in registry.operations:
        raise KeyError(f"Unknown operation_id: {operation_id}")
    return registry.operations[operation_id]


def build_path(
    operation: Operation, path_params: dict[str, Any] | None
) -> str:
    params = path_params or {}
    result = operation.path
    for param in operation.parameters:
        if param.location != "path":
            continue
        placeholder = f"{{{param.name}}}"
        if placeholder not in result:
            continue
        if param.name not in params:
            raise KeyError(f"Missing path parameter: {param.name}")
        value = quote(str(params[param.name]), safe="")
        result = result.replace(placeholder, value)
    return result


def build_request(
    operation: Operation,
    path_params: dict[str, Any] | None,
    query_params: dict[str, Any] | None,
) -> tuple[str, dict[str, Any]]:
    path = build_path(operation, path_params)
    return path, dict(query_params or {})


def search_operations(query: str, limit: int = 10) -> list[dict[str, Any]]:
    normalized = query.strip().lower()
    registry = get_registry()
    if not normalized:
        return [
            {
                "operation_id": op.operation_id,
                "score": 0.0,
                "method": op.method.upper(),
                "path": op.path,
                "summary": op.summary,
                "deprecated": op.deprecated,
                "protocol": op.protocol,
                "namespace": op.namespace,
            }
            for op in sorted(registry.operations.values(), key=lambda item: item.operation_id)
        ][:limit]
    results: list[tuple[float, Operation]] = []
    for op in registry.operations.values():
        haystack = " ".join(
            part for part in [op.operation_id, op.summary, op.description] if part
        ).lower()
        if not haystack:
            continue
        if normalized in op.operation_id.lower():
            score = 1.0
        elif normalized in haystack:
            score = 0.8
        else:
            score = SequenceMatcher(None, normalized, haystack).ratio()
        results.append((score, op))
    results.sort(key=lambda item: item[0], reverse=True)
    return [
        {
            "operation_id": op.operation_id,
            "score": round(score, 4),
            "method": op.method.upper(),
            "path": op.path,
            "summary": op.summary,
            "deprecated": op.deprecated,
            "protocol": op.protocol,
            "namespace": op.namespace,
        }
        for score, op in results[:limit]
    ]


def operation_to_dict(operation: Operation) -> dict[str, Any]:
    return {
        "operation_id": operation.operation_id,
        "method": operation.method.upper(),
        "path": operation.path,
        "summary": operation.summary,
        "description": operation.description,
        "deprecated": operation.deprecated,
        "protocol": operation.protocol,
        "namespace": operation.namespace,
        "safety_validation_required": operation.safety_validation_required,
        "parameters": [
            {
                "name": param.name,
                "in": param.location,
                "required": param.required,
                "schema": param.schema,
            }
            for param in operation.parameters
        ],
        "request_body": operation.request_body,
    }


def _schema_for_params(params: list[Parameter]) -> dict[str, Any]:
    properties: dict[str, Any] = {}
    required: list[str] = []
    for param in params:
        properties[param.name] = param.schema or {"type": "string"}
        if param.required:
            required.append(param.name)
    schema: dict[str, Any] = {"type": "object", "properties": properties}
    if required:
        schema["required"] = required
    return schema


def operation_schema(operation: Operation) -> dict[str, Any]:
    path_params = [param for param in operation.parameters if param.location == "path"]
    query_params = [param for param in operation.parameters if param.location == "query"]
    body_schema: dict[str, Any] | None = None
    if operation.request_body:
        content = operation.request_body.get("content", {})
        json_body = content.get("application/json") if isinstance(content, dict) else None
        if isinstance(json_body, dict) and "schema" in json_body:
            body_schema = cast(dict[str, Any], json_body.get("schema"))

    input_schema: dict[str, Any] = {
        "type": "object",
        "properties": {
            "path_params": _schema_for_params(path_params),
            "query_params": _schema_for_params(query_params),
        },
    }
    if body_schema is not None:
        input_schema["properties"]["body"] = body_schema
    output_schema: dict[str, Any] = {"type": "object"}
    if operation.responses:
        for status in ("200", "201", "202"):
            if status in operation.responses:
                output_schema = operation.responses[status]
                break
        else:
            for status, schema in operation.responses.items():
                if status.startswith("2"):
                    output_schema = schema
                    break
    return {
        "operation_id": operation.operation_id,
        "name": operation.operation_id,
        "description": operation.description or operation.summary or "",
        "category": operation.namespace,
        "async": True,
        "input_schema": input_schema,
        "output_schema": output_schema,
    }
