import re
from typing import Any, Dict, List, Optional, Tuple

import requests
from langchain_core.language_models import BaseLLM
from loguru import logger
from requests.adapters import HTTPAdapter
from urllib3.util import Retry

from ...constants import SNIPPET_LENGTH_SHORT
from ..rate_limiting import RateLimitError
from ..search_engine_base import BaseSearchEngine
from ...security import SafeSession


class SemanticScholarSearchEngine(BaseSearchEngine):
    """
    Semantic Scholar search engine implementation with two-phase approach.
    Provides efficient access to scientific literature across all fields.
    """

    # Mark as public search engine
    is_public = True
    # Scientific/academic search engine
    is_scientific = True

    def __init__(
        self,
        max_results: int = 10,
        api_key: Optional[str] = None,
        year_range: Optional[Tuple[int, int]] = None,
        get_abstracts: bool = True,
        get_references: bool = False,
        get_citations: bool = False,
        get_embeddings: bool = False,
        get_tldr: bool = True,
        citation_limit: int = 10,
        reference_limit: int = 10,
        llm: Optional[BaseLLM] = None,
        max_filtered_results: Optional[int] = None,
        optimize_queries: bool = True,
        max_retries: int = 5,
        retry_backoff_factor: float = 1.0,
        fields_of_study: Optional[List[str]] = None,
        publication_types: Optional[List[str]] = None,
        settings_snapshot: Optional[Dict[str, Any]] = None,
        **kwargs,
    ):
        """
        Initialize the Semantic Scholar search engine.

        Args:
            max_results: Maximum number of search results
            api_key: Semantic Scholar API key for higher rate limits (optional)
            year_range: Optional tuple of (start_year, end_year) to filter results
            get_abstracts: Whether to fetch abstracts for all results
            get_references: Whether to fetch references for papers
            get_citations: Whether to fetch citations for papers
            get_embeddings: Whether to fetch SPECTER embeddings for papers
            get_tldr: Whether to fetch TLDR summaries for papers
            citation_limit: Maximum number of citations to fetch per paper
            reference_limit: Maximum number of references to fetch per paper
            llm: Language model for relevance filtering
            max_filtered_results: Maximum number of results to keep after filtering
            optimize_queries: Whether to optimize natural language queries
            max_retries: Maximum number of retries for API requests
            retry_backoff_factor: Backoff factor for retries
            fields_of_study: List of fields of study to filter results
            publication_types: List of publication types to filter results
            settings_snapshot: Settings snapshot for configuration
            **kwargs: Additional parameters to pass to parent class
        """
        # 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,
            settings_snapshot=settings_snapshot,
            **kwargs,
        )

        # Get API key from settings if not provided
        if not api_key and settings_snapshot:
            from ...config.search_config import get_setting_from_snapshot

            try:
                api_key = get_setting_from_snapshot(
                    "search.engine.web.semantic_scholar.api_key",
                    settings_snapshot=settings_snapshot,
                )
            except Exception:
                logger.debug(
                    "Failed to read semantic_scholar.api_key from settings snapshot",
                    exc_info=True,
                )

        self.api_key = api_key
        self.year_range = year_range
        self.get_abstracts = get_abstracts
        self.get_references = get_references
        self.get_citations = get_citations
        self.get_embeddings = get_embeddings
        self.get_tldr = get_tldr
        self.citation_limit = citation_limit
        self.reference_limit = reference_limit
        self.optimize_queries = optimize_queries
        self.max_retries = max_retries
        self.retry_backoff_factor = retry_backoff_factor
        self.fields_of_study = (
            self._ensure_list(fields_of_study)
            if fields_of_study is not None
            else None
        )
        self.publication_types = (
            self._ensure_list(publication_types)
            if publication_types is not None
            else None
        )

        # Base API URLs
        self.base_url = "https://api.semanticscholar.org/graph/v1"
        self.paper_search_url = f"{self.base_url}/paper/search"
        self.paper_details_url = f"{self.base_url}/paper"

        # Create a session with retry capabilities
        self.session: SafeSession | None = self._create_session()

        # Log API key status
        if self.api_key:
            logger.info(
                "Using Semantic Scholar with API key (higher rate limits)"
            )
        else:
            logger.info(
                "Using Semantic Scholar without API key (lower rate limits)"
            )

    def _create_session(self) -> SafeSession:
        """Create and configure a requests session with retry capabilities"""
        session = SafeSession()

        # Configure automatic retries with exponential backoff
        retry_strategy = Retry(
            total=self.max_retries,
            backoff_factor=self.retry_backoff_factor,
            status_forcelist=[429, 500, 502, 503, 504],
            allowed_methods={"HEAD", "GET", "POST", "OPTIONS"},
        )

        adapter = HTTPAdapter(max_retries=retry_strategy)
        session.mount("https://", adapter)

        # Set up headers
        headers = {"Accept": "application/json"}
        if self.api_key:
            headers["x-api-key"] = self.api_key

        session.headers.update(headers)

        return session

    def close(self):
        """
        Close the HTTP session and clean up resources.

        Call this method when done using the search engine to prevent
        connection/file descriptor leaks.
        """
        if hasattr(self, "session") and self.session:
            try:
                self.session.close()
            except Exception:
                logger.exception("Error closing SemanticScholar session")
            finally:
                self.session = None

    def __del__(self):
        """Destructor to ensure session is closed."""
        self.close()

    def __enter__(self):
        """Context manager entry."""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit - ensures session cleanup."""
        self.close()
        return False

    def _respect_rate_limit(self):
        """Apply rate limiting between requests"""
        # Apply rate limiting before request
        self._last_wait_time = self.rate_tracker.apply_rate_limit(
            self.engine_type
        )
        logger.debug(f"Applied rate limit wait: {self._last_wait_time:.2f}s")

    def _make_request(
        self,
        url: str,
        params: Optional[Dict] = None,
        data: Optional[Dict] = None,
        method: str = "GET",
    ) -> Dict:
        """
        Make a request to the Semantic Scholar API.

        Args:
            url: API endpoint URL
            params: Query parameters
            data: JSON data for POST requests
            method: HTTP method (GET or POST)

        Returns:
            API response as dictionary
        """
        self._respect_rate_limit()

        try:
            if self.session is None:
                raise RuntimeError("Session is not initialized")
            if method.upper() == "GET":
                response = self.session.get(url, params=params, timeout=30)
            elif method.upper() == "POST":
                response = self.session.post(
                    url, params=params, json=data, timeout=30
                )
            else:
                raise ValueError(f"Unsupported HTTP method: {method}")

            # Handle rate limiting
            if response.status_code == 429:
                logger.warning("Semantic Scholar rate limit exceeded")
                raise RateLimitError("Semantic Scholar rate limit exceeded")

            response.raise_for_status()
            return response.json()  # type: ignore[no-any-return]
        except requests.RequestException:
            logger.exception("API request failed")
            return {}

    def _optimize_query(self, query: str) -> str:
        """
        Optimize a natural language query for Semantic Scholar search.
        If LLM is available, uses it to extract key terms and concepts.

        Args:
            query: Natural language query

        Returns:
            Optimized query string
        """
        if not self.llm or not self.optimize_queries:
            return query

        try:
            prompt = f"""Transform this natural language question into an optimized academic search query.

