from __future__ import annotations

"""
Copyright (C) 2025 Luciano Guerche

This file is part of rabbitmq-mcp-server.

rabbitmq-mcp-server is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

rabbitmq-mcp-server is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with rabbitmq-mcp-server. If not, see <https://www.gnu.org/licenses/>.
"""

"""Implementação do MCP tool get-id."""

import difflib
import json
import re
from functools import lru_cache
from pathlib import Path
from typing import Any

import structlog

from rabbitmq_mcp_connection.schemas.mcp import MCPError, MCPToolResult

logger = structlog.get_logger(__name__)

# Path to operations registry
OPERATIONS_REGISTRY_PATH = (
    Path(__file__).resolve().parent.parent.parent.parent / "data" / "operations.json"
)

# Operation ID validation pattern
OPERATION_ID_PATTERN = r"^[a-z_]+\.[a-z_]+"


class OperationNotFoundError(KeyError):
    """Erro quando uma operação solicitada não existe."""


def _find_similar_operations(
    operation_id: str, operations: dict[str, Any]
) -> list[str]:
    """Find similar operation IDs using fuzzy matching.

    Args:
        operation_id: Invalid operation ID to find matches for
        operations: Dict of all available operations

    Returns:
        List of up to 3 similar operation IDs
    """
    # Extract namespace from invalid operation_id (part before first dot)
    parts = operation_id.split(".", 1)
    if len(parts) > 1:
        namespace = parts[0]
        # Find all operation IDs with same namespace
        namespace_ops = [
            op_id for op_id in operations if op_id.startswith(f"{namespace}.")
        ]
        if namespace_ops:
            # Use fuzzy string matching on namespace operations
            matches = difflib.get_close_matches(
                operation_id, namespace_ops, n=3, cutoff=0.4
            )
            if matches:
                return matches

    # Fallback: fuzzy match across all operation IDs
    matches = difflib.get_close_matches(
        operation_id, list(operations.keys()), n=3, cutoff=0.4
    )
    return matches


@lru_cache(maxsize=1)
def _load_operations_registry() -> dict[str, Any]:
    """Load operations registry from data/operations.json (cached).

    Returns:
        Dictionary mapping operation_id to operation data
    """
    if not OPERATIONS_REGISTRY_PATH.exists():
        raise FileNotFoundError(
            f"Operations registry not found: {OPERATIONS_REGISTRY_PATH}"
        )

    with open(OPERATIONS_REGISTRY_PATH, encoding="utf-8") as f:
        data: dict[str, Any] = json.load(f)

    operations: dict[str, Any] = data.get("operations", {})
    return operations


def _generate_usage_hints(operation_data: dict[str, Any]) -> list[str]:
    """Generate usage hints based on operation metadata.

    Analyzes operation flags and HTTP method to provide helpful warnings
    about authentication requirements, resource modifications, deprecation
    status, and rate limiting.

    Args:
        operation_data: Operation schema dictionary containing metadata flags
            (requires_auth, http_method, deprecated, rate_limit_exempt, etc.)

    Returns:
        List of usage hint strings with warning emojis, empty if no warnings apply

    Example:
        >>> hints = _generate_usage_hints({"requires_auth": True, "http_method": "DELETE"})
        >>> print(hints)
        ['⚠️ This operation requires authentication',
         '⚠️ This operation modifies resources (HTTP DELETE)']
    """
    hints = []

    # Check for authentication requirement
    if operation_data.get("requires_auth", True):
        hints.append("⚠️ This operation requires authentication")

    # Check for resource modification (destructive methods)
    method = operation_data.get("http_method", "").upper()
    if method in ["POST", "PUT", "PATCH", "DELETE"]:
        hints.append(f"⚠️ This operation modifies resources (HTTP {method})")

    # Check for deprecation
    if operation_data.get("deprecated", False):
        alternative = operation_data.get("alternative_operation", "a newer operation")
        hints.append(f"⚠️ This operation is deprecated - use {alternative} instead")

    # Check for rate limiting
    if not operation_data.get("rate_limit_exempt", True):
        hints.append("⚠️ This operation is rate-limited")

    return hints


