"""UseCase MCP Server — four-layer progressive disclosure for use case methods.

Creates an independent FastMCP server that exposes UseCaseService methods
to AI agents via progressive disclosure:
- list_apps: discover available applications
- list_services: list services in an app
- describe_service: get method signatures for a service
- call_use_case: execute a specific method
"""

from __future__ import annotations

import inspect
import json
from typing import TYPE_CHECKING, Annotated, Any, get_args, get_origin, get_type_hints

from pydantic import BaseModel, TypeAdapter

from nexusx.mcp.types.errors import (
    MCPErrors,
    create_error_response,
    create_success_response,
)
from nexusx.use_case.business import USE_CASE_METHODS_ATTR, get_return_type
from nexusx.use_case.context import FromContext
from nexusx.use_case.manager import UseCaseManager
from nexusx.use_case.selection import SelectionError, apply_selection
from nexusx.use_case.types import UseCaseAppConfig

if TYPE_CHECKING:
    from fastmcp import FastMCP

    from nexusx.use_case.manager import UseCaseResources

try:
    from fastmcp.server.context import Context
except ImportError:
    Context = None  # type: ignore[assignment, misc]


def create_use_case_mcp_server(
    apps: list[UseCaseAppConfig],
    name: str = "SQLModel-Nexus UseCase API",
) -> FastMCP:
    """Create an MCP server that exposes UseCase services as tools.

    Args:
        apps: List of UseCaseAppConfig instances, each containing a group
            of UseCaseService subclasses.
        name: Name of the MCP server (shown in MCP clients).

    Returns:
        A configured FastMCP server instance.

    Example::

        mcp = create_use_case_mcp_server(
            apps=[
                UseCaseAppConfig(
                    name="project",
                    services=[SprintService, TaskService],
                ),
            ],
            name="Project UseCase API",
        )
        mcp.run()
    """
    from fastmcp import FastMCP

    if not apps:
        raise ValueError("apps list cannot be empty")

    manager = UseCaseManager(apps)
    mcp = FastMCP(name)

    # ──────────────────────────────────────────────────
    # Resource: usage manual
    # ──────────────────────────────────────────────────

    @mcp.resource("nexusx://manual")
    def usage_manual() -> str:
        """nexusx UseCase API 使用说明书。

        首次连接时请先读取此资源，了解整体能力结构和推荐调用顺序。
        包含完整的四层渐进式工具使用指南。
        """
        app_names = list(manager.apps.keys())
        app_lines = "\n".join(f"  - `{a}`: {manager.apps[a].description or 'No description'}"
                              for a in app_names)
        return f"""# nexusx UseCase API — AI 使用指南

## 能力结构

本 MCP 服务器以**四层渐进式披露**组织工具，避免一次性暴露所有方法：

| 层级 | 工具 | 用途 |
|------|------|------|
| 0 | `list_apps()` | 发现可用应用 |
| 1 | `list_services(app_name)` | 查看某应用的服务 |
| 2 | `describe_service(app_name, service_name)` | 查看方法签名 + SDL 类型定义 |
| 3 | `call_use_case(...)` | 执行具体方法 |

## 推荐调用流程

```
list_apps()
  → list_services(app_name="xxx")
    → describe_service(app_name="xxx", service_name="yyy")
      → call_use_case(app_name="xxx", service_name="yyy", method_name="zzz", params='{{...}}')
```

## 可用应用

{app_lines}

## 注意事项

- `describe_service` 返回的 `types` 字段包含 SDL 格式的数据模型定义，
  在调用 `call_use_case` 前阅读它有助于理解返回结构
- Mutation 默认开启，可通过 `UseCaseAppConfig(enable_mutation=False)` 禁用
- `call_use_case` 的 `params` 参数接受 JSON 字符串

## 错误处理

所有工具返回统一格式：{{"success": bool, "data": ..., "hint": "..."}}
错误时返回：{{"success": false, "error": "...", "error_code": "..."}}
"""

    # Layer 0: Application discovery
    @mcp.tool()
    def list_apps() -> dict[str, Any]:
        """List all available UseCase applications.

        Returns a list of all configured applications with their metadata:
        - name: Application name
        - description: Application description
        - services_count: Number of services in the app

        IMPORTANT: All subsequent tool calls (except this one) require
        the app_name parameter. Choose an app_name from this list.

        Use this as the first step to discover what APIs are available,
        then use list_services to explore services within an app.

        Returns:
            Dictionary with success status, app list, and usage hints
        """
        try:
            apps_info = []
            for app in manager.apps.values():
                apps_info.append({
                    "name": app.name,
                    "description": app.description,
                    "services_count": len(app.services),
                })

            app_names = [a["name"] for a in apps_info]
            hint = (
                f"IMPORTANT: All subsequent tool calls require app_name parameter. "
                f"First time? Read resource 'nexusx://manual' for the full usage guide. "
                f"Available apps: {app_names}. "
                f"Example: list_services(app_name='{app_names[0] if app_names else 'app_name'}')\n"
                f"After exploring, use call_use_case to execute methods."
            )
            return {
                "success": True,
                "data": apps_info,
                "hint": hint,
            }
        except Exception as e:
            return create_error_response(str(e), MCPErrors.INTERNAL_ERROR)

    # Layer 1: Service listing
    @mcp.tool()
    def list_services(app_name: str) -> dict[str, Any]:
        """List all available UseCase services for an application.

        Returns a lightweight list of service names, descriptions, and
        method counts. Use this after list_apps to discover services,
        then use describe_service to explore a specific service's methods.

        Args:
            app_name: Name of the application (from list_apps).

        Returns:
            Dictionary with service list and usage hints.
        """
        try:
            app = manager.get_app(app_name)
            services_info = app.introspector.list_services()

            # Filter mutation methods from count when disabled
            if not app.enable_mutation:
                for svc in services_info:
                    service_cls = app.services.get(svc["name"])
                    if service_cls:
                        all_methods = getattr(service_cls, USE_CASE_METHODS_ATTR)
                        svc["methods_count"] = sum(
                            1
                            for m in all_methods.values()
                            if isinstance(m, dict) and m.get("kind") != "mutation"
                        )

            service_names = [s["name"] for s in services_info]
            hint = (
                f"Working with app '{app_name}'. "
                f"Use describe_service(app_name='{app_name}', service_name='...') "
                f"to explore methods. Available services: {service_names}."
            )

            return {
                "success": True,
                "data": services_info,
                "hint": hint,
            }
        except ValueError as e:
            return create_error_response(str(e), MCPErrors.APP_NOT_FOUND)
        except Exception as e:
            return create_error_response(str(e), MCPErrors.INTERNAL_ERROR)

    # Layer 2: Method description
    @mcp.tool()
    def describe_service(app_name: str, service_name: str) -> dict[str, Any]:
        """Get detailed method info for a specific UseCase service.

        Returns all methods on the service with their names, descriptions,
        parameter schemas (JSON Schema), and return type schemas.
        Use this after list_services to understand what methods are available,
        then use call_use_case to execute a specific method.

        Args:
            app_name: Name of the application (from list_apps).
            service_name: Name of the service (from list_services).

        Returns:
            Dictionary with success, data (service details with methods and types), and hint.
        """
        try:
            app = manager.get_app(app_name)
            info = app.introspector.describe_service(service_name)
            if info is None:
                return create_error_response(
                    f"Service '{service_name}' not found in app '{app_name}'. "
                    f"Use list_services(app_name='{app_name}') to see available services.",
                    MCPErrors.TYPE_NOT_FOUND,
                )

            method_names = [m["name"] for m in info.get("methods", [])]

            # Filter mutation methods when disabled
            if not app.enable_mutation:
                info["methods"] = [
                    m for m in info.get("methods", []) if m.get("kind") != "mutation"
                ]
                method_names = [m["name"] for m in info["methods"]]

            has_types = bool(info.get("types", "").strip())
            hint_parts = [
                f"Methods: {method_names}.",
            ]
            if has_types:
                hint_parts.append(
                    "Response includes 'types' field with SDL type definitions — "
                    "read it if you need to understand the data model before calling."
                )
            hint_parts.append(
                f"Use call_use_case(app_name='{app_name}', "
                f"service_name='{service_name}', "
                f"method_name='...', params='{{...}}') to execute."
            )
            hint_parts.append(
                "For methods with selection_supported=true, you may also pass "
                "selection='{ id owner { name } }'. Use each method's "
                "selection_example and the top-level selection_usage/types fields "
                "to choose valid fields."
            )
            hint = " ".join(hint_parts)

            result = create_success_response(info)
            result["hint"] = hint
            return result
        except ValueError as e:
            return create_error_response(str(e), MCPErrors.APP_NOT_FOUND)
        except Exception as e:
            return create_error_response(str(e), MCPErrors.INTERNAL_ERROR)

    # Layer 3: Execute use case
    @mcp.tool()
    async def call_use_case(
        app_name: str,
        service_name: str,
        method_name: str,
        params: str = "{}",
        selection: str | None = None,
        ctx: Context = None,  # type: ignore[assignment]
    ) -> dict[str, Any]:
        """Execute a use case method on a specific service.

        Call a method discovered via describe_service. The params argument
        should be a JSON object string matching the method's parameter schema.
        For methods returning Pydantic DTOs, you can optionally pass a
        rootless GraphQL-like selection string such as
        ``{ id title owner { name } }`` to project the response payload.
        Use describe_service first to inspect the returned DTO types.
        selection only changes the response shape; it does not change
        params, loading, pagination, or business execution.

        Args:
            app_name: Name of the application.
            service_name: Name of the service.
            method_name: Name of the method to call.
            params: JSON string with method parameters (default: "{}").
            selection: Optional rootless GraphQL-like field projection for
                Pydantic return values, for example ``{ id title owner { name } }``.
                Use fields from describe_service.types. Nested DTOs require
                sub-selection; scalar/dict/Any fields cannot have sub-selection;
                GraphQL arguments are not supported.

        Returns:
            Dictionary with success, data (method result), and hint.
        """
        # Parse params JSON
        try:
            kwargs = json.loads(params) if params else {}
        except json.JSONDecodeError as e:
            return create_error_response(
                f"Invalid JSON in params: {e}",
                MCPErrors.VALIDATION_ERROR,
            )

        if not isinstance(kwargs, dict):
            return create_error_response(
                "params must be a JSON object (dict), not an array or scalar",
                MCPErrors.VALIDATION_ERROR,
            )

        # Look up app
        try:
            app = manager.get_app(app_name)
        except ValueError:
            return create_error_response(
                f"App '{app_name}' not found. "
                f"Use list_apps() to see available apps.",
                MCPErrors.APP_NOT_FOUND,
            )

        # Look up service
        service_cls = app.services.get(service_name)
        if service_cls is None:
            available = list(app.services.keys())
            return create_error_response(
                f"Service '{service_name}' not found in app '{app_name}'. "
                f"Available services: {available}",
                MCPErrors.TYPE_NOT_FOUND,
            )

        # Look up method
        methods = getattr(service_cls, USE_CASE_METHODS_ATTR)
        if method_name not in methods:
            available = list(methods.keys())
            return create_error_response(
                f"Method '{method_name}' not found in service '{service_name}'. "
                f"Available methods: {available}",
                MCPErrors.TYPE_NOT_FOUND,
            )

        # Check mutation permission
        method_meta = methods.get(method_name, {})
        method_kind = (
            method_meta.get("kind", "query") if isinstance(method_meta, dict) else "query"
        )
        if not app.enable_mutation and method_kind == "mutation":
            return create_error_response(
                f"Method '{method_name}' is a mutation and mutations are disabled "
                f"for app '{app_name}'.",
                MCPErrors.INVALID_OPERATION,
            )

        # Execute
        try:
            method = getattr(service_cls, method_name)

            # Coerce JSON-parsed kwargs to match method parameter types
            func = method.__func__ if isinstance(method, classmethod) else method
            kwargs = _coerce_kwargs(func, kwargs)

            # Extract context and merge FromContext params into kwargs
            context = await _extract_context(app, ctx)
            from_context_params = _get_from_context_params(method)
            if from_context_params:
                if context is None:
                    context = {}
                sig = inspect.signature(method)
                for param_name in from_context_params:
                    if param_name in context:
                        kwargs[param_name] = context[param_name]
                    elif (
                        param_name not in kwargs
                        and sig.parameters[param_name].default
                        is inspect.Parameter.empty
                    ):
                        return create_error_response(
                            f"Required FromContext parameter '{param_name}' "
                            f"not found in context for {service_name}.{method_name}",
                            MCPErrors.VALIDATION_ERROR,
                        )

            result = await method(**kwargs)
        except TypeError as e:
            return create_error_response(
                f"Parameter error calling {service_name}.{method_name}: {e}",
                MCPErrors.VALIDATION_ERROR,
            )
        except Exception as e:
            return create_error_response(
                f"Error executing {service_name}.{method_name}: {e}",
                MCPErrors.QUERY_EXECUTION_ERROR,
            )

        if selection is not None:
            try:
                return_anno = get_return_type(method)
                result = apply_selection(result, return_anno, selection)
            except SelectionError as e:
                return create_error_response(
                    f"Invalid selection for {service_name}.{method_name}: {e}",
                    MCPErrors.VALIDATION_ERROR,
                )

        # Serialize result
        data = _serialize_result(result)

        response = create_success_response(data)
        response["hint"] = (
            f"Executed {app_name}.{service_name}.{method_name}. "
            f"Use describe_service(app_name='{app_name}', service_name='{service_name}') "
            f"to explore more methods."
        )
        return response

    async def _extract_context(
        app: UseCaseResources, ctx: Context
    ) -> dict | None:
        """Call the app's context_extractor if configured, returning a context dict."""
        if app.context_extractor is None or ctx is None:
            return None
        result = app.context_extractor(ctx)
        if inspect.isawaitable(result):
            return await result
        return result

    def _get_from_context_params(method: callable) -> set[str]:
        """Return parameter names annotated with FromContext."""
        from_context_params: set[str] = set()
        try:
            hints = get_type_hints(method, include_extras=True)
        except Exception:
            hints = {}
        sig = inspect.signature(method)
        for name in sig.parameters:
            annotation = hints.get(name)
            if annotation is not None and get_origin(annotation) is Annotated:
                for arg in get_args(annotation):
                    if isinstance(arg, FromContext):
                        from_context_params.add(name)
                        break
        return from_context_params

    return mcp


