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

from langchain_core.language_models import BaseLLM
from loguru import logger
from tenacity import (
    RetryError,
    retry,
    retry_if_exception_type,
    stop_after_attempt,
)
from tenacity.wait import wait_base

from ..advanced_search_system.filters.base_filter import BaseFilter
from ..utilities.thread_context import set_search_context

# Lazy import for metrics to avoid database dependencies in programmatic mode
# from ..metrics.search_tracker import get_search_tracker
from .rate_limiting import RateLimitError, get_tracker


class AdaptiveWait(wait_base):
    """Custom wait strategy that uses adaptive rate limiting."""

    def __init__(self, get_wait_func):
        self.get_wait_func = get_wait_func

    def __call__(self, retry_state):
        return self.get_wait_func()


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
        preview_filters: List[BaseFilter] | None = None,
        content_filters: List[BaseFilter] | None = None,
        search_snippets_only: bool = True,  # New parameter with default
        settings_snapshot: Optional[Dict[str, Any]] = None,
        programmatic_mode: bool = False,
        **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
            preview_filters: Filters that will be applied to all previews
                produced by the search engine, before relevancy checks.
            content_filters: Filters that will be applied to the full content
                produced by the search engine, after relevancy checks.
            search_snippets_only: Whether to return only snippets or full content
            settings_snapshot: Settings snapshot for configuration
            programmatic_mode: If True, disables database operations and uses memory-only tracking
            **kwargs: Additional engine-specific parameters
        """
        if max_filtered_results is None:
            max_filtered_results = 5
        if max_results is None:
            max_results = 10
        self._preview_filters: List[BaseFilter] = preview_filters
        if self._preview_filters is None:
            self._preview_filters = []
        self._content_filters: List[BaseFilter] = content_filters
        if self._content_filters is None:
            self._content_filters = []

        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
        self.search_snippets_only = search_snippets_only  # Store the setting
        self.settings_snapshot = (
            settings_snapshot or {}
        )  # Store settings snapshot
        self.programmatic_mode = programmatic_mode

        # Rate limiting attributes
        self.engine_type = self.__class__.__name__
        # Create a tracker with our settings if in programmatic mode
        if self.programmatic_mode:
            from .rate_limiting.tracker import AdaptiveRateLimitTracker

            self.rate_tracker = AdaptiveRateLimitTracker(
                settings_snapshot=self.settings_snapshot,
                programmatic_mode=self.programmatic_mode,
            )
        else:
            self.rate_tracker = get_tracker()
        self._last_wait_time = (
            0.0  # Default to 0 for successful searches without rate limiting
        )
        self._last_results_count = 0

    @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 _get_adaptive_wait(self) -> float:
        """Get adaptive wait time from tracker."""
        wait_time = self.rate_tracker.get_wait_time(self.engine_type)
        self._last_wait_time = wait_time
        logger.debug(
            f"{self.engine_type} waiting {wait_time:.2f}s before retry"
        )
        return wait_time

    def _record_retry_outcome(self, retry_state) -> None:
        """Record outcome after retry completes."""
        success = (
            not retry_state.outcome.failed if retry_state.outcome else False
        )
        self.rate_tracker.record_outcome(
            self.engine_type,
            self._last_wait_time or 0,
            success,
            retry_state.attempt_number,
            error_type="RateLimitError" if not success else None,
            search_result_count=self._last_results_count if success else 0,
        )

    def run(
        self, query: str, research_context: Dict[str, Any] | None = None
    ) -> 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
            research_context: Context from previous research to use.

        Returns:
            List of search results with full content (if available)
        """
        # Track search call for metrics (if available and not in programmatic mode)
        tracker = None
        if not self.programmatic_mode:
            from ..metrics.search_tracker import get_search_tracker

            tracker = get_search_tracker()

            # For thread-safe context propagation: if we have research_context parameter, use it
            # Otherwise, try to inherit from current thread context (normal case)
            # This allows strategies running in threads to explicitly pass context when needed
            if research_context:
                # Explicit context provided - use it and set it for this thread
                set_search_context(research_context)
            else:
                # Search tracker will not work without research context.
                logger.debug("Search metrics tracking disabled: no context.")
                tracker = None
        else:
            logger.debug(
                "Search metrics tracking disabled in programmatic mode"
            )

        engine_name = self.__class__.__name__.replace(
            "SearchEngine", ""
        ).lower()
        start_time = time.time()

        success = True
        error_message = None
        results_count = 0

        # Define the core search function with retry logic
        if self.rate_tracker.enabled:
            # Rate limiting enabled - use retry with adaptive wait
            @retry(
                stop=stop_after_attempt(3),
                wait=AdaptiveWait(lambda: self._get_adaptive_wait()),
                retry=retry_if_exception_type((RateLimitError,)),
                after=self._record_retry_outcome,
                reraise=True,
            )
            def _run_with_retry():
                nonlocal success, error_message, results_count
                return _execute_search()
        else:
            # Rate limiting disabled - run without retry
            def _run_with_retry():
                nonlocal success, error_message, results_count
                return _execute_search()

        def _execute_search():
            nonlocal success, error_message, results_count

            try:
                # 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}"
                    )
                    results_count = 0
                    return []

                for preview_filter in self._preview_filters:
                    previews = preview_filter.filter_results(previews, query)

                # Step 2: Filter previews for relevance with LLM
                # TEMPORARILY DISABLED: Skip LLM relevance filtering
                filtered_items = previews
                logger.info(
                    f"LLM relevance filtering disabled - returning all {len(previews)} previews"
                )

                # Step 3: Get full content for filtered items
                if self.search_snippets_only:
                    logger.info("Returning snippet-only results as per config")
                    results = filtered_items
                else:
                    results = self._get_full_content(filtered_items)

                for content_filter in self._content_filters:
                    results = content_filter.filter_results(results, query)

                results_count = len(results)
                self._last_results_count = results_count

                # Record success if we get here and rate limiting is enabled
                if self.rate_tracker.enabled:
                    logger.info(
                        f"Recording successful search for {self.engine_type}: wait_time={self._last_wait_time}s, results={results_count}"
                    )
                    self.rate_tracker.record_outcome(
                        self.engine_type,
                        self._last_wait_time,
                        success=True,
                        retry_count=1,  # First attempt succeeded
                        search_result_count=results_count,
                    )
                else:
                    logger.info(
                        f"Rate limiting disabled, not recording search for {self.engine_type}"
                    )

                return results

            except RateLimitError:
                # Only re-raise if rate limiting is enabled
                if self.rate_tracker.enabled:
                    raise
                else:
                    # If rate limiting is disabled, treat as regular error
                    success = False
                    error_message = "Rate limit hit but rate limiting disabled"
                    logger.warning(
                        f"Rate limit hit on {self.__class__.__name__} but rate limiting is disabled"
                    )
                    results_count = 0
                    return []
            except Exception as e:
                # Other errors - don't retry
                success = False
                error_message = str(e)
                logger.exception(
                    f"Search engine {self.__class__.__name__} failed"
                )
                results_count = 0
                return []

        try:
            return _run_with_retry()
        except RetryError as e:
            # All retries exhausted
            success = False
            error_message = f"Rate limited after all retries: {e}"
            logger.exception(
                f"{self.__class__.__name__} failed after all retries"
            )
            return []
        except Exception as e:
            success = False
            error_message = str(e)
            logger.exception(f"Search engine {self.__class__.__name__} error")
            return []
        finally:
            # Record search metrics (if tracking is available)
            if tracker is not None:
                response_time_ms = int((time.time() - start_time) * 1000)
                tracker.record_search(
                    engine_name=engine_name,
                    query=query,
                    results_count=results_count,
                    response_time_ms=response_time_ms,
                    success=success,
                    error_message=error_message,
                )

    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(UTC).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:
            logger.exception("Relevance filtering error")
            # 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
