from typing import Any, Dict, List, Optional

from loguru import logger

from ...config.search_config import get_setting_from_snapshot
from ...web.services.socket_service import SocketIOService
from ..search_engine_base import BaseSearchEngine
from ..search_engine_factory import create_search_engine
from ..search_engines_config import get_available_engines
from .search_engine_wikipedia import WikipediaSearchEngine


class MetaSearchEngine(BaseSearchEngine):
    """
    LLM-powered meta search engine that intelligently selects and uses
    the appropriate search engines based on query analysis
    """

    def __init__(
        self,
        llm,
        max_results: int = 10,
        use_api_key_services: bool = True,
        max_engines_to_try: int = 3,
        max_filtered_results: Optional[int] = None,
        _engine_selection_callback=None,
        settings_snapshot: Optional[Dict[str, Any]] = None,
        programmatic_mode: bool = False,
        **kwargs,
    ):
        """
        Initialize the meta search engine.

        Args:
            llm: Language model instance for query classification and relevance filtering
            max_results: Maximum number of search results to return
            use_api_key_services: Whether to include services that require API keys
            max_engines_to_try: Maximum number of engines to try before giving up
            max_filtered_results: Maximum number of results to keep after filtering
            settings_snapshot: Settings snapshot for thread context
            programmatic_mode: If True, disables database operations and metrics tracking
            **kwargs: Additional parameters (ignored but accepted for compatibility)
        """
        # Initialize the BaseSearchEngine with the LLM, max_filtered_results, and max_results
        super().__init__(
            llm=llm,
            max_filtered_results=max_filtered_results,
            max_results=max_results,
            settings_snapshot=settings_snapshot,
            programmatic_mode=programmatic_mode,
        )

        self.use_api_key_services = use_api_key_services
        self.max_engines_to_try = max_engines_to_try
        self.settings_snapshot = settings_snapshot or {}

        # Cache for engine instances
        self.engine_cache: Dict[str, Any] = {}

        # Get available engines (excluding 'meta' and 'auto')
        self.available_engines = self._get_available_engines()
        logger.info(
            f"Meta Search Engine initialized with {len(self.available_engines)} available engines: {', '.join(self.available_engines)}"
        )

        # Create a fallback engine in case everything else fails
        self.fallback_engine = WikipediaSearchEngine(
            max_results=self.max_results,
            llm=llm,
            max_filtered_results=max_filtered_results,
        )

    def _get_search_config(self) -> Dict[str, Any]:
        """Get search config for available engines (used for reliability/strengths lookups)."""
        return get_available_engines(
            settings_snapshot=self.settings_snapshot,
            use_api_key_services=self.use_api_key_services,
        )

    def _get_available_engines(self) -> List[str]:
        """Get list of available engines based on user settings."""
        available = get_available_engines(
            settings_snapshot=self.settings_snapshot,
            use_api_key_services=self.use_api_key_services,
        )

        if not available:
            error_msg = "No search engines enabled for auto search. Please enable at least one engine in settings."
            logger.error(error_msg)
            raise RuntimeError(error_msg)

        return list(available.keys())

    def analyze_query(self, query: str) -> List[str]:
        """
        Analyze the query to determine the best search engines to use.
        Prioritizes SearXNG for general queries, but selects specialized engines
        for domain-specific queries (e.g., scientific papers, code).

        Args:
            query: The search query

        Returns:
            List of search engine names sorted by suitability
        """
        try:
            # First check if this is a specialized query that should use specific engines
            specialized_domains = {
                "scientific paper": ["arxiv", "pubmed", "wikipedia"],
                "medical research": ["pubmed", "searxng"],
                "clinical": ["pubmed", "searxng"],
                "github": ["github", "searxng"],
                "repository": ["github", "searxng"],
                "code": ["github", "searxng"],
                "programming": ["github", "searxng"],
            }

            # Quick heuristic check for specialized queries
            query_lower = query.lower()
            for term, engines in specialized_domains.items():
                if term in query_lower:
                    valid_engines = []
                    for engine in engines:
                        if engine in self.available_engines:
                            valid_engines.append(engine)

                    if valid_engines:
                        logger.info(
                            f"Detected specialized query type: {term}, using engines: {valid_engines}"
                        )
                        return valid_engines

            # For searches containing "arxiv", prioritize the arxiv engine
            if "arxiv" in query_lower and "arxiv" in self.available_engines:
                return ["arxiv"] + [
                    e for e in self.available_engines if e != "arxiv"
                ]

            # For searches containing "pubmed", prioritize the pubmed engine
            if "pubmed" in query_lower and "pubmed" in self.available_engines:
                return ["pubmed"] + [
                    e for e in self.available_engines if e != "pubmed"
                ]

            # Check if SearXNG is available and prioritize it for general queries
            if "searxng" in self.available_engines:
                # For general queries, return SearXNG first followed by reliability-ordered engines
                engines_without_searxng = [
                    e for e in self.available_engines if e != "searxng"
                ]
                reliability_sorted = sorted(
                    engines_without_searxng,
                    key=lambda x: (
                        self._get_search_config()
                        .get(x, {})
                        .get("reliability", 0)
                    ),
                    reverse=True,
                )
                return ["searxng"] + reliability_sorted

            # If LLM is not available or SearXNG is not available, fall back to reliability
            if not self.llm or "searxng" not in self.available_engines:
                logger.warning(
                    "No LLM available or SearXNG not available, using reliability-based engines"
                )
                # Return engines sorted by reliability
                return sorted(
                    self.available_engines,
                    key=lambda x: (
                        self._get_search_config()
                        .get(x, {})
                        .get("reliability", 0)
                    ),
                    reverse=True,
                )

            # Create a prompt that outlines the available search engines and their strengths
            engines_info = []
            for engine_name in self.available_engines:
                try:
                    if engine_name in self._get_search_config():
                        strengths = self._get_search_config()[engine_name].get(
                            "strengths", "General search"
                        )
                        weaknesses = self._get_search_config()[engine_name].get(
                            "weaknesses", "None specified"
                        )
                        description = self._get_search_config()[
                            engine_name
                        ].get("description", engine_name)
                        engines_info.append(
                            f"- {engine_name}: {description}\n  Strengths: {strengths}\n  Weaknesses: {weaknesses}"
                        )
                except KeyError:
                    logger.exception(f"Missing key for engine {engine_name}")

            # Only proceed if we have engines available to choose from
            if not engines_info:
                logger.warning(
                    "No engine information available for prompt, using reliability-based sorting instead"
                )
                return sorted(
                    self.available_engines,
                    key=lambda x: (
                        self._get_search_config()
                        .get(x, {})
                        .get("reliability", 0)
                    ),
                    reverse=True,
                )

            # Use a stronger prompt that emphasizes SearXNG preference for general queries
            prompt = f"""You are a search query analyst. Consider this search query:

QUERY: {query}

I have these search engines available:
{chr(10).join(engines_info)}

Determine which search engines would be most appropriate for answering this query.
First analyze the nature of the query: Is it factual, scientific, code-related, medical, etc.?

IMPORTANT GUIDELINES:
- Use SearXNG for most general queries as it combines results from multiple search engines
- For academic/scientific searches, prefer arXiv
- For medical research, prefer PubMed
- For code repositories and programming, prefer GitHub
- For every other query type, SearXNG is usually the best option

Output ONLY a comma-separated list of 1-3 search engine names in order of most appropriate to least appropriate.
Example output: searxng,wikipedia,brave"""

            # Get analysis from LLM
            response = self.llm.invoke(prompt)

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

            # Extract engine names
            valid_engines = []
            for engine_name in content.split(","):
                cleaned_name = engine_name.strip().lower()
                if cleaned_name in self.available_engines:
                    valid_engines.append(cleaned_name)

            # If SearXNG is available but not selected by the LLM, add it as a fallback
            if (
                "searxng" in self.available_engines
                and "searxng" not in valid_engines
            ):
                # Add it as the last option if the LLM selected others
                if valid_engines:
                    valid_engines.append("searxng")
                # Use it as the first option if no valid engines were selected
                else:
                    valid_engines = ["searxng"]

            # If still no valid engines, use reliability-based ordering
            if not valid_engines:
                valid_engines = sorted(
                    self.available_engines,
                    key=lambda x: (
                        self._get_search_config()
                        .get(x, {})
                        .get("reliability", 0)
                    ),
                    reverse=True,
                )

            return valid_engines
        except Exception:
            logger.exception("Error analyzing query with LLM")
            # Fall back to SearXNG if available, then reliability-based ordering
            if "searxng" in self.available_engines:
                return ["searxng"] + sorted(
                    [e for e in self.available_engines if e != "searxng"],
                    key=lambda x: (
                        self._get_search_config()
                        .get(x, {})
                        .get("reliability", 0)
                    ),
                    reverse=True,
                )
            return sorted(
                self.available_engines,
                key=lambda x: (
                    self._get_search_config().get(x, {}).get("reliability", 0)
                ),
                reverse=True,
            )

    def _get_previews(self, query: str) -> List[Dict[str, Any]]:
        """
        Get preview information by selecting the best search engine for this query.

        Args:
            query: The search query

        Returns:
            List of preview dictionaries
        """
        # Get ranked list of engines for this query
        ranked_engines = self.analyze_query(query)

        if not ranked_engines:
            logger.warning(
                "No suitable search engines found for query, using fallback engine"
            )
            return self.fallback_engine._get_previews(query)

        # Limit the number of engines to try
        engines_to_try = ranked_engines[: self.max_engines_to_try]
        logger.info(
            f"SEARCH_PLAN: Will try these engines in order: {', '.join(engines_to_try)}"
        )

        all_errors = []
        # Try each engine in order
        for engine_name in engines_to_try:
            logger.info(f"Trying search engine: {engine_name}")

            # Get or create the engine instance
            engine = self._get_engine_instance(engine_name)

            if not engine:
                logger.warning(f"Failed to initialize {engine_name}, skipping")
                all_errors.append(f"Failed to initialize {engine_name}")
                continue

            try:
                # Get previews from this engine
                previews = engine._get_previews(query)

                # If search was successful, return results
                if previews and len(previews) > 0:
                    logger.info(f"ENGINE_SELECTED: {engine_name}")
                    logger.info(
                        f"Successfully got {len(previews)} preview results from {engine_name}"
                    )
                    # Store selected engine for later use
                    self._selected_engine = engine
                    self._selected_engine_name = engine_name

                    # Emit a socket event to inform about the selected engine
                    try:
                        SocketIOService().emit_socket_event(
                            "search_engine_selected",
                            {
                                "engine": engine_name,
                                "result_count": len(previews),
                            },
                        )
                    except Exception:
                        logger.exception("Socket emit error (non-critical)")

                    return previews

                logger.info(f"{engine_name} returned no previews")
                all_errors.append(f"{engine_name} returned no previews")

            except Exception as e:
                error_msg = f"Error getting previews from {engine_name}: {e!s}"
                logger.exception(error_msg)
                all_errors.append(error_msg)

        # If we reach here, all engines failed, use fallback
        logger.warning(
            f"All engines failed or returned no preview results: {', '.join(all_errors)}"
        )
        logger.info("Using fallback Wikipedia engine for previews")
        self._selected_engine = self.fallback_engine
        self._selected_engine_name = "wikipedia"
        return self.fallback_engine._get_previews(query)

    def _get_full_content(
        self, relevant_items: List[Dict[str, Any]]
    ) -> List[Dict[str, Any]]:
        """
        Get full content using the engine that provided the previews.

        Args:
            relevant_items: List of relevant preview dictionaries

        Returns:
            List of result dictionaries with full content
        """
        # Check if we should get full content
        if get_setting_from_snapshot(
            "search.snippets_only",
            True,
            settings_snapshot=self.settings_snapshot,
        ):
            logger.info("Snippet-only mode, skipping full content retrieval")
            return relevant_items

        logger.info("Getting full content for relevant items")

        # Use the selected engine to get full content
        if hasattr(self, "_selected_engine"):
            try:
                logger.info(
                    f"Using {self._selected_engine_name} to get full content"
                )
                return self._selected_engine._get_full_content(relevant_items)
            except Exception:
                logger.exception(
                    f"Error getting full content from {self._selected_engine_name}"
                )
                # Fall back to returning relevant items without full content
                return relevant_items
        else:
            logger.warning(
                "No engine was selected during preview phase, returning relevant items as-is"
            )
            return relevant_items

    def _get_engine_instance(
        self, engine_name: str
    ) -> Optional[BaseSearchEngine]:
        """Get or create an instance of the specified search engine"""
        # Return cached instance if available
        if engine_name in self.engine_cache:
            return self.engine_cache[engine_name]  # type: ignore[no-any-return]

        # Create a new instance
        engine = None
        try:
            # Only pass parameters that all engines accept
            common_params = {"llm": self.llm, "max_results": self.max_results}

            # Add max_filtered_results if specified
            if self.max_filtered_results is not None:
                common_params["max_filtered_results"] = (
                    self.max_filtered_results
                )

            engine = create_search_engine(
                engine_name,
                settings_snapshot=self.settings_snapshot,
                programmatic_mode=self.programmatic_mode,
                **common_params,  # type: ignore[arg-type]
            )
        except Exception:
            logger.exception(
                f"Error creating engine instance for {engine_name}"
            )
            return None

        if engine:
            # Cache the instance
            self.engine_cache[engine_name] = engine

        return engine

    def close(self):
        """Close all cached child search engines, fallback engine, and own resources."""
        from ...utilities.resource_utils import safe_close

        for engine in self.engine_cache.values():
            safe_close(engine, "child search engine")
        self.engine_cache.clear()
        if (
            hasattr(self, "fallback_engine")
            and self.fallback_engine is not None
        ):
            safe_close(self.fallback_engine, "fallback search engine")
        super().close()

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