def _coerce_value(value: Any, annotation: Any) -> Any:
    """Use Pydantic TypeAdapter to coerce a JSON-native value to the target type."""
    if value is None:
        return value
    try:
        adapter = TypeAdapter(annotation)
        return adapter.validate_python(value)
    except Exception:
        return value


def _coerce_kwargs(func: Any, kwargs: dict[str, Any]) -> dict[str, Any]:
    """Coerce JSON-parsed kwargs to match the function's parameter type hints."""
    try:
        hints = get_type_hints(func, include_extras=True)
    except Exception:
        return kwargs

    sig = inspect.signature(func)
    coerced = dict(kwargs)

    for name, param in sig.parameters.items():
        if name == "cls" or name not in coerced:
            continue
        anno = hints.get(name, param.annotation)
        if anno is inspect.Parameter.empty or anno is None:
            continue
        coerced[name] = _coerce_value(coerced[name], anno)

    return coerced


def _serialize_result(result: Any) -> Any:
    """Serialize a method result to a JSON-friendly structure."""
    if result is None:
        return None

    if isinstance(result, BaseModel):
        return result.model_dump()

    if isinstance(result, list):
        return [_serialize_result(item) for item in result]

    if isinstance(result, dict):
        return result

    if isinstance(result, (str, int, float, bool)):
        return result

    # Fallback: try model_dump for any Pydantic-like object
    if hasattr(result, "model_dump"):
        return result.model_dump()

    return result