Original query: "{query}"

INSTRUCTIONS:
1. Extract key academic concepts, technical terms, and proper nouns
2. Remove generic words, filler words, and non-technical terms
3. Add quotation marks around specific phrases that should be kept together
4. Return ONLY the optimized search query with no explanation
5. Keep it under 100 characters if possible

EXAMPLE TRANSFORMATIONS:
"What are the latest findings about mRNA vaccines and COVID-19?" → "mRNA vaccines COVID-19 recent findings"
"How does machine learning impact climate change prediction?" → "machine learning "climate change" prediction"
"Tell me about quantum computing approaches for encryption" → "quantum computing encryption"

Return ONLY the optimized search query with no explanation.
"""

            response = self.llm.invoke(prompt)
            optimized_query = (
                str(response.content)
                if hasattr(response, "content")
                else str(response)
            ).strip()

            # Clean up the query - remove any explanations
            lines = optimized_query.split("\n")
            optimized_query = lines[0].strip()

            # Safety check - if query looks too much like an explanation, use original
            if len(optimized_query.split()) > 15 or ":" in optimized_query:
                logger.warning(
                    "Query optimization result looks too verbose, using original"
                )
                return query

            logger.info(f"Original query: '{query}'")
            logger.info(f"Optimized for search: '{optimized_query}'")

            return optimized_query
        except Exception:
            logger.exception("Error optimizing query")
            return query  # Fall back to original query on error

    def _direct_search(self, query: str) -> List[Dict[str, Any]]:
        """
        Make a direct search request to the Semantic Scholar API.

        Args:
            query: The search query

        Returns:
            List of paper dictionaries
        """
        try:
            # Configure fields to retrieve
            fields = [
                "paperId",
                "externalIds",
                "url",
                "title",
                "abstract",
                "venue",
                "year",
                "authors",
                "citationCount",  # Add citation count for ranking
                "openAccessPdf",  # PDF URL for open access papers
            ]

            if self.get_tldr:
                fields.append("tldr")

            params = {
                "query": query,
                "limit": min(
                    self.max_results, 100
                ),  # API limit is 100 per request
                "fields": ",".join(fields),
            }

            # Add year filter if specified
            if self.year_range:
                start_year, end_year = self.year_range
                params["year"] = f"{start_year}-{end_year}"

            # Add fields of study filter if specified
            if self.fields_of_study:
                params["fieldsOfStudy"] = ",".join(self.fields_of_study)

            # Add publication types filter if specified
            if self.publication_types:
                params["publicationTypes"] = ",".join(self.publication_types)

            response = self._make_request(self.paper_search_url, params)

            if "data" in response:
                papers = response["data"]
                logger.info(
                    f"Found {len(papers)} papers with direct search for query: '{query}'"
                )
                return papers  # type: ignore[no-any-return]
            logger.warning(
                f"No data in response for direct search query: '{query}'"
            )
            return []

        except Exception:
            logger.exception("Error in direct search")
            return []

    def _adaptive_search(self, query: str) -> Tuple[List[Dict[str, Any]], str]:
        """
        Perform an adaptive search that adjusts based on result volume.
        Uses LLM to generate better fallback queries when available.

        Args:
            query: The search query

        Returns:
            Tuple of (list of paper results, search strategy used)
        """
        # Start with a standard search
        papers = self._direct_search(query)
        strategy = "standard"

        # If no results, try different variations
        if not papers:
            # Try removing quotes to broaden search
            if '"' in query:
                unquoted_query = query.replace('"', "")
                logger.info(
                    "No results with quoted terms, trying without quotes: %s",
                    unquoted_query,
                )
                papers = self._direct_search(unquoted_query)

                if papers:
                    strategy = "unquoted"
                    return papers, strategy

            # If LLM is available, use it to generate better fallback queries
            if self.llm:
                try:
                    # Generate alternate search queries focusing on core concepts
                    prompt = f"""You are helping refine a search query that returned no results.

