import enum
import json
import time
from typing import Any, Dict, List, Optional

import requests
from langchain_core.language_models import BaseLLM
from loguru import logger

from ...config import search_config
from ...security.safe_requests import safe_get
from ..search_engine_base import BaseSearchEngine


@enum.unique
class SafeSearchSetting(enum.IntEnum):
    """
    Acceptable settings for safe search.
    """

    OFF = 0
    MODERATE = 1
    STRICT = 2


class SearXNGSearchEngine(BaseSearchEngine):
    """
    SearXNG search engine implementation that requires an instance URL provided via
    environment variable or configuration. Designed for ethical usage with proper
    rate limiting and single-instance approach.
    """

    # Mark as public search engine
    is_public = True
    # Mark as generic search engine (general web search)
    is_generic = True

    @staticmethod
    def _normalize_list(value):
        """Ensure *value* is a ``list[str]`` or ``None``.

        Settings saved via the web UI may arrive as raw JSON strings
        (e.g. ``'[\\r\\n  "general"\\r\\n]'``) instead of parsed lists.
        This helper decodes such strings so that ``",".join()`` later
        works on list items rather than individual characters (issue #1030).
        """
        if value is None:
            return None
        if isinstance(value, list):
            return value
        if isinstance(value, str):
            stripped = value.strip()
            if stripped:
                try:
                    parsed = json.loads(stripped)
                    if isinstance(parsed, list):
                        return [str(item) for item in parsed]
                except (json.JSONDecodeError, ValueError, RecursionError):
                    pass
                # Comma-separated fallback
                return [
                    item.strip() for item in stripped.split(",") if item.strip()
                ]
        return None

    def _is_valid_search_result(self, url: str) -> bool:
        """
        Check if a parsed result is a valid search result vs an error page.

        When SearXNG's backend engines fail or get rate-limited, it returns
        error/stats pages that shouldn't be treated as search results.

        Returns False for:
        - Relative URLs (don't start with http:// or https://, case-insensitive)
        - URLs pointing to the SearXNG instance itself (catches /stats, /preferences, etc.)
        """
        # Must have an absolute URL (case-insensitive scheme check)
        if not url or not url.lower().startswith(("http://", "https://")):
            return False

        # Reject URLs pointing back to the SearXNG instance itself
        # This catches all internal pages like /stats?engine=, /preferences, /about
        if url.startswith(self.instance_url):
            return False

        return True

    def __init__(
        self,
        max_results: int = 15,
        instance_url: str = "http://localhost:8080",
        categories: Optional[List[str]] = None,
        engines: Optional[List[str]] = None,
        language: str = "en",
        safe_search: str = SafeSearchSetting.OFF.name,
        time_range: Optional[str] = None,
        delay_between_requests: float = 0.0,
        llm: Optional[BaseLLM] = None,
        max_filtered_results: Optional[int] = None,
        include_full_content: bool = True,
        settings_snapshot: Optional[Dict[str, Any]] = None,
        **kwargs,
    ):  # API key is actually the instance URL
        """
        Initialize the SearXNG search engine with ethical usage patterns.

        Args:
            max_results: Maximum number of search results
            instance_url: URL of your SearXNG instance (preferably self-hosted)
            categories: List of SearXNG categories to search in (general, images, videos, news, etc.)
            engines: List of engines to use (google, bing, duckduckgo, etc.)
            language: Language code for search results
            safe_search: Safe search level (0=off, 1=moderate, 2=strict)
            time_range: Time range for results (day, week, month, year)
            delay_between_requests: Seconds to wait between requests
            llm: Language model for relevance filtering
            max_filtered_results: Maximum number of results to keep after filtering
            include_full_content: Whether to include full webpage content in results
        """

        # Initialize the BaseSearchEngine with LLM, max_filtered_results, and max_results
        super().__init__(
            llm=llm,
            max_filtered_results=max_filtered_results,
            max_results=max_results,
            include_full_content=include_full_content,
            settings_snapshot=settings_snapshot,
            **kwargs,  # Pass through all other kwargs including search_snippets_only
        )

        # Validate and normalize the instance URL if provided
        self.instance_url = instance_url.rstrip("/")
        logger.info(
            f"SearXNG initialized with instance URL: {self.instance_url}"
        )
        try:
            # Make sure it's accessible.
            # allow_private_ips=True since SearXNG is typically self-hosted on local network
            response = safe_get(
                self.instance_url, timeout=5, allow_private_ips=True
            )
            if response.status_code == 200:
                logger.info("SearXNG instance is accessible.")
                self.is_available = True
            else:
                self.is_available = False
                logger.error(
                    f"Failed to access SearXNG instance at {self.instance_url}. Status code: {response.status_code}"
                )
        except requests.RequestException:
            self.is_available = False
            logger.exception(
                f"Error while trying to access SearXNG instance at {self.instance_url}"
            )

        # Add debug logging for all parameters
        logger.info(
            f"SearXNG init params: max_results={max_results}, language={language}, "
            f"max_filtered_results={max_filtered_results}, is_available={self.is_available}"
        )

        self.max_results = max_results
        self.categories = self._normalize_list(categories) or ["general"]
        self.engines = self._normalize_list(engines)
        self.language = language
        try:
            # Handle both string names and integer values
            if isinstance(safe_search, int) or (
                isinstance(safe_search, str) and str(safe_search).isdigit()
            ):
                self.safe_search = SafeSearchSetting(int(safe_search))
            else:
                self.safe_search = SafeSearchSetting[safe_search]
        except (ValueError, KeyError):
            logger.exception(
                "'{}' is not a valid safe search setting. Disabling safe search",
                safe_search,
            )
            self.safe_search = SafeSearchSetting.OFF
        self.time_range = time_range

        self.delay_between_requests = float(delay_between_requests)

        if self.is_available:
            self.search_url = f"{self.instance_url}/search"
            logger.info(
                f"SearXNG engine initialized with instance: {self.instance_url}"
            )
            logger.info(
                f"Rate limiting set to {self.delay_between_requests} seconds between requests"
            )

            self._init_full_search(
                web_search=self,
                language=language,
                max_results=max_results,
                region="wt-wt",
                time_period="y",
                safe_search=self.safe_search.value,
            )

        self.last_request_time: float = 0.0

    def _respect_rate_limit(self):
        """Apply self-imposed rate limiting between requests"""
        current_time = time.time()
        time_since_last_request = current_time - self.last_request_time

        if time_since_last_request < self.delay_between_requests:
            wait_time = self.delay_between_requests - time_since_last_request
            logger.info(f"Rate limiting: waiting {wait_time:.2f} seconds")
            time.sleep(wait_time)

        self.last_request_time = time.time()

    def _get_search_results(self, query: str) -> List[Dict[str, Any]]:
        """
        Get search results from SearXNG with ethical rate limiting.

        Args:
            query: The search query

        Returns:
            List of search results from SearXNG
        """
        if not self.is_available:
            logger.error(
                "SearXNG engine is disabled (no instance URL provided) - cannot run search"
            )
            return []

        logger.info(f"SearXNG running search for query: {query}")

        try:
            self._respect_rate_limit()

            initial_headers = {
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
                "Accept-Language": "en-US,en;q=0.9",
            }

            try:
                initial_response = safe_get(
                    self.instance_url,
                    headers=initial_headers,
                    timeout=10,
                    allow_private_ips=True,
                )
                cookies = initial_response.cookies
            except Exception:
                logger.exception("Failed to get initial cookies")
                cookies = None

            params = {
                "q": query,
                "categories": ",".join(self.categories),
                "language": self.language,
                "format": "html",  # Use HTML format instead of JSON
                "pageno": 1,
                "safesearch": self.safe_search.value,
                "count": self.max_results,
            }

            if self.engines:
                params["engines"] = ",".join(self.engines)

            if self.time_range:
                params["time_range"] = self.time_range

            # Browser-like headers
            headers = {
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
                "Accept-Language": "en-US,en;q=0.9",
                "Referer": self.instance_url + "/",
                "Connection": "keep-alive",
                "Upgrade-Insecure-Requests": "1",
            }

            logger.info(
                f"Sending request to SearXNG instance at {self.instance_url}"
            )
            response = safe_get(
                self.search_url,
                params=params,
                headers=headers,
                cookies=cookies,
                timeout=15,
                allow_private_ips=True,
            )

            if response.status_code == 200:
                try:
                    from bs4 import BeautifulSoup

                    soup = BeautifulSoup(response.text, "html.parser")
                    results = []

                    result_elements = soup.select(".result-item")

                    if not result_elements:
                        result_elements = soup.select(".result")

                    if not result_elements:
                        result_elements = soup.select("article")

                    if not result_elements:
                        logger.debug(
                            f"Classes found in HTML: {[c['class'] for c in soup.select('[class]') if 'class' in c.attrs][:10]}"
                        )
                        result_elements = soup.select('div[id^="result"]')

                    logger.info(
                        f"Found {len(result_elements)} search result elements"
                    )

                    for idx, result_element in enumerate(result_elements):
                        if idx >= self.max_results:
                            break

                        title_element = (
                            result_element.select_one(".result-title")
                            or result_element.select_one(".title")
                            or result_element.select_one("h3")
                            or result_element.select_one("a[href]")
                        )

                        url_element = (
                            result_element.select_one(".result-url")
                            or result_element.select_one(".url")
                            or result_element.select_one("a[href]")
                        )

                        content_element = (
                            result_element.select_one(".result-content")
                            or result_element.select_one(".content")
                            or result_element.select_one(".snippet")
                            or result_element.select_one("p")
                        )

                        title = (
                            title_element.get_text(strip=True)
                            if title_element
                            else ""
                        )

                        url = ""
                        if url_element and url_element.has_attr("href"):
                            url = str(url_element["href"])
                        elif url_element:
                            url = url_element.get_text(strip=True)

                        content = (
                            content_element.get_text(strip=True)
                            if content_element
                            else ""
                        )

                        if (
                            not url
                            and title_element
                            and title_element.has_attr("href")
                        ):
                            url = str(title_element["href"])

                        logger.debug(
                            f"Extracted result {idx}: title={title[:30]}..., url={url[:30]}..., content={content[:30]}..."
                        )

                        # Add to results only if it's a valid search result
                        # (not an error page or internal SearXNG page)
                        if self._is_valid_search_result(url):
                            results.append(
                                {
                                    "title": title,
                                    "url": url,
                                    "content": content,
                                    "engine": "searxng",
                                    "category": "general",
                                }
                            )
                        else:
                            # Check if this is a backend engine failure
                            if url and "/stats?engine=" in url:
                                try:
                                    engine_name = url.split("/stats?engine=")[
                                        1
                                    ].split("&")[0]
                                    logger.warning(
                                        f"SearXNG backend engine failed or rate-limited: {engine_name}"
                                    )
                                except (IndexError, AttributeError):
                                    pass  # Couldn't parse engine name
                            logger.debug(
                                f"Filtered invalid SearXNG result: title={title!r}, url={url!r}"
                            )

                    if results:
                        logger.info(
                            f"SearXNG returned {len(results)} valid results from HTML parsing"
                        )
                    else:
                        logger.warning(
                            f"SearXNG returned no valid results for query: {query}. "
                            "This may indicate SearXNG backend engine issues or rate limiting."
                        )
                    return results

                except ImportError:
                    logger.exception(
                        "BeautifulSoup not available for HTML parsing"
                    )
                    return []
                except Exception:
                    logger.exception("Error parsing HTML results")
                    return []
            else:
                logger.error(
                    f"SearXNG returned status code {response.status_code}"
                )
                return []

        except Exception:
            logger.exception("Error getting SearXNG results")
            return []

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

        Args:
            query: The search query

        Returns:
            List of preview dictionaries
        """
        if not self.is_available:
            logger.warning(
                "SearXNG engine is disabled (no instance URL provided)"
            )
            return []

        logger.info(f"Getting SearXNG previews for query: {query}")

        results = self._get_search_results(query)

        if not results:
            logger.warning(f"No SearXNG results found for query: {query}")
            return []

        previews = []
        for i, result in enumerate(results):
            title = result.get("title", "")
            url = result.get("url", "")
            content = result.get("content", "")

            preview = {
                "id": url or f"searxng-result-{i}",
                "title": title,
                "link": url,
                "snippet": content,
                "engine": result.get("engine", ""),
                "category": result.get("category", ""),
            }

            previews.append(preview)

        return previews

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

        Args:
            relevant_items: List of relevant preview dictionaries

        Returns:
            List of result dictionaries with full content
        """
        if not self.is_available:
            return relevant_items

        if (
            hasattr(search_config, "SEARCH_SNIPPETS_ONLY")
            and search_config.SEARCH_SNIPPETS_ONLY
        ):
            logger.info("Snippet-only mode, skipping full content retrieval")
            return relevant_items

        if not hasattr(self, "full_search"):
            return relevant_items

        logger.info("Retrieving full webpage content")

        try:
            return self.full_search._get_full_content(relevant_items)

        except Exception:
            logger.exception("Error retrieving full content")
            return relevant_items

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

    def results(
        self, query: str, max_results: Optional[int] = None
    ) -> List[Dict[str, Any]]:
        """
        Get search results in a format compatible with other search engines.

        Args:
            query: The search query
            max_results: Optional override for maximum results

        Returns:
            List of search result dictionaries
        """
        if not self.is_available:
            return []

        original_max_results = self.max_results

        try:
            if max_results is not None:
                self.max_results = max_results

            results = self._get_search_results(query)

            formatted_results = []
            for result in results:
                formatted_results.append(
                    {
                        "title": result.get("title", ""),
                        "link": result.get("url", ""),
                        "snippet": result.get("content", ""),
                    }
                )

            return formatted_results

        finally:
            self.max_results = original_max_results

    @staticmethod
    def get_self_hosting_instructions() -> str:
        """
        Get instructions for self-hosting a SearXNG instance.

        Returns:
            String with installation instructions
        """
        return """
# SearXNG Self-Hosting Instructions

The most ethical way to use SearXNG is to host your own instance. Here's how:

## Using Docker (easiest method)

1. Install Docker if you don't have it already
2. Run these commands:

```bash
# Pull the SearXNG Docker image
docker pull searxng/searxng

# Run SearXNG (will be available at http://localhost:8080)
docker run -d -p 8080:8080 --name searxng searxng/searxng
```

## Using Docker Compose (recommended for production)

1. Create a file named `docker-compose.yml` with the following content:

```yaml
version: '3'
services:
  searxng:
    container_name: searxng
    image: searxng/searxng
    ports:
      - "8080:8080"
    volumes:
      - ./searxng:/etc/searxng
    environment:
      - SEARXNG_BASE_URL=http://localhost:8080/
    restart: unless-stopped
```

2. Run with Docker Compose:

```bash
docker-compose up -d
```

For more detailed instructions and configuration options, visit:
https://searxng.github.io/searxng/admin/installation.html
"""

    def run(
        self, query: str, research_context: Dict[str, Any] | None = None
    ) -> List[Dict[str, Any]]:
        """
        Override BaseSearchEngine run method to add SearXNG-specific error handling.
        """
        if not self.is_available:
            logger.error(
                "SearXNG run method called but engine is not available (missing instance URL)"
            )
            return []

        logger.info(f"SearXNG instance URL: {self.instance_url}")

        try:
            # Call the parent class's run method
            results = super().run(query, research_context=research_context)
            logger.info(f"SearXNG search completed with {len(results)} results")
            return results
        except Exception:
            logger.exception("Error in SearXNG run method")
            # Return empty results on error
            return []
