"""OpenAPI-to-MCP meta-tools.

These agent-facing tools allow AI agents to:
  1. Import an OpenAPI/Swagger spec (from URL, file, or raw JSON/YAML)
  2. Browse the discovered API operations
  3. Customize tool names and descriptions
  4. Configure authentication
  5. Execute API operations as MCP tool calls

All credentials are held in memory only -- never logged or persisted.
"""

from __future__ import annotations

import json
import logging
from typing import Annotated, Any

from pydantic import Field

from mcp_bridge.openapi.executor import ApiToolExecutor
from mcp_bridge.openapi.models import ApiSpec
from mcp_bridge.openapi.parser import OpenAPIParser

logger = logging.getLogger(__name__)

# Global state -- loaded specs and their executors
_specs: dict[str, ApiSpec] = {}  # plugin_name → ApiSpec
_executors: dict[str, ApiToolExecutor] = {}  # plugin_name → executor


# ---------------------------------------------------------------------------
# Meta-Tools (registered with FastMCP in server.py)
# ---------------------------------------------------------------------------


async def import_openapi_spec(
    source: Annotated[
        str,
        Field(
            description="The specification source. Can be: A URL (e.g., 'https://api.example.com/openapi.json'), a local file path (e.g., '/path/to/spec.yaml'), or a raw JSON or YAML string."
        ),
    ],
    plugin_name: Annotated[
        str | None,
        Field(
            description="Custom name for this API plugin. If not provided, uses the API title from the spec."
        ),
    ] = None,
    source_type: Annotated[
        str,
        Field(
            description="Source type hint: 'url', 'file', 'string', or 'auto' (default)."
        ),
    ] = "auto",
) -> str:
    """Import an OpenAPI/Swagger specification and discover API operations.

    Parses the spec and extracts all endpoints, parameters, auth schemes,
    and descriptions. Each endpoint becomes an available API tool.

    Supports OpenAPI 3.0+/3.1 and Swagger 2.0 in JSON or YAML format.

    Args:
        source: The specification source. Can be:
            - A URL (e.g., 'https://api.example.com/openapi.json')
            - A local file path (e.g., '/path/to/spec.yaml')
            - A raw JSON or YAML string
        plugin_name: Custom name for this API plugin. If not provided,
            uses the API title from the spec.
        source_type: Source type hint: 'url', 'file', 'string', or 'auto' (default).

    Returns:
        JSON with the parsed spec summary: operations count, auth schemes,
        tags, and a hint to configure auth and browse operations.
    """
    parser = OpenAPIParser()

    try:
        # Auto-detect source type
        if source_type == "auto":
            if source.startswith(("http://", "https://")):
                source_type = "url"
            elif source.strip().startswith(("{", "openapi:", "swagger:")):
                source_type = "string"
            else:
                source_type = "file"

        if source_type == "url":
            spec = await parser.parse_url(source)
        elif source_type == "file":
            spec = parser.parse_file(source)
        else:
            spec = parser.parse_string(source)

        # Set plugin name
        name = plugin_name or spec.title.lower().replace(" ", "-").replace("/", "-")
        spec.plugin_name = name

        # Store spec and create executor
        _specs[name] = spec
        _executors[name] = ApiToolExecutor(spec)

        return json.dumps(
            {
                "plugin_name": name,
                "api_title": spec.title,
                "api_version": spec.version,
                "base_url": spec.base_url,
                "operations_total": spec.operation_count,
                "operations_enabled": spec.enabled_count,
                "tags": spec.tags,
                "auth_schemes": [
                    {"name": s.name, "type": s.type}
                    for s in spec.auth_schemes
                ],
                "hint": f"Use list_api_tools('{name}') to see all operations. "
                f"Use configure_api_auth('{name}', ...) to set credentials.",
            },
            indent=2,
        )

    except Exception as e:
        logger.error("Failed to import OpenAPI spec: %s", e)
        return json.dumps({"error": str(e)})


