"""HAL API client — French academic publications + native BibTeX.

HAL (Hyper Articles en Ligne) is a multi-disciplinary open-access archive
for scholarly deposits run by CNRS/CCSD.  It covers French academic
publications including theses, INRIA reports, and conference papers.

Endpoints used:
    - Search: ``http://api.archives-ouvertes.fr/search/?q=...&wt=json``
    - BibTeX: ``http://api.archives-ouvertes.fr/search/?q=halId_s:{id}&wt=bibtex``
"""

from __future__ import annotations

import re
from typing import Any

import httpx

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

__all__ = ["HALClient", "HALError"]


# ── Constants ────────────────────────────────────────────────────────────

_API_BASE = "http://api.archives-ouvertes.fr/search/"
_SEARCH_FIELDS = ",".join(
    [
        "halId_s",
        "title_s",
        "authFullName_s",
        "producedDateY_i",
        "doiId_s",
        "abstract_s",
        "journalTitle_s",
    ]
)


# ── Exceptions ───────────────────────────────────────────────────────────


class HALError(Exception):
    """Raised on HAL API failures."""


# ── Client ───────────────────────────────────────────────────────────────


class HALClient:
    """HAL API client for French academic publications.

    Provides search over the HAL Solr index and native BibTeX export.
    """

    # ── Public API ───────────────────────────────────────────────────

    def search(self, query: str, *, limit: int = 5) -> list[Paper]:
        """Search HAL documents by title/keywords.

        Args:
            query: Search query string.
            limit: Maximum results (default 5).

        Returns:
            List of ``Paper`` objects.

        Raises:
            HALError: On HTTP errors.
        """
        if not query.strip():
            return []

        try:
            resp = request_with_retry(
                "GET",
                _API_BASE,
                params={
                    "q": f"title_t:{query}",
                    "wt": "json",
                    "fl": _SEARCH_FIELDS,
                    "rows": str(limit),
                },
                timeout=30,
                follow_redirects=True,
            )
        except httpx.HTTPStatusError as exc:
            msg = f"HAL search failed: HTTP {exc.response.status_code}"
            raise HALError(msg) from exc
        except httpx.RequestError as exc:
            msg = f"HAL search failed: {exc}"
            raise HALError(msg) from exc

        data = resp.json()
        docs = data.get("response", {}).get("docs", [])
        return [self._normalize(doc) for doc in docs]

    def bibtex(self, hal_id: str) -> BibEntry:
        """Fetch native BibTeX for a HAL document.

        Args:
            hal_id: HAL identifier (e.g. ``hal-01234567``).

        Returns:
            ``BibEntry`` with the raw BibTeX string.

        Raises:
            HALError: On HTTP errors or empty response.
        """
        try:
            resp = request_with_retry(
                "GET",
                _API_BASE,
                params={
                    "q": f"halId_s:{hal_id}",
                    "wt": "bibtex",
                },
                timeout=30,
                follow_redirects=True,
            )
        except httpx.HTTPStatusError as exc:
            msg = (
                f"HAL BibTeX fetch failed for {hal_id}: HTTP {exc.response.status_code}"
            )
            raise HALError(msg) from exc
        except httpx.RequestError as exc:
            msg = f"HAL BibTeX fetch failed for {hal_id}: {exc}"
            raise HALError(msg) from exc

        raw = resp.text.strip()
        if not raw:
            msg = f"HAL returned empty BibTeX for {hal_id}"
            raise HALError(msg)

        return self._parse_bibtex(raw)

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

    @staticmethod
    def _normalize(doc: dict[str, Any]) -> Paper:
        """Convert a HAL Solr doc to a ``Paper``."""
        # Title — can be list or string
        title_raw = doc.get("title_s", "Untitled")
        if isinstance(title_raw, list):
            title = title_raw[0] if title_raw else "Untitled"
        else:
            title = str(title_raw)

        # Authors
        authors = doc.get("authFullName_s", ["Unknown"])
        if not authors:
            authors = ["Unknown"]

        year = doc.get("producedDateY_i")
        doi = doc.get("doiId_s")
        hal_id = doc.get("halId_s")

        # Abstract — can be list or string
        abstract_raw = doc.get("abstract_s")
        abstract: str | None = None
        if isinstance(abstract_raw, list) and abstract_raw:
            abstract = abstract_raw[0]
        elif isinstance(abstract_raw, str):
            abstract = abstract_raw

        venue = doc.get("journalTitle_s")

        return Paper(
            title=title,
            authors=authors,
            year=year,
            doi=doi,
            hal_id=hal_id,
            abstract=abstract,
            venue=venue,
            source="hal",
        )

    @staticmethod
    def _parse_bibtex(raw: str) -> BibEntry:
        """Parse a raw BibTeX string into a ``BibEntry``."""
        # Extract entry type and key from @type{key,
        type_key_match = re.search(r"@(\w+)\{([^,]+),", raw)
        entry_type = type_key_match.group(1).strip() if type_key_match else "misc"
        key = type_key_match.group(2).strip() if type_key_match else "unknown"

        # Extract DOI
        doi_match = re.search(r"DOI\s*=\s*\{([^}]+)\}", raw, re.IGNORECASE)
        doi = doi_match.group(1).strip() if doi_match else None

        return BibEntry(key=key, entry_type=entry_type, raw=raw, doi=doi)
