import json
import logging
from abc import ABC, abstractmethod
from datetime import datetime
from typing import Any, Dict, List, Optional

from langchain_core.language_models import BaseLLM

from ..config import search_config

logger = logging.getLogger(__name__)


class BaseSearchEngine(ABC):
    """
    Abstract base class for search engines with two-phase retrieval capability.
    Handles common parameters and implements the two-phase search approach.
    """

    def __init__(
        self,
        llm: Optional[BaseLLM] = None,
        max_filtered_results: Optional[int] = None,
        max_results: Optional[int] = 10,  # Default value if not provided
        **kwargs,
    ):
        """
        Initialize the search engine with common parameters.

        Args:
            llm: Optional language model for relevance filtering
            max_filtered_results: Maximum number of results to keep after filtering
            max_results: Maximum number of search results to return
            **kwargs: Additional engine-specific parameters
        """
        if max_filtered_results is None:
            max_filtered_results = 5
        if max_results is None:
            max_results = 10

        self.llm = llm  # LLM for relevance filtering
        self._max_filtered_results = int(max_filtered_results)  # Ensure it's an integer
        self._max_results = max(1, int(max_results))  # Ensure it's a positive integer

    @property
    def max_filtered_results(self) -> int:
        """Get the maximum number of filtered results."""
        return self._max_filtered_results

    @max_filtered_results.setter
    def max_filtered_results(self, value: int) -> None:
        """Set the maximum number of filtered results."""
        if value is None:
            value = 5
            logger.warning("Setting max_filtered_results to 5")
        self._max_filtered_results = int(value)

    @property
    def max_results(self) -> int:
        """Get the maximum number of search results."""
        return self._max_results

    @max_results.setter
    def max_results(self, value: int) -> None:
        """Set the maximum number of search results."""
        if value is None:
            value = 10
        self._max_results = max(1, int(value))

    def run(self, query: str) -> List[Dict[str, Any]]:
        """
        Run the search engine with a given query, retrieving and filtering results.
        This implements a two-phase retrieval approach:
        1. Get preview information for many results
        2. Filter the previews for relevance
        3. Get full content for only the relevant results

        Args:
            query: The search query

        Returns:
            List of search results with full content (if available)
        """
        # Ensure we're measuring time correctly for citation tracking

        # Step 1: Get preview information for items
        previews = self._get_previews(query)
        if not previews:
            logger.info(
                f"Search engine {self.__class__.__name__} returned no preview results for query: {query}"
            )
            return []

        # Step 2: Filter previews for relevance with LLM
        filtered_items = self._filter_for_relevance(previews, query)
        if not filtered_items:
            logger.info(
                f"All preview results were filtered out as irrelevant for query: {query}"
            )
            # Do not fall back to previews, return empty list instead
            return []

        # Step 3: Get full content for filtered items
        # Import config inside the method to avoid circular import

        if (
            hasattr(search_config, "SEARCH_SNIPPETS_ONLY")
            and search_config.SEARCH_SNIPPETS_ONLY
        ):
            logger.info("Returning snippet-only results as per config")
            results = filtered_items
        else:
            results = self._get_full_content(filtered_items)

        return results

    def invoke(self, query: str) -> List[Dict[str, Any]]:
        """Compatibility method for LangChain tools"""
        return self.run(query)

    def _filter_for_relevance(
        self, previews: List[Dict[str, Any]], query: str
    ) -> List[Dict[str, Any]]:
        """
        Filter search results by relevance to the query using the LLM.

        Args:
            previews: List of preview dictionaries
            query: The original search query

        Returns:
            Filtered list of preview dictionaries
        """
        # If no LLM or too few previews, return all
        if not self.llm or len(previews) <= 1:
            return previews

        # Create a simple context for LLM
        preview_context = []
        for i, preview in enumerate(previews):
            title = preview.get("title", "Untitled").strip()
            snippet = preview.get("snippet", "").strip()

            # Clean up snippet if too long
            if len(snippet) > 300:
                snippet = snippet[:300] + "..."

            preview_context.append(f"[{i}] Title: {title}\nSnippet: {snippet}")

        # Set a reasonable limit on context length
        current_date = datetime.now().strftime("%Y-%m-%d")
        prompt = f"""Analyze these search results and provide a ranked list of the most relevant ones.

IMPORTANT: Evaluate and rank based on these criteria (in order of importance):
1. Timeliness - current/recent information as of {current_date}
2. Direct relevance to query: "{query}"
3. Source reliability (prefer official sources, established websites)
4. Factual accuracy (cross-reference major claims)

Search results to evaluate:
{json.dumps(previews, indent=2)}

Return ONLY a JSON array of indices (0-based) ranked from most to least relevant.
Include ONLY indices that meet ALL criteria, with the most relevant first.
Example response: [4, 0, 2]

Respond with ONLY the JSON array, no other text."""

        try:
            # Get LLM's evaluation
            response = self.llm.invoke(prompt)

            # Log the raw response for debugging
            logger.info(f"Raw LLM response for relevance filtering: {response}")

            # Handle different response formats
            response_text = ""
            if hasattr(response, "content"):
                response_text = response.content
            else:
                response_text = str(response)

            # Clean up response
            response_text = response_text.strip()
            logger.debug(f"Cleaned response text: {response_text}")

            # Find JSON array in response
            start_idx = response_text.find("[")
            end_idx = response_text.rfind("]")

            if start_idx >= 0 and end_idx > start_idx:
                array_text = response_text[start_idx : end_idx + 1]
                try:
                    ranked_indices = json.loads(array_text)

                    # Validate that ranked_indices is a list of integers
                    if not isinstance(ranked_indices, list):
                        logger.warning(
                            "LLM response is not a list, returning empty results"
                        )
                        return []

                    if not all(isinstance(idx, int) for idx in ranked_indices):
                        logger.warning(
                            "LLM response contains non-integer indices, returning empty results"
                        )
                        return []

                    # Return the results in ranked order
                    ranked_results = []
                    for idx in ranked_indices:
                        if idx < len(previews):
                            ranked_results.append(previews[idx])
                        else:
                            logger.warning(f"Index {idx} out of range, skipping")

                    # Limit to max_filtered_results if specified
                    if (
                        self.max_filtered_results
                        and len(ranked_results) > self.max_filtered_results
                    ):
                        logger.info(
                            f"Limiting filtered results to top {self.max_filtered_results}"
                        )
                        return ranked_results[: self.max_filtered_results]

                    return ranked_results

                except json.JSONDecodeError as e:
                    logger.warning(f"Failed to parse JSON from LLM response: {e}")
                    logger.debug(f"Problematic JSON text: {array_text}")
                    return []
            else:
                logger.warning(
                    "Could not find JSON array in response, returning original previews"
                )
                logger.debug(f"Response text without JSON array: {response_text}")
                return previews[: min(5, len(previews))]

        except Exception as e:
            logger.error(f"Relevance filtering error: {e}", exc_info=True)
            # Fall back to returning top results on error
            return previews[: min(5, len(previews))]

    @abstractmethod
    def _get_previews(self, query: str) -> List[Dict[str, Any]]:
        """
        Get preview information (titles, summaries) for initial search results.

        Args:
            query: The search query

        Returns:
            List of preview dictionaries with at least 'id', 'title', and 'snippet' keys
        """
        pass

    @abstractmethod
    def _get_full_content(
        self, relevant_items: List[Dict[str, Any]]
    ) -> List[Dict[str, Any]]:
        """
        Get full content for the relevant items.

        Args:
            relevant_items: List of relevant preview dictionaries

        Returns:
            List of result dictionaries with full content
        """
        pass
