from typing import Any, Dict, List, Optional
from urllib.parse import urlparse

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

from ...security import safe_get, safe_post
from ..rate_limiting import RateLimitError
from ..search_engine_base import BaseSearchEngine


class TinyFishSearchEngine(BaseSearchEngine):
    """TinyFish Search + Fetch API search engine implementation."""

    is_public = True
    is_generic = True

    SEARCH_URL = "https://api.search.tinyfish.ai"
    FETCH_URL = "https://api.fetch.tinyfish.ai"
    SEARCH_TIMEOUT = 10
    FETCH_TIMEOUT = 150
    MAX_QUERY_LEN = 400
    MAX_FETCH_URLS = 10

    def __init__(
        self,
        max_results: int = 10,
        location: str = "US",
        language: str = "en",
        api_key: Optional[str] = None,
        llm: Optional[BaseLLM] = None,
        include_full_content: bool = True,
        fetch_format: str = "markdown",
        max_filtered_results: Optional[int] = None,
        search_snippets_only: Optional[bool] = None,
        settings_snapshot: Optional[Dict[str, Any]] = None,
        **kwargs,
    ):
        """
        Initialize the TinyFish search engine.

        Args:
            max_results: Maximum number of search results
            location: Country code for geo-targeted search results
            language: Language code for search results
            api_key: TinyFish API key
            llm: Language model for relevance filtering
            include_full_content: Whether to fetch extracted page content
            fetch_format: TinyFish Fetch output format: markdown, html, or json
            max_filtered_results: Maximum results to keep after filtering
            search_snippets_only: Whether to return only search snippets
            settings_snapshot: Settings snapshot for thread context
            **kwargs: Additional parameters ignored for compatibility
        """
        if search_snippets_only is None:
            search_snippets_only = not include_full_content

        super().__init__(
            llm=llm,
            max_filtered_results=max_filtered_results,
            max_results=max_results,
            include_full_content=include_full_content,
            search_snippets_only=search_snippets_only,
            settings_snapshot=settings_snapshot,
        )
        self.location = self._normalize_location(location)
        self.language = self._normalize_language(language)
        self.fetch_format = fetch_format
        self.api_key = self._resolve_api_key(
            api_key,
            "search.engine.web.tinyfish.api_key",
            engine_name="TinyFish",
            settings_snapshot=settings_snapshot,
        )

    @staticmethod
    def _normalize_location(location: str) -> str:
        return (location or "US").strip().upper()

    @staticmethod
    def _normalize_language(language: str) -> str:
        language_value = (language or "en").strip()
        language_map = {
            "english": "en",
            "spanish": "es",
            "french": "fr",
            "german": "de",
            "italian": "it",
            "portuguese": "pt",
            "japanese": "ja",
            "chinese": "zh",
            "korean": "ko",
        }
        return language_map.get(language_value.lower(), language_value)

    def _headers(self) -> Dict[str, str]:
        return {"X-API-Key": self.api_key}

    def _raise_request_exception_if_rate_limited(
        self, error: requests.exceptions.RequestException
    ) -> None:
        response = getattr(error, "response", None)
        status_code = getattr(response, "status_code", None)
        if status_code is not None:
            self._raise_if_rate_limit(status_code)

    def _get_previews(self, query: str) -> List[Dict[str, Any]]:
        """
        Get preview information from TinyFish Search.

        Args:
            query: The search query

        Returns:
            List of preview dictionaries
        """
        logger.info("Getting search results from TinyFish")

        try:
            self._last_wait_time = self.rate_tracker.apply_rate_limit(
                self.engine_type
            )

            response = safe_get(
                self.SEARCH_URL,
                params={
                    "query": query[: self.MAX_QUERY_LEN],
                    "location": self.location,
                    "language": self.language,
                },
                headers=self._headers(),
                timeout=self.SEARCH_TIMEOUT,
            )
            self._raise_if_rate_limit(response.status_code)
            response.raise_for_status()

            data = response.json()
            results = data.get("results", [])[: self.max_results]

            previews = []
            for idx, result in enumerate(results):
                link = result.get("url", "")
                display_link = result.get("site_name", "")
                if not display_link and link:
                    try:
                        display_link = urlparse(link).netloc or ""
                    except Exception:
                        logger.debug(
                            f"Failed to parse URL for display: {link[:50]}"
                        )

                preview = {
                    "id": link or str(idx),
                    "title": result.get("title", ""),
                    "link": link,
                    "snippet": result.get("snippet", ""),
                    "displayed_link": display_link,
                    "position": result.get("position", idx + 1),
                }
                preview["_full_result"] = result
                previews.append(preview)

            self._search_results = previews
            return previews

        except RateLimitError:
            raise
        except requests.exceptions.RequestException as e:
            # The search query rides in the request URL (params=query=...), so
            # the requests exception string/traceback would leak it into logs.
            # Log only a static message plus the HTTP status — never the
            # URL-bearing error. (_scrub_error does not strip a plain query=.)
            status_code = getattr(
                getattr(e, "response", None), "status_code", None
            )
            logger.warning(
                f"Error getting TinyFish search results (status={status_code})"
            )
            self._raise_request_exception_if_rate_limited(e)
            return []
        except Exception:
            logger.exception("Unexpected error getting TinyFish search results")
            return []

    def _get_full_content(
        self, relevant_items: List[Dict[str, Any]]
    ) -> List[Dict[str, Any]]:
        """
        Fetch clean page content from TinyFish Fetch for selected results.

        Args:
            relevant_items: List of relevant preview dictionaries

        Returns:
            List of result dictionaries enriched with extracted content
        """
        results = [
            {k: v for k, v in item.items() if k != "_full_result"}
            for item in relevant_items
        ]
        if not self.include_full_content:
            return results

        url_to_result = {
            result.get("link") or result.get("url"): result
            for result in results
            if result.get("link") or result.get("url")
        }
        urls = list(url_to_result.keys())[: self.MAX_FETCH_URLS]
        if not urls:
            return results

        try:
            response = safe_post(
                self.FETCH_URL,
                json={"urls": urls, "format": self.fetch_format},
                headers={**self._headers(), "Content-Type": "application/json"},
                timeout=self.FETCH_TIMEOUT,
            )
            self._raise_if_rate_limit(response.status_code)
            response.raise_for_status()

            data = response.json()
            for page in data.get("results", []):
                url = page.get("url") or page.get("final_url")
                result = url_to_result.get(url)
                if result is None:
                    result = url_to_result.get(page.get("final_url"))
                if result is None:
                    continue

                content = page.get("text")
                if content:
                    result["content"] = content
                for field in ("final_url", "description", "language"):
                    if page.get(field):
                        result[field] = page[field]
                if page.get("title") and not result.get("title"):
                    result["title"] = page["title"]

            errors = data.get("errors", [])
            if errors:
                logger.info(
                    f"TinyFish Fetch returned {len(errors)} per-URL errors"
                )

        except RateLimitError:
            raise
        except requests.exceptions.RequestException as e:
            # Mirror _get_previews: avoid logging the URL-bearing exception
            # string; record a static message plus the HTTP status only.
            status_code = getattr(
                getattr(e, "response", None), "status_code", None
            )
            logger.warning(
                f"Error fetching TinyFish page content (status={status_code})"
            )
            self._raise_request_exception_if_rate_limited(e)
        except Exception:
            logger.exception("Unexpected error fetching TinyFish page content")

        return results