@lru_cache(maxsize=256)
def _format_operation_cached(operation_id: str, schema_json: str) -> str:
    """Format operation schema with pretty-printing (cached).

    Caches formatted JSON output to avoid repeated formatting overhead.
    Cache size of 256 is sufficient for all 132 operations in registry.

    Args:
        operation_id: Operation identifier (used as cache key component)
        schema_json: JSON string of schema data (normalized with sort_keys=True)

    Returns:
        Pretty-printed JSON string with 2-space indentation and UTF-8 encoding

    Note:
        Input schema_json must be normalized (sort_keys=True) for cache effectiveness.
        Cache statistics can be inspected via _format_operation_cached.cache_info().
    """
    schema = json.loads(schema_json)
    return json.dumps(schema, indent=2, ensure_ascii=False)


def get_operation_schema(operation_id: str) -> dict[str, Any]:
    """Get operation schema from registry.

    Args:
        operation_id: Operation identifier

    Returns:
        Operation schema dictionary

    Raises:
        OperationNotFoundError: If operation not found
    """
    operations = _load_operations_registry()
    if operation_id not in operations:
        raise OperationNotFoundError(operation_id)

    schema: dict[str, Any] = operations[operation_id]
    return schema


async def handle_get_id(
    operation_id: str, correlation_id: str | None = None
) -> MCPToolResult:
    """Handle get-id tool request with enhanced error messages and usage hints.

    Retrieves complete operation documentation from operations.json registry,
    validates operation_id format, generates usage hints, and returns formatted
    response. Implements AC #1-7 requirements for get-id tool.

    Args:
        operation_id: Operation identifier to retrieve (format: namespace.action,
            e.g., "queues.list"). Must be non-empty and match pattern validation.
        correlation_id: Optional correlation ID for request tracing in logs

    Returns:
        MCPToolResult with operation schema and formatted output on success,
        or error with helpful suggestions on failure

    Raises:
        None - all errors returned as MCPToolResult with error field

    Example:
        >>> result = await handle_get_id("queues.list", correlation_id="req-123")
        >>> if result.success:
        ...     print(result.result["schema"]["description"])
        ...     print(result.result["formatted"])  # Pretty-printed JSON
    """
    # Validate operation_id is non-empty (AC #5)
    if not operation_id or not operation_id.strip():
        logger.warning(
            "operation_lookup",
            operation_id=operation_id,
            found=False,
            error="empty_operation_id",
            correlation_id=correlation_id,
        )
        return MCPToolResult(
            success=False,
            error=MCPError(
                code="INVALID_PARAMETER",
                message="Parameter 'operation_id' is required and must be non-empty",
            ),
        )

    # Validate operation_id matches expected pattern (AC #2)
    if not re.match(OPERATION_ID_PATTERN, operation_id):
        logger.warning(
            "operation_lookup",
            operation_id=operation_id,
            found=False,
            error="malformed_operation_id",
            correlation_id=correlation_id,
        )
        return MCPToolResult(
            success=False,
            error=MCPError(
                code="INVALID_PARAMETER",
                message=f"Parameter 'operation_id' must match pattern 'namespace.action' (e.g., 'queues.list'). Got: '{operation_id}'",
            ),
        )

    try:
        schema = get_operation_schema(operation_id)

        # Generate usage hints
        hints = _generate_usage_hints(schema)

        # Add usage hints to response if any exist
        if hints:
            schema["usage_hints"] = hints

        # Format with pretty-printing using cached function
        schema_json = json.dumps(schema, sort_keys=True)  # Normalize for cache key
        formatted_schema = _format_operation_cached(operation_id, schema_json)

        # Log successful lookup
        logger.info(
            "operation_lookup",
            operation_id=operation_id,
            found=True,
            response_size=len(formatted_schema),
            correlation_id=correlation_id,
        )

        return MCPToolResult(
            success=True,
            result={
                "schema": schema,
                "formatted": formatted_schema,
            },
        )
    except OperationNotFoundError as exc:
        # Find similar operations for helpful suggestions
        operations = _load_operations_registry()
        similar_ops = _find_similar_operations(operation_id, operations)

        error_message = f"Operation '{exc.args[0]}' not found."
        if similar_ops:
            error_message += f" Did you mean: {', '.join(similar_ops)}?"
        else:
            error_message += " Use search-ids to discover operations."

        # Log failed lookup
        logger.warning(
            "operation_lookup",
            operation_id=operation_id,
            found=False,
            similar_operations=similar_ops,
            correlation_id=correlation_id,
        )

        return MCPToolResult(
            success=False,
            error=MCPError(
                code="OPERATION_NOT_FOUND",
                message=error_message,
            ),
        )


__all__ = ["handle_get_id", "get_operation_schema", "OperationNotFoundError"]