async def list_api_tools(
    plugin_name: Annotated[str, Field(description="The plugin name from import_openapi_spec.")],
    tag: Annotated[str | None, Field(description="Filter operations by tag.")] = None,
    search: Annotated[str | None, Field(description="Search operations by name, path, or description.")] = None,
    show_disabled: Annotated[bool, Field(description="Include disabled and deprecated operations.")] = False,
) -> str:
    """List all API operations available as tools from an imported spec.

    Browse by tag, search by name/description, or list all operations.

    Args:
        plugin_name: The plugin name from import_openapi_spec.
        tag: Filter operations by tag.
        search: Search operations by name, path, or description.
        show_disabled: Include disabled and deprecated operations.

    Returns:
        JSON with operations list including tool names, descriptions,
        HTTP methods, paths, and parameter summaries.
    """
    spec = _specs.get(plugin_name)
    if not spec:
        return json.dumps({"error": f"Plugin '{plugin_name}' not found. Import a spec first."})

    operations = spec.enabled_operations if not show_disabled else spec.operations

    # Filter by tag
    if tag:
        operations = [op for op in operations if tag in op.tags]

    # Filter by search
    if search:
        search_lower = search.lower()
        operations = [
            op
            for op in operations
            if search_lower in op.effective_tool_name.lower()
            or search_lower in op.effective_description.lower()
            or search_lower in op.path.lower()
        ]

    return json.dumps(
        {
            "plugin_name": plugin_name,
            "operations": [
                {
                    "tool_name": op.effective_tool_name,
                    "method": op.method.upper(),
                    "path": op.path,
                    "description": op.effective_description,
                    "tags": op.tags,
                    "parameters": [
                        {"name": p.name, "in": p.location, "required": p.required, "type": p.schema_type}
                        for p in op.parameters
                    ],
                    "has_body": op.request_body is not None,
                    "enabled": op.enabled,
                    "deprecated": op.deprecated,
                }
                for op in operations
            ],
            "count": len(operations),
            "tags": spec.tags,
            "hint": f"Use call_api_tool('{plugin_name}', '<tool_name>', '<args_json>') to execute.",
        },
        indent=2,
    )


async def configure_api_auth(
    plugin_name: Annotated[str, Field(description="The plugin name from import_openapi_spec.")],
    credentials: Annotated[
        str,
        Field(
            description="JSON object mapping scheme names to values. Example: '{\"BearerAuth\": \"eyJ...\", \"ApiKeyAuth\": \"sk-...\"}'. For basic auth, use \"username:password\" format."
        ),
    ],
) -> str:
    """Configure authentication for an imported API.

    Set credentials for the API's security schemes. Credentials are
    stored in memory only and never logged or persisted.

    Args:
        plugin_name: The plugin name from import_openapi_spec.
        credentials: JSON object mapping scheme names to values.
            Example: '{"BearerAuth": "eyJ...", "ApiKeyAuth": "sk-..."}'
            For basic auth, use "username:password" format.

    Returns:
        JSON confirming which auth schemes were configured.
    """
    spec = _specs.get(plugin_name)
    executor = _executors.get(plugin_name)
    if not spec or not executor:
        return json.dumps({"error": f"Plugin '{plugin_name}' not found."})

    try:
        creds = json.loads(credentials) if isinstance(credentials, str) else credentials
    except json.JSONDecodeError:
        return json.dumps({"error": f"Invalid credentials JSON: {credentials}"})

    executor.configure_all_auth(creds)

    configured_schemes = []
    for scheme in spec.auth_schemes:
        if scheme.name in creds:
            configured_schemes.append({"name": scheme.name, "type": scheme.type, "configured": True})
        else:
            configured_schemes.append({"name": scheme.name, "type": scheme.type, "configured": False})

    return json.dumps(
        {
            "plugin_name": plugin_name,
            "auth_schemes": configured_schemes,
            "hint": f"Auth configured. Use call_api_tool('{plugin_name}', ...) to make calls.",
        },
        indent=2,
    )