Original query: "{query}"

The query might be too specific or use natural language phrasing that doesn't match academic paper keywords.

Please provide THREE alternative search queries that:
1. Focus on the core academic concepts
2. Use precise terminology commonly found in academic papers
3. Break down complex queries into more searchable components
4. Format each as a concise keyword-focused search term (not a natural language question)

Format each query on a new line with no numbering or explanation. Keep each query under 8 words and very focused.
"""
                    # Get the LLM's response
                    response = self.llm.invoke(prompt)

                    # Extract the alternative queries
                    alt_queries = []
                    if hasattr(
                        response, "content"
                    ):  # Handle various LLM response formats
                        content = response.content
                        alt_queries = [
                            q.strip()
                            for q in content.strip().split("\n")
                            if q.strip()
                        ]
                    elif isinstance(response, str):
                        alt_queries = [
                            q.strip()
                            for q in response.strip().split("\n")
                            if q.strip()
                        ]

                    # Try each alternative query
                    for alt_query in alt_queries[
                        :3
                    ]:  # Limit to first 3 alternatives
                        logger.info("Trying LLM-suggested query: %s", alt_query)
                        alt_papers = self._direct_search(alt_query)

                        if alt_papers:
                            logger.info(
                                "Found %s papers using LLM-suggested query: %s",
                                len(alt_papers),
                                alt_query,
                            )
                            strategy = "llm_alternative"
                            return alt_papers, strategy
                except Exception:
                    logger.exception("Error using LLM for query refinement")
                    # Fall through to simpler strategies

            # Fallback: Try with the longest words (likely specific terms)
            words = re.findall(r"\w+", query)
            longer_words = [word for word in words if len(word) > 6]
            if longer_words:
                # Use up to 3 of the longest words
                longer_words = sorted(longer_words, key=len, reverse=True)[:3]
                key_terms_query = " ".join(longer_words)
                logger.info("Trying with key terms: %s", key_terms_query)
                papers = self._direct_search(key_terms_query)

                if papers:
                    strategy = "key_terms"
                    return papers, strategy

            # Final fallback: Try with just the longest word
            if words:
                longest_word = max(words, key=len)
                if len(longest_word) > 5:  # Only use if it's reasonably long
                    logger.info("Trying with single key term: %s", longest_word)
                    papers = self._direct_search(longest_word)

                    if papers:
                        strategy = "single_term"
                        return papers, strategy

        return papers, strategy

    def _get_paper_details(self, paper_id: str) -> Dict[str, Any]:
        """
        Get detailed information about a specific paper.

        Args:
            paper_id: Semantic Scholar Paper ID

        Returns:
            Dictionary with paper details
        """
        try:
            # Construct fields parameter
            fields = [
                "paperId",
                "externalIds",
                "corpusId",
                "url",
                "title",
                "abstract",
                "venue",
                "year",
                "authors",
                "fieldsOfStudy",
                "citationCount",  # Add citation count
            ]

            if self.get_tldr:
                fields.append("tldr")

            if self.get_embeddings:
                fields.append("embedding")

            # Add citation and reference fields if requested
            if self.get_citations:
                fields.append(f"citations.limit({self.citation_limit})")

            if self.get_references:
                fields.append(f"references.limit({self.reference_limit})")

            # Make the request
            url = f"{self.paper_details_url}/{paper_id}"
            params = {"fields": ",".join(fields)}

            return self._make_request(url, params)

        except Exception:
            logger.exception("Error getting paper details for paper")
            return {}

    def _get_previews(self, query: str) -> List[Dict[str, Any]]:
        """
        Get preview information for Semantic Scholar papers.

        Args:
            query: The search query

        Returns:
            List of preview dictionaries
        """
        logger.info(f"Getting Semantic Scholar previews for query: {query}")

        # Optimize the query if LLM is available
        optimized_query = self._optimize_query(query)

        # Use the adaptive search approach
        papers, strategy = self._adaptive_search(optimized_query)

        if not papers:
            logger.warning("No Semantic Scholar results found")
            return []

        # Format as previews
        previews = []
        for paper in papers:
            try:
                # Format authors - ensure we have a valid list with string values
                authors = []
                if paper.get("authors"):
                    authors = [
                        author.get("name", "")
                        for author in paper["authors"]
                        if author and author.get("name")
                    ]

                # Ensure we have valid strings for all fields
                paper_id = paper.get("paperId", "")
                title = paper.get("title", "")
                url = paper.get("url", "")

                # Handle abstract safely, ensuring we always have a string
                abstract = paper.get("abstract")
                snippet = ""
                if abstract:
                    snippet = (
                        abstract[:SNIPPET_LENGTH_SHORT] + "..."
                        if len(abstract) > SNIPPET_LENGTH_SHORT
                        else abstract
                    )

                venue = paper.get("venue", "")
                year = paper.get("year")
                external_ids = paper.get("externalIds", {})

                # Handle TLDR safely
                tldr_text = ""
                if paper.get("tldr") and isinstance(paper.get("tldr"), dict):
                    tldr_text = paper.get("tldr", {}).get("text", "")

                # Create preview with basic information, ensuring no None values
                preview = {
                    "id": paper_id if paper_id else "",
                    "title": title if title else "",
                    "link": url if url else "",
                    "snippet": snippet,
                    "authors": authors,
                    "venue": venue if venue else "",
                    "year": year,
                    "external_ids": external_ids if external_ids else {},
                    "source": "Semantic Scholar",
                    "_paper_id": paper_id if paper_id else "",
                    "_search_strategy": strategy,
                    "tldr": tldr_text,
                }

                # Store the full paper object for later reference
                preview["_full_paper"] = paper

                previews.append(preview)
            except Exception:
                logger.exception("Error processing paper preview")
                # Continue with the next paper

        # Sort by year (newer first) if available
        def _year_key(p: dict[str, Any]) -> int:
            year = p.get("year")
            try:
                return int(year) if year is not None else 0
            except (TypeError, ValueError):
                return 0

        previews = sorted(previews, key=_year_key, reverse=True)

        logger.info(
            f"Found {len(previews)} Semantic Scholar previews using strategy: {strategy}"
        )
        return previews

    def _get_full_content(
        self, relevant_items: List[Dict[str, Any]]
    ) -> List[Dict[str, Any]]:
        """
        Get full content for the relevant Semantic Scholar papers.
        Gets additional details like citations, references, and full metadata.

        Args:
            relevant_items: List of relevant preview dictionaries

        Returns:
            List of result dictionaries with full content
        """
        # For Semantic Scholar, we already have most content from the preview
        # Additional API calls are only needed for citations/references

        logger.info(
            f"Getting content for {len(relevant_items)} Semantic Scholar papers"
        )

        results = []
        for item in relevant_items:
            result = item.copy()
            paper_id = item.get("_paper_id", "")

            # Skip if no paper ID
            if not paper_id:
                results.append(result)
                continue

            # Get paper details if citations or references are requested
            if self.get_citations or self.get_references or self.get_embeddings:
                paper_details = self._get_paper_details(paper_id)

                if paper_details:
                    # Add citation information
                    if self.get_citations and "citations" in paper_details:
                        result["citations"] = paper_details["citations"]

                    # Add reference information
                    if self.get_references and "references" in paper_details:
                        result["references"] = paper_details["references"]

                    # Add embedding if available
                    if self.get_embeddings and "embedding" in paper_details:
                        result["embedding"] = paper_details["embedding"]

                    # Add fields of study
                    if "fieldsOfStudy" in paper_details:
                        result["fields_of_study"] = paper_details[
                            "fieldsOfStudy"
                        ]

            # Remove temporary fields
            if "_paper_id" in result:
                del result["_paper_id"]
            if "_search_strategy" in result:
                del result["_search_strategy"]
            if "_full_paper" in result:
                del result["_full_paper"]

            results.append(result)

        return results
