"""Semantic Scholar provider.

Wraps the Semantic Scholar bulk search API
(``graph/v1/paper/search/bulk``) with retry logic, rate limiting, and
regex-based post-filtering.
"""

from __future__ import annotations

import logging
import re
import time
from typing import Any, Optional
from urllib.parse import quote

import requests

from ..models import apply_filters, FilterConfig
from .base import BaseProvider, ProgressCallback
from . import register_provider

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

DEFAULT_FIELDS = (
    "externalIds,authors,title,year,publicationDate,abstract,"
    "publicationTypes,journal,referenceCount,citationCount,"
    "isOpenAccess,openAccessPdf"
)

API_DELAY = 0.5
MAX_RETRIES = 3


# ---------------------------------------------------------------------------
# Provider
# ---------------------------------------------------------------------------


@register_provider("semantic_scholar")
class SemanticScholarProvider(BaseProvider):
    """Provider for the Semantic Scholar bulk search API."""

    provider_name = "semantic_scholar"

    def __init__(self) -> None:
        self.base_url = "https://api.semanticscholar.org/graph/v1/paper/search/bulk"

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def search(
        self,
        query: str,
        max_retrieval: int = 10_000,
        limit: int = 50,
        fields: str = DEFAULT_FIELDS,
        search_type: str = "limited",
        apply_regex_filter: bool = True,
        regex_search_fields: Optional[list[str]] = None,
        match_mode: str = "any",
        filters: Optional[FilterConfig] = None,
        progress_callback: ProgressCallback = None,
    ) -> tuple[list[dict[str, Any]], dict[str, Any]]:
        """Search Semantic Scholar with regex post-filtering.

        Returns:
            Tuple of (papers_list, metadata_dict).  The metadata dict
            contains counts at each filtering stage.
        """
        extended_query = self._build_extended_query(query, search_type)
        rsf = regex_search_fields or ["title", "abstract"]

        papers = self._fetch_all(extended_query, fields, max_retrieval, progress_callback=progress_callback)
        meta = {"total_from_api": len(papers)}

        if apply_regex_filter:
            papers = self._apply_regex_filter(papers, query, rsf, match_mode)
        meta["total_after_regex"] = len(papers)

        if filters and filters.is_active():
            papers = apply_filters(papers, filters)
        meta["total_after_filters"] = len(papers)

        return papers[:limit], meta

    def search_by_author(
        self,
        author_name: str,
        max_retrieval: int = 10_000,
        limit: int = 50,
        fields: str = DEFAULT_FIELDS,
        search_type: str = "limited",
        filters: Optional[FilterConfig] = None,
        progress_callback: ProgressCallback = None,
    ) -> tuple[list[dict[str, Any]], dict[str, Any]]:
        """Search for papers by a specific author."""
        extended_query = self._build_extended_query(author_name, search_type)

        papers = self._fetch_all(extended_query, fields, max_retrieval, progress_callback=progress_callback)
        meta = {"total_from_api": len(papers)}

        # Post-filter by author name in authors list
        target = author_name.lower()
        author_filtered: list[dict[str, Any]] = []
        for paper in papers:
            authors = paper.get("authors", [])
            if not isinstance(authors, list):
                continue
            for a in authors:
                name = a.get("name", "") if isinstance(a, dict) else str(a)
                if target in name.lower():
                    author_filtered.append(paper)
                    break
        meta["total_after_author_filter"] = len(author_filtered)

        if filters and filters.is_active():
            author_filtered = apply_filters(author_filtered, filters)
        meta["total_after_filters"] = len(author_filtered)

        return author_filtered[:limit], meta

    def get_stats(
        self,
        query: str,
        max_retrieval: int = 10_000,
        fields: str = DEFAULT_FIELDS,
        search_type: str = "limited",
    ) -> dict[str, Any]:
        """Fetch papers and return metadata about the result set."""
        extended_query = self._build_extended_query(query, search_type)
        papers = self._fetch_all(extended_query, fields, max_retrieval)
        return {
            "provider": self.provider_name,
            "query": query,
            "extended_query": extended_query,
            "total_papers": len(papers),
        }

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _build_extended_query(self, query: str, search_type: str = "limited") -> str:
        """Transform a regex pattern into an API-compatible keyword query."""
        if search_type == "limited":
            op = " + "
        elif search_type == "extended":
            op = " | "
        else:
            op = " "

        terms: list[str] = []
        seen: set[str] = set()
        for match in re.finditer(
            r'(?:"(.*?)")|(?:\'(.*?)\')|(?:([a-zA-Z-]{3,}[.*+?]{0,2}))',
            query,
        ):
            raw = "".join(match.groups(default="")).strip()
            if not raw or raw in seen:
                continue
            seen.add(raw)
            raw = raw.replace(".*", "*").replace(".+", "*").replace(".?", "*")
            terms.append(raw)

        return op.join(terms)

    def _fetch_all(
        self,
        extended_query: str,
        fields: str = DEFAULT_FIELDS,
        max_retrieval: int = 10_000_000,
        progress_callback: ProgressCallback = None,
    ) -> list[dict[str, Any]]:
        """Paginate through the Semantic Scholar bulk API."""
        url = (
            f"{self.base_url}?query={quote(extended_query)}&fields={fields}"
        )

        papers: list[dict[str, Any]] = []
        retrieved = 0
        token: Optional[str] = None

        while retrieved < max_retrieval:
            request_url = f"{url}&token={token}" if token else url
            data = self._request_with_retry(request_url)
            if data is None:
                logger.warning("Stopping due to repeated API failures.")
                break

            if "data" in data:
                papers.extend(data["data"])
                retrieved += len(data["data"])

            # Report progress after each page
            if progress_callback is not None:
                batch_size = 500  # approximate batch size
                total = min(max_retrieval, retrieved + batch_size * 2)  # estimate
                progress_callback(retrieved, total, f"Fetching from Semantic Scholar...")

            token = data.get("token")
            if not token:
                break

            if API_DELAY > 0:
                time.sleep(API_DELAY)

        logger.info("Retrieved %d papers from Semantic Scholar.", retrieved)
        return papers

    @staticmethod
    def _request_with_retry(url: str, max_retries: int = MAX_RETRIES) -> Optional[dict]:
        """Make a GET request with retry logic for transient errors."""
        for attempt in range(1, max_retries + 1):
            try:
                r = requests.get(url, timeout=30)
                r.raise_for_status()
                return r.json()
            except requests.exceptions.Timeout:
                logger.warning("Request timed out (attempt %d/%d).", attempt, max_retries)
            except requests.exceptions.HTTPError as e:
                status = e.response.status_code if e.response is not None else "unknown"
                logger.warning("HTTP %s error (attempt %d/%d).", status, attempt, max_retries)
                if status == 429:
                    time.sleep(5 * attempt)
                elif status in (400, 404):
                    return None
            except requests.exceptions.ConnectionError:
                logger.warning("Connection error (attempt %d/%d).", attempt, max_retries)
            except Exception:
                logger.exception("Unexpected error (attempt %d/%d).", attempt, max_retries)

            if attempt < max_retries:
                time.sleep(2 ** attempt)

        return None

    @staticmethod
    def _apply_regex_filter(
        papers: list[dict[str, Any]],
        query: str,
        regex_search_fields: list[str],
        match_mode: str = "any",
    ) -> list[dict[str, Any]]:
        """Filter papers by applying the original regex to specified fields."""
        filtered: list[dict[str, Any]] = []
        for paper in papers:
            field_matches: dict[str, bool] = {}
            for field in regex_search_fields:
                value = paper.get(field)
                if value and isinstance(value, str):
                    field_matches[field] = bool(re.search(query, value, re.IGNORECASE))
                else:
                    field_matches[field] = False

            keep = all(field_matches.values()) if match_mode == "all" else any(field_matches.values())
            if keep:
                paper_copy = dict(paper)
                paper_copy["_regex_matches"] = field_matches
                filtered.append(paper_copy)

        return filtered
