"""
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/>.
"""

"""
MCP Tool: Semantic Search for Operation IDs
Implements search_ids using cosine similarity with embeddings
"""

import time
from functools import lru_cache
from typing import Any

import numpy as np
from pydantic import BaseModel

from config.logging import get_logger
from tools.vector_db.embeddings import embed_text

logger = get_logger(__name__)

# Constants
DEFAULT_THRESHOLD = 0.7
DEFAULT_MAX_RESULTS = 10
MAX_RESULTS_CAP = 50
CACHE_SIZE = 100


class OperationMatch(BaseModel):
    operation_id: str
    description: str
    similarity_score: float
    namespace: str


class SearchResult(BaseModel):
    results: list[OperationMatch]


@lru_cache(maxsize=CACHE_SIZE)
def _get_cached_query_embedding(query: str) -> tuple[float, ...]:
    """Generate and cache query embeddings using LRU cache."""
    embedding = embed_text(query)
    return tuple(embedding.tolist())


def search_ids(
    query: str,
    threshold: float = DEFAULT_THRESHOLD,
    max_results: int = DEFAULT_MAX_RESULTS,
    embeddings: dict[str, Any] | None = None,
    operations_registry: dict[str, Any] | None = None,
    correlation_id: str | None = None,
) -> SearchResult:
    """
    Search for RabbitMQ operations using natural language queries.

    Args:
        query: Natural language search query
        threshold: Minimum similarity score (0.0-1.0), default 0.7
        max_results: Maximum results to return (1-50), default 10
        embeddings: Embeddings dictionary loaded from data/embeddings.json
        operations_registry: Operations registry loaded from data/operations.json
        correlation_id: Optional correlation ID for tracing

    Returns:
        SearchResult with matching operations ranked by similarity

    Raises:
        ValueError: If parameters are invalid
    """
    start_time = time.time()

    # Validate parameters
    if not query or not query.strip():
        raise ValueError("query must be a non-empty string")
    if not (0.0 <= threshold <= 1.0):
        raise ValueError(f"threshold must be between 0.0 and 1.0, got {threshold}")
    if not (1 <= max_results <= MAX_RESULTS_CAP):
        raise ValueError(
            f"max_results must be between 1 and {MAX_RESULTS_CAP}, got {max_results}"
        )

    if embeddings is None:
        raise ValueError("embeddings data not loaded")
    if operations_registry is None:
        raise ValueError("operations_registry data not loaded")

    # Check cache hit before generating embedding
    cache_info_before = _get_cached_query_embedding.cache_info()

    # Generate query embedding (cached)
    query_embedding_tuple = _get_cached_query_embedding(query)
    query_embedding = np.array(query_embedding_tuple, dtype=np.float32)

    # Check if cache was hit
    cache_info_after = _get_cached_query_embedding.cache_info()
    cache_hit = cache_info_after.hits > cache_info_before.hits

    # Get embeddings dictionary from wrapper structure
    embeddings_dict = embeddings.get("embeddings", {})
    operations_dict = operations_registry.get("operations", {})

    # Compute cosine similarity for all operations (vectorized)
    results = []
    operation_embeddings = []
    operation_ids = []

    for op_id, op_embedding in embeddings_dict.items():
        operation_embeddings.append(op_embedding)
        operation_ids.append(op_id)

    if not operation_embeddings:
        execution_time_ms = (time.time() - start_time) * 1000
        logger.info(
            "search_query",
            query=query,
            threshold=threshold,
            max_results=max_results,
            correlation_id=correlation_id,
            result_count=0,
            execution_time_ms=f"{execution_time_ms:.2f}",
            cache_hit=cache_hit,
        )
        return SearchResult(results=[])

    # Vectorized cosine similarity calculation
    op_embeddings_array = np.array(operation_embeddings, dtype=np.float32)
    similarities = np.dot(op_embeddings_array, query_embedding)

    # Filter by threshold and sort by similarity descending
    for i, (op_id, similarity) in enumerate(zip(operation_ids, similarities)):
        if similarity >= threshold:
            op_data = operations_dict.get(op_id, {})
            results.append(
                {
                    "operation_id": op_id,
                    "description": op_data.get("description", ""),
                    "similarity_score": float(similarity),
                    "namespace": op_data.get("namespace", ""),
                }
            )

    # Sort by similarity score descending
    results.sort(key=lambda x: x["similarity_score"], reverse=True)

    # Limit to max_results
    results = results[:max_results]

    # Calculate execution time
    execution_time_ms = (time.time() - start_time) * 1000

    # Get top score
    top_score = results[0]["similarity_score"] if results else 0.0

    # Count namespace distribution
    namespace_dist: dict[str, int] = {}
    for r in results:
        ns = r.get("namespace", "unknown")
        namespace_dist[ns] = namespace_dist.get(ns, 0) + 1

    # Log search operation
    logger.info(
        "search_query",
        query=query,
        threshold=threshold,
        max_results=max_results,
        correlation_id=correlation_id,
        result_count=len(results),
        top_score=f"{top_score:.4f}" if results else None,
        execution_time_ms=f"{execution_time_ms:.2f}",
        cache_hit=cache_hit,
        cache_size=cache_info_after.currsize,
        cache_hit_rate=f"{cache_info_after.hits / max(1, cache_info_after.hits + cache_info_after.misses):.2f}",
        namespace_distribution=namespace_dist if results else {},
    )

    # Log zero-result queries for analytics
    if not results:
        logger.info(
            "search_zero_results",
            query=query,
            threshold=threshold,
            correlation_id=correlation_id,
            suggestion="Try broader terms or check spelling",
        )

    # Format results
    formatted_results = [
        OperationMatch(
            operation_id=r["operation_id"],
            description=r["description"],
            similarity_score=r["similarity_score"],
            namespace=r["namespace"],
        )
        for r in results
    ]

    return SearchResult(results=formatted_results)
