"""Semantic Scholar API client — direct httpx calls.

Provides title-based paper discovery from the Semantic Scholar
academic graph (200M+ papers). Uses the public REST API directly
instead of the ``semanticscholar`` wrapper library.
"""

from __future__ import annotations

import logging
from typing import Any

from axm_bib.http import request_with_retry
from axm_bib.models import Paper

logger = logging.getLogger(__name__)

__all__ = ["SemanticScholarClient", "SemanticScholarError"]

_BASE_URL = "https://api.semanticscholar.org/graph/v1"
_RECS_URL = "https://api.semanticscholar.org/recommendations/v1"
_HTTP_NOT_FOUND = 404
_SEARCH_FIELDS = ",".join(
    [
        "title",
        "authors",
        "year",
        "externalIds",
        "abstract",
        "venue",
        "citationCount",
        "isOpenAccess",
        "openAccessPdf",
    ]
)


class SemanticScholarError(Exception):
    """Error raised by Semantic Scholar operations."""


class SemanticScholarClient:
    """Semantic Scholar API client (direct httpx).

    Args:
        api_key: Optional API key for higher rate limits.
    """

    def __init__(self, api_key: str | None = None) -> None:
        self._headers: dict[str, str] = {}
        if api_key:
            self._headers["x-api-key"] = api_key

    # ── Private ──────────────────────────────────────────────────────

    def _graph_request(self, path: str, *, limit: int) -> list[dict[str, Any]]:
        """Execute a Graph API GET and return the `data` list."""
        try:
            resp = request_with_retry(
                "GET",
                f"{_BASE_URL}/{path}",
                params={"fields": _SEARCH_FIELDS, "limit": limit},
                headers=self._headers,
                timeout=30.0,
            )
        except Exception as exc:
            msg = f"S2 graph request failed ({path}): {exc}"
            raise SemanticScholarError(msg) from exc

        result: list[dict[str, Any]] = resp.json().get("data", [])
        return result

    # ── Graph API ─────────────────────────────────────────────────────

    def citations(self, paper_id: str, *, limit: int = 10) -> list[Paper]:
        """Get papers that cite the given paper.

        Args:
            paper_id: S2 paper ID, DOI, arXiv ID, or corpus ID.
            limit: Maximum results (default 10).

        Returns:
            List of citing ``Paper`` objects.

        Raises:
            SemanticScholarError: On API failure (including 429).
        """
        data = self._graph_request(f"paper/{paper_id}/citations", limit=limit)
        papers: list[Paper] = []
        for item in data:
            inner = item.get("citingPaper", {})
            paper = self._normalize(inner)
            if paper is not None:
                papers.append(paper)
        return papers

    def references(self, paper_id: str, *, limit: int = 10) -> list[Paper]:
        """Get papers cited by the given paper.

        Args:
            paper_id: S2 paper ID, DOI, arXiv ID, or corpus ID.
            limit: Maximum results (default 10).

        Returns:
            List of referenced ``Paper`` objects.

        Raises:
            SemanticScholarError: On API failure (including 429).
        """
        data = self._graph_request(f"paper/{paper_id}/references", limit=limit)
        papers: list[Paper] = []
        for item in data:
            inner = item.get("citedPaper", {})
            paper = self._normalize(inner)
            if paper is not None:
                papers.append(paper)
        return papers

    def recommendations(self, paper_id: str, *, limit: int = 10) -> list[Paper]:
        """Get recommended papers similar to the given paper.

        Uses the S2 Recommendations API.

        Args:
            paper_id: S2 paper ID, DOI, arXiv ID, or corpus ID.
            limit: Maximum results (default 10).

        Returns:
            List of recommended ``Paper`` objects.

        Raises:
            SemanticScholarError: On API failure (including 429).
        """
        url = f"{_RECS_URL}/papers/forpaper/{paper_id}"
        try:
            resp = request_with_retry(
                "GET",
                url,
                params={"fields": _SEARCH_FIELDS, "limit": limit},
                headers=self._headers,
                timeout=30.0,
            )
        except Exception as exc:
            msg = f"S2 recommendations failed for {paper_id}: {exc}"
            raise SemanticScholarError(msg) from exc

        items = resp.json().get("recommendedPapers", [])
        papers: list[Paper] = []
        for item in items:
            paper = self._normalize(item)
            if paper is not None:
                papers.append(paper)
        return papers

    # ── Direct lookup ──────────────────────────────────────────────────

    def get_paper(self, paper_id: str) -> Paper | None:
        """Look up a single paper by identifier (direct API, not search).

        Uses the ``/paper/{paper_id}`` endpoint which accepts S2 paper IDs,
        DOIs (``DOI:...``), arXiv IDs (``ARXIV:...``), and other formats.
        More reliable than ``search_paper`` for exact lookups.

        Args:
            paper_id: S2-supported identifier (e.g. ``"ARXIV:2210.02406"``).

        Returns:
            Normalized ``Paper`` or ``None`` if not found.

        Raises:
            SemanticScholarError: On API failure (network, 5xx, 429).
        """
        try:
            resp = request_with_retry(
                "GET",
                f"{_BASE_URL}/paper/{paper_id}",
                params={"fields": _SEARCH_FIELDS},
                headers=self._headers,
                timeout=30.0,
                retry_on=(429, 500, 502, 503),
            )
            if resp.status_code == _HTTP_NOT_FOUND:
                return None
        except Exception as exc:
            msg = f"S2 paper lookup failed for {paper_id}: {exc}"
            raise SemanticScholarError(msg) from exc

        return self._normalize(resp.json())

    # ── Search ────────────────────────────────────────────────────────

    def search_paper(self, title: str, limit: int = 5) -> list[Paper]:
        """Search papers by title/keywords.

        Args:
            title: Search query string.
            limit: Maximum number of results.

        Returns:
            List of normalized Paper objects.

        Raises:
            SemanticScholarError: On API failure.
        """
        if not title.strip():
            return []

        params: dict[str, str | int] = {
            "query": title,
            "limit": limit,
            "fields": _SEARCH_FIELDS,
        }

        try:
            resp = request_with_retry(
                "GET",
                f"{_BASE_URL}/paper/search",
                params=params,
                headers=self._headers,
                timeout=30.0,
            )
        except Exception as exc:
            msg = f"Semantic Scholar search failed: {exc}"
            raise SemanticScholarError(msg) from exc

        data = resp.json().get("data", [])

        papers: list[Paper] = []
        for item in data:
            paper = self._normalize(item)
            if paper is not None:
                papers.append(paper)

        return papers

    @staticmethod
    def _normalize(item: dict[str, Any]) -> Paper | None:
        """Convert a Semantic Scholar result dict to a Paper model."""
        try:
            external_ids = item.get("externalIds") or {}
            doi = external_ids.get("DOI")
            arxiv_id = external_ids.get("ArXiv")

            raw_authors = item.get("authors") or []
            authors = [a.get("name", "Unknown") for a in raw_authors if a]
            if not authors:
                authors = ["Unknown"]

            oa_pdf = item.get("openAccessPdf")
            pdf_url: str | None = None
            if isinstance(oa_pdf, dict):
                pdf_url = oa_pdf.get("url")

            return Paper(
                title=item.get("title") or "Untitled",
                authors=authors,
                year=item.get("year"),
                doi=doi,
                arxiv_id=arxiv_id,
                abstract=item.get("abstract"),
                venue=item.get("venue"),
                citation_count=item.get("citationCount"),
                open_access_pdf=pdf_url,
                source="semantic_scholar",
            )
        except Exception:  # noqa: BLE001
            logger.debug(
                "Failed to normalize S2 result: %s",
                item.get("title", "?"),
                exc_info=True,
            )
            return None