async def customize_api_tool(
    plugin_name: Annotated[str, Field(description="The plugin name from import_openapi_spec.")],
    operation_id: Annotated[str, Field(description="The operation ID to customize.")],
    tool_name: Annotated[str | None, Field(description="New tool name (optional).")] = None,
    tool_description: Annotated[str | None, Field(description="New description (optional).")] = None,
    enabled: Annotated[bool | None, Field(description="Enable or disable this operation (optional).")] = None,
) -> str:
    """Customize an API operation's MCP tool name, description, or enabled state.

    Use this to rename tools, add better descriptions, or disable operations
    you don't need.

    Args:
        plugin_name: The plugin name from import_openapi_spec.
        operation_id: The operation ID to customize.
        tool_name: New tool name (optional).
        tool_description: New description (optional).
        enabled: Enable or disable this operation (optional).

    Returns:
        JSON with the updated operation details.
    """
    spec = _specs.get(plugin_name)
    if not spec:
        return json.dumps({"error": f"Plugin '{plugin_name}' not found."})

    for op in spec.operations:
        if op.operation_id == operation_id or op.effective_tool_name == operation_id:
            if tool_name is not None:
                op.tool_name = tool_name
            if tool_description is not None:
                op.tool_description = tool_description
            if enabled is not None:
                op.enabled = enabled

            return json.dumps(
                {
                    "plugin_name": plugin_name,
                    "operation_id": op.operation_id,
                    "tool_name": op.effective_tool_name,
                    "description": op.effective_description,
                    "enabled": op.enabled,
                    "message": "Operation updated.",
                },
                indent=2,
            )

    return json.dumps({"error": f"Operation '{operation_id}' not found in plugin '{plugin_name}'."})


async def call_api_tool(
    plugin_name: Annotated[str, Field(description="The plugin name from import_openapi_spec.")],
    tool_name: Annotated[str, Field(description="The tool name (from list_api_tools).")],
    arguments: Annotated[
        str | None,
        Field(
            description="JSON string with the tool's arguments. Example: '{\"work_item_id\": \"12345\", \"expand\": \"all\"}'"
        ),
    ] = None,
) -> str:
    """Execute an API operation as an MCP tool call.

    Makes the actual HTTP request to the API with the provided arguments.
    Parameters are automatically routed to path, query, header, or body
    based on the OpenAPI spec definition.

    Args:
        plugin_name: The plugin name from import_openapi_spec.
        tool_name: The tool name (from list_api_tools).
        arguments: JSON string with the tool's arguments.
            Example: '{"work_item_id": "12345", "expand": "all"}'

    Returns:
        JSON with: success, status_code, body (API response), latency_ms.
    """
    executor = _executors.get(plugin_name)
    if not executor:
        return json.dumps({"error": f"Plugin '{plugin_name}' not found."})

    args: dict[str, Any] = {}
    if arguments:
        try:
            args = json.loads(arguments) if isinstance(arguments, str) else arguments
        except json.JSONDecodeError:
            return json.dumps({"error": f"Invalid arguments JSON: {arguments}"})

    result = await executor.execute(tool_name, args)

    # Format the result for the agent
    if result.get("success"):
        body = result.get("body")
        if isinstance(body, (dict, list)):
            return json.dumps(body, indent=2, default=str)
        return str(body)
    else:
        return json.dumps(
            {
                "error": result.get("error", f"HTTP {result.get('status_code')}"),
                "status_code": result.get("status_code"),
                "body": result.get("body"),
                "latency_ms": result.get("latency_ms"),
            },
            indent=2,
            default=str,
        )


async def get_api_tool_schema(
    plugin_name: Annotated[str, Field(description="The plugin name from import_openapi_spec.")],
    tool_name: Annotated[str, Field(description="The tool name to get the schema for.")],
) -> str:
    """Get the full JSON Schema for an API tool's input parameters.

    Returns the complete parameter schema including types, descriptions,
    required fields, and enum values.

    Args:
        plugin_name: The plugin name from import_openapi_spec.
        tool_name: The tool name to get the schema for.

    Returns:
        JSON Schema for the tool's input parameters.
    """
    spec = _specs.get(plugin_name)
    if not spec:
        return json.dumps({"error": f"Plugin '{plugin_name}' not found."})

    for op in spec.operations:
        if op.effective_tool_name == tool_name or op.operation_id == tool_name:
            return json.dumps(
                {
                    "tool_name": op.effective_tool_name,
                    "method": op.method.upper(),
                    "path": op.path,
                    "description": op.effective_description,
                    "input_schema": op.to_json_schema(),
                },
                indent=2,
            )

    return json.dumps({"error": f"Tool '{tool_name}' not found in plugin '{plugin_name}'."})
