"""Stack Exchange search engine for Q&A content."""

import html
import re
import time
from typing import Any, Dict, List, Optional

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

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


class StackExchangeSearchEngine(BaseSearchEngine):
    """
    Stack Exchange search engine for Q&A content.

    Provides access to Stack Overflow and other Stack Exchange sites.
    No authentication required (300 requests/day without key).
    """

    is_public = True
    is_generic = False
    is_scientific = False
    is_code = True

    # Common Stack Exchange sites
    SITES = {
        "stackoverflow": "Stack Overflow",
        "serverfault": "Server Fault",
        "superuser": "Super User",
        "askubuntu": "Ask Ubuntu",
        "unix": "Unix & Linux",
        "math": "Mathematics",
        "physics": "Physics",
        "stats": "Cross Validated",
        "security": "Information Security",
        "dba": "Database Administrators",
    }

    # Sites with their own .com domains (not *.stackexchange.com)
    SITE_DOMAINS = {
        "stackoverflow": "stackoverflow.com",
        "serverfault": "serverfault.com",
        "superuser": "superuser.com",
        "askubuntu": "askubuntu.com",
    }

    def __init__(
        self,
        max_results: int = 10,
        site: str = "stackoverflow",
        sort: str = "relevance",
        accepted_only: bool = False,
        has_answers: bool = False,
        min_score: Optional[int] = None,
        tagged: Optional[str] = None,
        llm: Optional[BaseLLM] = None,
        max_filtered_results: Optional[int] = None,
        settings_snapshot: Optional[Dict[str, Any]] = None,
        **kwargs,
    ):
        """
        Initialize the Stack Exchange search engine.

        Args:
            max_results: Maximum number of search results
            site: Stack Exchange site to search (stackoverflow, serverfault, etc.)
            sort: Sort order (relevance, votes, creation, activity)
            accepted_only: Only return questions with accepted answers
            has_answers: Only return questions that have answers
            min_score: Minimum score for questions
            tagged: Filter by tags (semicolon separated)
            llm: Language model for relevance filtering
            max_filtered_results: Maximum results after filtering
            settings_snapshot: Settings snapshot for thread context
        """
        super().__init__(
            llm=llm,
            max_filtered_results=max_filtered_results,
            max_results=max_results,
            settings_snapshot=settings_snapshot,
            **kwargs,
        )

        # Validate site parameter
        if site not in self.SITES:
            valid_sites = ", ".join(self.SITES.keys())
            raise ValueError(
                f"Invalid site: '{site}'. Must be one of: {valid_sites}"
            )

        # Validate sort parameter
        valid_sorts = ("relevance", "votes", "creation", "activity")
        if sort not in valid_sorts:
            raise ValueError(
                f"Invalid sort: '{sort}'. Must be one of: {', '.join(valid_sorts)}"
            )

        # Validate sort/min_score combination: the StackExchange API's "min"
        # parameter works with any sort except "relevance".
        if min_score is not None and sort == "relevance":
            raise ValueError(
                "min_score requires a numeric sort order (votes, creation, or activity). "
                "sort='relevance' does not support the 'min' parameter."
            )

        self.site = site
        self.sort = sort
        self.accepted_only = accepted_only
        self.has_answers = has_answers
        self.min_score = min_score
        self.tagged = tagged

        self.base_url = "https://api.stackexchange.com/2.3"
        self.search_url = f"{self.base_url}/search/advanced"

        # User-Agent and required headers for API requests
        self.headers = {
            "User-Agent": "LocalDeepResearch/1.0 (https://github.com/LearningCircuit/local-deep-research)",
            "Accept-Encoding": "gzip, deflate",
        }

        # Track backoff requirement from API responses
        self._backoff_until: float = 0

    def _apply_backoff(self) -> None:
        """Apply backoff if required by previous API response."""
        if self._backoff_until > 0:
            wait_time = self._backoff_until - time.time()
            if wait_time > 0:
                logger.info(
                    f"Stack Exchange backoff: waiting {wait_time:.1f} seconds"
                )
                time.sleep(wait_time)
            self._backoff_until = 0

    def _handle_backoff(self, data: Dict[str, Any]) -> None:
        """Handle backoff field in API response."""
        backoff = data.get("backoff")
        if backoff:
            self._backoff_until = time.time() + min(int(backoff), 300)
            logger.warning(
                f"Stack Exchange API requested backoff of {backoff} seconds"
            )

    def _build_query_params(self, query: str) -> Dict[str, Any]:
        """Build query parameters for the API request."""
        params = {
            "q": query,
            "site": self.site,
            "order": "desc",
            "sort": self.sort,
            "pagesize": min(self.max_results, 100),
            "filter": "withbody",  # Include question body
        }

        if self.accepted_only:
            params["accepted"] = "True"

        if self.has_answers:
            params["answers"] = "1"

        if self.min_score is not None:
            params["min"] = self.min_score

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

        return params

    def _decode_html(self, text: str) -> str:
        """Decode HTML entities in text."""
        return html.unescape(text)

    def _get_site_name(self) -> str:
        """Get human-readable site name."""
        return self.SITES.get(self.site, self.site.title())

    def _get_previews(self, query: str) -> List[Dict[str, Any]]:
        """
        Get preview information for Stack Exchange questions.

        Args:
            query: The search query

        Returns:
            List of preview dictionaries
        """
        logger.info(
            f"Getting Stack Exchange previews for query: {query} on {self.site}"
        )

        # Apply rate limiting
        self._last_wait_time = self.rate_tracker.apply_rate_limit(
            self.engine_type
        )

        # Apply backoff if required by previous API response
        self._apply_backoff()

        try:
            params = self._build_query_params(query)

            response = safe_get(
                self.search_url,
                params=params,
                headers=self.headers,
                timeout=30,
            )

            self._raise_if_rate_limit(response.status_code)

            response.raise_for_status()
            data = response.json()

            # Handle backoff if present in response
            self._handle_backoff(data)

            # Check for API errors
            if "error_id" in data:
                error_msg = data.get("error_message", "Unknown error")
                logger.error(f"Stack Exchange API error: {error_msg}")
                return []

            results = data.get("items", [])
            quota_remaining = data.get("quota_remaining", 0)
            logger.info(
                f"Found {len(results)} Stack Exchange results, quota remaining: {quota_remaining}"
            )

            if quota_remaining < 10:
                logger.warning(f"Stack Exchange quota low: {quota_remaining}")

            previews = []
            for question in results[: self.max_results]:
                try:
                    question_id = question.get("question_id")
                    title = self._decode_html(question.get("title", "Untitled"))

                    # Get owner info
                    owner = question.get("owner", {})
                    author = self._decode_html(
                        owner.get("display_name", "Unknown")
                    )
                    author_link = owner.get("link", "")
                    author_reputation = owner.get("reputation", 0)

                    # Get question stats
                    score = question.get("score", 0)
                    view_count = question.get("view_count", 0)
                    answer_count = question.get("answer_count", 0)
                    is_answered = question.get("is_answered", False)
                    accepted_answer_id = question.get("accepted_answer_id")

                    # Get tags
                    tags = question.get("tags", [])

                    # Build answer status prefix
                    status_parts = []
                    if is_answered:
                        status = f"Answered ({answer_count} answer{'s' if answer_count != 1 else ''}"
                        if accepted_answer_id:
                            status += ", accepted"
                        status += ")"
                        status_parts.append(status)
                    elif answer_count > 0:
                        status_parts.append(
                            f"{answer_count} answer{'s' if answer_count != 1 else ''}"
                        )
                    if tags:
                        status_parts.append(f"Tags: {', '.join(tags[:4])}")
                    prefix = " | ".join(status_parts)

                    # Get body (snippet)
                    body = question.get("body", "")
                    # Strip HTML for snippet
                    body_text = html.unescape(re.sub(r"<[^>]+>", " ", body))
                    body_text = " ".join(body_text.split())[:1000]
                    snippet = f"{prefix} | {body_text}" if prefix else body_text

                    # Get dates
                    creation_date = question.get("creation_date", 0)
                    last_activity = question.get("last_activity_date", 0)

                    # Build link
                    fallback_domain = self.SITE_DOMAINS.get(
                        self.site, f"{self.site}.stackexchange.com"
                    )
                    link = question.get(
                        "link",
                        f"https://{fallback_domain}/questions/{question_id}",
                    )

                    preview = {
                        "id": str(question_id),
                        "title": title,
                        "link": link,
                        "snippet": snippet,
                        "author": author,
                        "author_link": author_link,
                        "author_reputation": author_reputation,
                        "score": score,
                        "view_count": view_count,
                        "answer_count": answer_count,
                        "is_answered": is_answered,
                        "has_accepted_answer": accepted_answer_id is not None,
                        "tags": tags,
                        "creation_date": creation_date,
                        "last_activity_date": last_activity,
                        "site": self.site,
                        "source": self._get_site_name(),
                        "_raw": question,
                    }

                    previews.append(preview)

                except Exception:
                    logger.exception("Error parsing Stack Exchange question")
                    continue

            return previews

        except (requests.RequestException, ValueError) as e:
            logger.exception("Stack Exchange API request failed")
            self._raise_if_rate_limit(e)
            return []

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

        Fetches the question body and top answers from the API.

        Args:
            relevant_items: List of relevant preview dictionaries

        Returns:
            List of result dictionaries with full content
        """
        logger.info(
            f"Getting full content for {len(relevant_items)} Stack Exchange questions"
        )

        results = []
        for item in relevant_items:
            result = item.copy()

            raw = item.get("_raw", {})
            if raw:
                # Get full body
                body = raw.get("body", "")
                clean_body = html.unescape(re.sub(r"<[^>]+>", " ", body))
                clean_body = " ".join(clean_body.split())

                # Build content with question + answers
                content_parts = []
                content_parts.append(
                    f"Question: {result.get('title', 'Untitled')}"
                )
                if result.get("tags"):
                    content_parts.append(f"Tags: {', '.join(result['tags'])}")
                content_parts.append(f"\n{clean_body}")

                # Fetch top answers
                question_id = raw.get("question_id")
                if question_id:
                    try:
                        question_id = int(question_id)
                    except (TypeError, ValueError):
                        question_id = None
                if question_id:
                    answers = self._fetch_top_answers(
                        question_id, max_answers=3
                    )
                    if answers:
                        content_parts.append(
                            f"\n--- Top Answers ({len(answers)}) ---"
                        )
                        for ans in answers:
                            ans_body = html.unescape(
                                re.sub(r"<[^>]+>", " ", ans.get("body", ""))
                            )
                            ans_body = " ".join(ans_body.split())[:3000]
                            score = ans.get("score", 0)
                            accepted = ans.get("is_accepted", False)
                            label = f"[Score: {score}"
                            if accepted:
                                label += ", Accepted"
                            label += "]"
                            content_parts.append(f"\n{label}\n{ans_body}")

                result["content"] = "\n".join(content_parts)

            # Clean up internal fields
            if "_raw" in result:
                del result["_raw"]

            results.append(result)

        return results

    def _fetch_top_answers(
        self, question_id: int, max_answers: int = 3
    ) -> List[Dict[str, Any]]:
        """Fetch top answers for a question, sorted by votes."""
        try:
            self._apply_backoff()
            url = f"{self.base_url}/questions/{question_id}/answers"
            params = {
                "site": self.site,
                "order": "desc",
                "sort": "votes",
                "pagesize": max_answers,
                "filter": "withbody",
            }
            response = safe_get(
                url, params=params, headers=self.headers, timeout=30
            )
            self._raise_if_rate_limit(response.status_code)
            response.raise_for_status()
            data = response.json()
            self._handle_backoff(data)

            if "error_id" in data:
                logger.warning(
                    f"Stack Exchange API error fetching answers for "
                    f"{question_id}: {data.get('error_message', 'Unknown')}"
                )
                return []

            quota_remaining = data.get("quota_remaining")
            if quota_remaining is not None and quota_remaining < 10:
                logger.warning(f"Stack Exchange quota low: {quota_remaining}")

            return data.get("items", [])  # type: ignore[no-any-return]
        except (RateLimitError, ValueError):
            raise
        except Exception:
            logger.warning(
                f"Failed to fetch answers for question {question_id}"
            )
            return []

    def get_question(self, question_id: int) -> Optional[Dict[str, Any]]:
        """
        Get a specific question by ID.

        Args:
            question_id: The Stack Exchange question ID

        Returns:
            Question dictionary or None
        """
        try:
            url = f"{self.base_url}/questions/{question_id}"
            params = {"site": self.site, "filter": "withbody"}
            response = safe_get(
                url, params=params, headers=self.headers, timeout=30
            )
            self._raise_if_rate_limit(response.status_code)
            response.raise_for_status()
            data = response.json()
            self._handle_backoff(data)
            items = data.get("items", [])
            return items[0] if items else None
        except RateLimitError:
            raise
        except Exception:
            logger.exception(
                f"Error fetching Stack Exchange question {question_id}"
            )
            return None

    def get_answers(self, question_id: int) -> List[Dict[str, Any]]:
        """
        Get answers for a specific question.

        Args:
            question_id: The Stack Exchange question ID

        Returns:
            List of answer dictionaries
        """
        try:
            url = f"{self.base_url}/questions/{question_id}/answers"
            params = {
                "site": self.site,
                "order": "desc",
                "sort": "votes",
                "filter": "withbody",
            }
            response = safe_get(
                url, params=params, headers=self.headers, timeout=30
            )
            self._raise_if_rate_limit(response.status_code)
            response.raise_for_status()
            data = response.json()
            self._handle_backoff(data)
            return data.get("items", [])  # type: ignore[no-any-return]
        except RateLimitError:
            raise
        except Exception:
            logger.exception(
                f"Error fetching answers for question {question_id}"
            )
            return []

    def search_by_tag(self, tag: str, query: str = "") -> List[Dict[str, Any]]:
        """
        Search questions by tag.

        Args:
            tag: The tag to filter by
            query: Optional search query

        Returns:
            List of matching questions
        """
        original_tagged = self.tagged
        try:
            self.tagged = tag
            return self.run(query)
        finally:
            self.tagged = original_tagged
