from loguru import logger
import random
import time
from typing import Any, Dict, List, Optional

from langchain_core.language_models import BaseLLM
from requests.exceptions import RequestException

from ...security.safe_requests import safe_get
from ..rate_limiting import RateLimitError
from ..search_engine_base import BaseSearchEngine


class GooglePSESearchEngine(BaseSearchEngine):
    """Google Programmable Search Engine implementation"""

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

    def __init__(
        self,
        max_results: int = 10,
        region: str = "us",
        safe_search: bool = True,
        search_language: str = "English",
        api_key: Optional[str] = None,
        search_engine_id: Optional[str] = None,
        llm: Optional[BaseLLM] = None,
        include_full_content: bool = False,
        max_filtered_results: Optional[int] = None,
        settings_snapshot: Optional[Dict[str, Any]] = None,
        max_retries: int = 3,
        retry_delay: float = 2.0,
        **kwargs,
    ):
        """
        Initialize the Google Programmable Search Engine.

        Args:
            max_results: Maximum number of search results
            region: Region code for search results
            safe_search: Whether to enable safe search
            search_language: Language for search results
            api_key: Google API key (can also be set via LDR_SEARCH_ENGINE_WEB_GOOGLE_PSE_API_KEY env var or in UI settings)
            search_engine_id: Google CSE ID (can also be set via LDR_SEARCH_ENGINE_WEB_GOOGLE_PSE_ENGINE_ID env var or in UI settings)
            llm: Language model for relevance filtering
            include_full_content: Whether to include full webpage content in results
            max_filtered_results: Maximum number of results to keep after filtering
            max_retries: Maximum number of retry attempts for API requests
            retry_delay: Base delay in seconds between retry attempts
            **kwargs: Additional parameters (ignored but accepted for compatibility)
        """
        # 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,
        )

        # Google PSE returns full content via its API (snippet + htmlSnippet),
        # so _init_full_search() is intentionally not called here.

        # Retry configuration
        self.max_retries = max_retries
        self.retry_delay = retry_delay

        # Rate limiting - keep track of last request time
        self.last_request_time: float = 0.0
        self.min_request_interval = (
            0.5  # Minimum time between requests in seconds
        )

        # Language code mapping — Google PSE uses "zh-CN" for Chinese
        from ...utilities.search_utilities import LANGUAGE_CODE_MAP

        language_code_mapping = {**LANGUAGE_CODE_MAP, "chinese": "zh-CN"}

        # Get language code
        search_language = search_language.lower()
        self.language = language_code_mapping.get(search_language, "en")

        # Safe search setting
        self.safe = "active" if safe_search else "off"

        # Region/Country setting
        self.region = region

        # API key and Search Engine ID - check params, env vars, or database
        from ...config.thread_settings import (
            get_setting_from_snapshot,
            NoSettingsContextError,
        )

        self.api_key = api_key
        if not self.api_key:
            try:
                self.api_key = get_setting_from_snapshot(
                    "search.engine.web.google_pse.api_key",
                    default=None,
                    settings_snapshot=self.settings_snapshot,
                )
            except NoSettingsContextError:
                # No settings context available
                logger.debug(
                    "No settings context available for Google PSE API key"
                )
                pass

        self.search_engine_id = search_engine_id
        if not self.search_engine_id:
            try:
                self.search_engine_id = get_setting_from_snapshot(
                    "search.engine.web.google_pse.engine_id",
                    default=None,
                    settings_snapshot=self.settings_snapshot,
                )
            except NoSettingsContextError:
                # No settings context available
                logger.debug(
                    "No settings context available for Google PSE engine ID"
                )
                pass

        if not self.api_key:
            raise ValueError(
                "Google API key is required. Set it in the UI settings, use the api_key parameter, or set the LDR_SEARCH_ENGINE_WEB_GOOGLE_PSE_API_KEY environment variable."
            )
        if not self.search_engine_id:
            raise ValueError(
                "Google Search Engine ID is required. Set it in the UI settings, use the search_engine_id parameter, or set the LDR_SEARCH_ENGINE_WEB_GOOGLE_PSE_ENGINE_ID environment variable."
            )

        # Validate connection and credentials
        self._validate_connection()

    def _validate_connection(self):
        """Test the connection to ensure API key and Search Engine ID are valid"""
        try:
            # Make a minimal test query
            response = self._make_request("test")

            # Check if we got a valid response
            if response.get("error"):
                error_msg = response["error"].get("message", "Unknown error")
                raise ValueError(f"Google PSE API error: {error_msg}")  # noqa: TRY301 — except only adds logging before re-raise

            # If we get here, the connection is valid
            logger.info("Google PSE connection validated successfully")
            return True

        except Exception as e:
            # Log the error and re-raise a sanitized exception. Use
            # logger.warning with the api_key redacted from str(e) so
            # the upstream exception message — which can embed the key
            # in the URL — does not leak. The re-raised exception uses
            # `type(e)(safe_msg) from None` to preserve the original
            # exception type (for callers that dispatch on it) while
            # replacing the args with the redacted message and
            # suppressing the traceback chain (which carries the URL
            # in earlier frames).
            safe_msg = self._scrub_error(e)
            logger.warning(
                f"Error validating Google PSE connection: {safe_msg}"
            )
            raise type(e)(safe_msg) from None

    def _respect_rate_limit(self):
        """Ensure we don't exceed rate limits by adding appropriate delay between requests"""
        current_time = time.time()
        elapsed = current_time - self.last_request_time

        # If we've made a request recently, wait until the minimum interval has passed
        if elapsed < self.min_request_interval:
            sleep_time = self.min_request_interval - elapsed
            logger.debug("Rate limiting: sleeping for {:.2f} s", sleep_time)
            time.sleep(sleep_time)

        # Update the last request time
        self.last_request_time = time.time()

    def _make_request(self, query: str, start_index: int = 1) -> Dict:
        """
        Make a request to the Google PSE API with retry logic and rate limiting

        Args:
            query: Search query string
            start_index: Starting index for pagination

        Returns:
            JSON response from the API

        Raises:
            RequestException: If all retry attempts fail
        """
        # Base URL for the API
        url = "https://www.googleapis.com/customsearch/v1"

        # Parameters for the request
        params = {
            "key": self.api_key,
            "cx": self.search_engine_id,
            "q": query,
            "num": min(10, self.max_results),  # Max 10 per request
            "start": start_index,
            "safe": self.safe,
            "lr": f"lang_{self.language}",
            "gl": self.region,
        }

        # Implement retry logic with exponential backoff
        attempt = 0
        last_exception: Exception | None = None

        while attempt < self.max_retries:
            try:
                # Add jitter to retries after the first attempt
                if attempt > 0:
                    # Security: random jitter for exponential backoff retry, not security-sensitive
                    jitter = random.uniform(0.5, 1.5)
                    sleep_time = (
                        self.retry_delay * (2 ** (attempt - 1)) * jitter
                    )
                    logger.info(
                        "Retry attempt {} / {} for query '{}'. Waiting {} s",
                        attempt + 1,
                        self.max_retries,
                        query,
                        f"{sleep_time:.2f}",
                    )
                    time.sleep(sleep_time)

                # Make the request
                logger.debug(
                    "Making request to Google PSE API: {} (start_index={})",
                    query,
                    start_index,
                )
                # Apply rate limiting before request
                self._last_wait_time = self.rate_tracker.apply_rate_limit(
                    self.engine_type
                )

                response = safe_get(url, params=params, timeout=10)

                # Check for HTTP errors
                response.raise_for_status()

                # Return the JSON response
                return response.json()  # type: ignore[no-any-return]

            except RequestException as e:
                error_msg = str(e)
                sanitized = self._sanitize_error_message(error_msg)
                logger.warning(
                    "Request error on attempt {} / {}: {}",
                    attempt + 1,
                    self.max_retries,
                    sanitized,
                )

                # Check for rate limiting patterns
                if (
                    "quota" in error_msg.lower()
                    or "quotaExceeded" in error_msg
                    or "dailyLimitExceeded" in error_msg
                    or "rateLimitExceeded" in error_msg
                    or "429" in error_msg
                    or "403" in error_msg
                ):
                    raise RateLimitError(
                        f"Google PSE rate limit/quota exceeded: {sanitized}"
                    )

                last_exception = e
            except Exception as e:
                error_msg = str(e)
                sanitized = self._sanitize_error_message(error_msg)
                logger.warning(
                    "Error on attempt {} / {}: {}",
                    attempt + 1,
                    self.max_retries,
                    sanitized,
                )

                # Check for rate limiting patterns in general errors
                if "quota" in error_msg.lower() or "limit" in error_msg.lower():
                    raise RateLimitError(
                        f"Google PSE error (possible rate limit): {sanitized}"
                    )

                last_exception = e

            attempt += 1

        # If we get here, all retries failed
        error_msg = f"Failed to get response from Google PSE API after {self.max_retries} attempts"
        logger.error(error_msg)

        if last_exception:
            raise RequestException(
                f"{error_msg}: {self._sanitize_error_message(str(last_exception))}"
            )
        raise RequestException(error_msg)

    def _get_previews(self, query: str) -> List[Dict[str, Any]]:
        """Get search result previews/snippets"""
        results = []

        # Google PSE API returns a maximum of 10 results per request
        # We may need to make multiple requests to get the desired number
        start_index = 1
        total_results = 0

        while total_results < self.max_results:
            try:
                response = self._make_request(query, start_index)

                # Break if no items
                if "items" not in response:
                    break

                items = response.get("items", [])

                # Process each result
                for item in items:
                    title = item.get("title", "")
                    snippet = item.get("snippet", "")
                    url = item.get("link", "")

                    # Skip results without URL
                    if not url:
                        continue

                    results.append(
                        {
                            "title": title,
                            "snippet": snippet,
                            "link": url,
                            "source": "Google Programmable Search",
                        }
                    )

                    total_results += 1
                    if total_results >= self.max_results:
                        break

                # Check if there are more results
                if not items or total_results >= self.max_results:
                    break

                # Update start index for next request
                start_index += len(items)

            except Exception as e:
                safe_msg = self._scrub_error(e)
                logger.warning(f"Error getting search results: {safe_msg}")
                break

        logger.info(
            "Retrieved {} search results for query: '{}'", len(results), query
        )
        return results
