"""OpenAlex search engine implementation for academic papers and research."""

from typing import Any, Dict, List, Optional

from langchain_core.language_models import BaseLLM
from loguru import logger

from ...constants import SNIPPET_LENGTH_LONG
from ...advanced_search_system.filters.journal_reputation_filter import (
    JournalReputationFilter,
)
from ...security.safe_requests import safe_get
from ..rate_limiting import RateLimitError
from ..search_engine_base import BaseSearchEngine


class OpenAlexSearchEngine(BaseSearchEngine):
    """OpenAlex search engine implementation with natural language query support."""

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

    def __init__(
        self,
        max_results: int = 25,
        email: Optional[str] = None,
        sort_by: str = "relevance",
        filter_open_access: bool = False,
        min_citations: int = 0,
        from_publication_date: Optional[str] = None,
        llm: Optional[BaseLLM] = None,
        max_filtered_results: Optional[int] = None,
        settings_snapshot: Optional[Dict[str, Any]] = None,
        **kwargs,
    ):
        """
        Initialize the OpenAlex search engine.

        Args:
            max_results: Maximum number of search results
            email: Email for polite pool (gets faster response) - optional
            sort_by: Sort order ('relevance', 'cited_by_count', 'publication_date')
            filter_open_access: Only return open access papers
            min_citations: Minimum citation count filter
            from_publication_date: Filter papers from this date (YYYY-MM-DD)
            llm: Language model for relevance filtering
            max_filtered_results: Maximum number of results to keep after filtering
            settings_snapshot: Settings snapshot for configuration
            **kwargs: Additional parameters to pass to parent class
        """
        # Initialize journal reputation filter if needed
        content_filters = []
        journal_filter = JournalReputationFilter.create_default(
            model=llm,  # type: ignore[arg-type]
            engine_name="openalex",
            settings_snapshot=settings_snapshot,
        )
        if journal_filter is not None:
            content_filters.append(journal_filter)

        # Initialize the BaseSearchEngine
        super().__init__(
            llm=llm,
            max_filtered_results=max_filtered_results,
            max_results=max_results,
            content_filters=content_filters,  # type: ignore[arg-type]
            settings_snapshot=settings_snapshot,
            **kwargs,
        )

        self.sort_by = sort_by
        self.filter_open_access = filter_open_access
        self.min_citations = min_citations
        # Only set from_publication_date if it's not empty or "False"
        self.from_publication_date = (
            from_publication_date
            if from_publication_date and from_publication_date != "False"
            else None
        )

        # Get email from settings if not provided
        if not email and settings_snapshot:
            from ...config.search_config import get_setting_from_snapshot

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

        # Handle "False" string for email
        self.email = email if email and email != "False" else None

        # API configuration
        self.api_base = "https://api.openalex.org"
        self.headers = {
            "User-Agent": f"Local-Deep-Research-Agent{f' ({email})' if email else ''}",
            "Accept": "application/json",
        }

        if email:
            # Email allows access to polite pool with faster response times
            logger.info(f"Using OpenAlex polite pool with email: {email}")
        else:
            logger.info(
                "Using OpenAlex without email (consider adding email for faster responses)"
            )

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

        Args:
            query: The search query (natural language supported!)

        Returns:
            List of preview dictionaries
        """
        logger.info(f"Searching OpenAlex for: {query}")

        # Build the search URL with parameters
        params = {
            "search": query,  # OpenAlex handles natural language beautifully
            "per_page": min(self.max_results, 200),  # OpenAlex allows up to 200
            "page": 1,
            # Request specific fields including abstract for snippets
            "select": "id,display_name,publication_year,publication_date,doi,primary_location,authorships,cited_by_count,open_access,best_oa_location,abstract_inverted_index",
        }

        # Add optional filters
        filters = []

        if self.filter_open_access:
            filters.append("is_oa:true")

        if self.min_citations > 0:
            filters.append(f"cited_by_count:>{self.min_citations}")

        if self.from_publication_date and self.from_publication_date != "False":
            filters.append(
                f"from_publication_date:{self.from_publication_date}"
            )

        if filters:
            params["filter"] = ",".join(filters)

        # Add sorting
        sort_map = {
            "relevance": "relevance_score:desc",
            "cited_by_count": "cited_by_count:desc",
            "publication_date": "publication_date:desc",
        }
        params["sort"] = sort_map.get(self.sort_by, "relevance_score:desc")

        # Add email to params for polite pool
        if self.email and self.email != "False":
            params["mailto"] = self.email

        try:
            # Apply rate limiting before making the request (simple like PubMed)
            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"
            )

            # Make the API request
            logger.info(f"Making OpenAlex API request with params: {params}")
            response = safe_get(
                f"{self.api_base}/works",
                params=params,
                headers=self.headers,
                timeout=30,
            )
            logger.info(f"OpenAlex API response status: {response.status_code}")

            # Log rate limit info if available
            if "x-ratelimit-remaining" in response.headers:
                remaining = response.headers.get("x-ratelimit-remaining")
                limit = response.headers.get("x-ratelimit-limit", "unknown")
                logger.debug(
                    f"OpenAlex rate limit: {remaining}/{limit} requests remaining"
                )

            if response.status_code == 200:
                data = response.json()
                results = data.get("results", [])
                meta = data.get("meta", {})
                total_count = meta.get("count", 0)

                logger.info(
                    f"OpenAlex returned {len(results)} results (total available: {total_count:,})"
                )

                # Log first result structure for debugging
                if results:
                    first_result = results[0]
                    logger.debug(
                        f"First result keys: {list(first_result.keys())}"
                    )
                    logger.debug(
                        f"First result has abstract: {'abstract_inverted_index' in first_result}"
                    )
                    if "open_access" in first_result:
                        logger.debug(
                            f"Open access structure: {first_result['open_access']}"
                        )

                # Format results as previews
                previews = []
                for i, work in enumerate(results):
                    logger.debug(
                        f"Formatting work {i + 1}/{len(results)}: {work.get('display_name', 'Unknown')[:50]}"
                    )
                    preview = self._format_work_preview(work)
                    if preview:
                        previews.append(preview)
                        logger.debug(
                            f"Preview created with snippet: {preview.get('snippet', '')[:100]}..."
                        )
                    else:
                        logger.warning(f"Failed to format work {i + 1}")

                logger.info(
                    f"Successfully formatted {len(previews)} previews from {len(results)} results"
                )
                return previews

            if response.status_code == 429:
                # Rate limited (very rare with OpenAlex)
                logger.warning("OpenAlex rate limit reached")
                raise RateLimitError("OpenAlex rate limit exceeded")  # noqa: TRY301 — re-raised by except RateLimitError for base class retry

            logger.error(
                f"OpenAlex API error: {response.status_code} - {response.text[:200]}"
            )
            return []

        except RateLimitError:
            # Re-raise rate limit errors for base class retry handling
            raise
        except Exception:
            logger.exception("Error searching OpenAlex")
            return []

    def _format_work_preview(
        self, work: Dict[str, Any]
    ) -> Optional[Dict[str, Any]]:
        """
        Format an OpenAlex work as a preview dictionary.

        Args:
            work: OpenAlex work object

        Returns:
            Formatted preview dictionary or None if formatting fails
        """
        try:
            # Extract basic information
            work_id = work.get("id", "")
            title = work.get("display_name", "No title")
            logger.debug(f"Formatting work: {title[:50]}")

            # Build snippet from abstract or first part of title
            abstract = None
            if work.get("abstract_inverted_index"):
                logger.debug(
                    f"Found abstract_inverted_index with {len(work['abstract_inverted_index'])} words"
                )
                # Reconstruct abstract from inverted index
                abstract = self._reconstruct_abstract(
                    work["abstract_inverted_index"]
                )
                logger.debug(
                    f"Reconstructed abstract length: {len(abstract) if abstract else 0}"
                )
            else:
                logger.debug("No abstract_inverted_index found")

            snippet = (
                abstract[:SNIPPET_LENGTH_LONG]
                if abstract
                else f"Academic paper: {title}"
            )
            logger.debug(f"Created snippet: {snippet[:100]}...")

            # Get publication info
            publication_year = work.get("publication_year", "unknown")
            publication_date = work.get("publication_date", "unknown")

            # Get venue/journal info
            venue = work.get("primary_location", {})
            journal_name = "unknown"
            if venue:
                source = venue.get("source", {})
                if source:
                    journal_name = source.get("display_name", "unknown")

            # Get authors
            authors = []
            for authorship in work.get("authorships", [])[
                :5
            ]:  # Limit to 5 authors
                author = authorship.get("author", {})
                if author:
                    authors.append(author.get("display_name", ""))

            authors_str = ", ".join(authors)
            if len(work.get("authorships", [])) > 5:
                authors_str += " et al."

            # Get metrics
            cited_by_count = work.get("cited_by_count", 0)

            # Get URL - prefer DOI, fallback to OpenAlex URL
            url = work.get("doi", work_id)
            if not url.startswith("http"):
                if url.startswith("https://doi.org/"):
                    pass  # Already a full DOI URL
                elif url.startswith("10."):
                    url = f"https://doi.org/{url}"
                else:
                    url = work_id  # OpenAlex URL

            # Check if open access
            open_access_info = work.get("open_access", {})
            is_oa = (
                open_access_info.get("is_oa", False)
                if open_access_info
                else False
            )
            oa_url = None
            if is_oa:
                best_location = work.get("best_oa_location", {})
                if best_location:
                    oa_url = best_location.get("pdf_url") or best_location.get(
                        "landing_page_url"
                    )

            return {
                "id": work_id,
                "title": title,
                "link": url,
                "snippet": snippet,
                "authors": authors_str,
                "year": publication_year,
                "date": publication_date,
                "journal": journal_name,
                "citations": cited_by_count,
                "is_open_access": is_oa,
                "oa_url": oa_url,
                "abstract": abstract,
                "type": "academic_paper",
            }

        except Exception:
            logger.exception(
                f"Error formatting OpenAlex work: {work.get('id', 'unknown')}"
            )
            return None

    def _reconstruct_abstract(
        self, inverted_index: Dict[str, List[int]]
    ) -> str:
        """
        Reconstruct abstract text from OpenAlex inverted index format.

        Args:
            inverted_index: Dictionary mapping words to their positions

        Returns:
            Reconstructed abstract text
        """
        try:
            # Create position-word mapping
            position_word = {}
            for word, positions in inverted_index.items():
                for pos in positions:
                    position_word[pos] = word

            # Sort by position and reconstruct
            sorted_positions = sorted(position_word.keys())
            words = [position_word[pos] for pos in sorted_positions]

            return " ".join(words)

        except Exception:
            logger.debug("Could not reconstruct abstract from inverted index")
            return ""

    def _get_full_content(
        self, relevant_items: List[Dict[str, Any]]
    ) -> List[Dict[str, Any]]:
        """
        Get full content for relevant items (OpenAlex provides most content in preview).

        Args:
            relevant_items: List of relevant preview dictionaries

        Returns:
            List of result dictionaries with full content
        """
        # OpenAlex returns comprehensive data in the initial search,
        # so we don't need a separate full content fetch
        results = []
        for item in relevant_items:
            result = {
                "title": item.get("title", ""),
                "link": item.get("link", ""),
                "snippet": item.get("snippet", ""),
                "content": item.get("abstract", item.get("snippet", "")),
                "metadata": {
                    "authors": item.get("authors", ""),
                    "year": item.get("year", ""),
                    "journal": item.get("journal", ""),
                    "citations": item.get("citations", 0),
                    "is_open_access": item.get("is_open_access", False),
                    "oa_url": item.get("oa_url"),
                },
            }
            results.append(result)

        return results
