"""Venue enrichment — enrich arXiv BibEntries with publication venue metadata.

Uses Semantic Scholar (primary) and DBLP (fallback) to discover publication
venues for arXiv preprints, then promotes the BibTeX entry type and injects
the venue field (``booktitle``).
"""

from __future__ import annotations

import logging
import re

from httpx import HTTPError as _HTTPError

from axm_bib.clients.dblp import DBLPClient, DBLPError
from axm_bib.clients.semantic_scholar import (
    SemanticScholarClient,
    SemanticScholarError,
)
from axm_bib.models import BibEntry, Paper

__all__ = ["VenueEnricher"]

logger = logging.getLogger(__name__)

# All recoverable client/network errors for venue lookup fallback
_SEARCH_ERRORS = (
    OSError,
    ValueError,
    _HTTPError,
    DBLPError,
    SemanticScholarError,
)


# ── DRY helper: CoRR filter ──────────────────────────────────────────────


def _dblp_non_corr_venue(papers: list[Paper]) -> str | None:
    """Return the first non-CoRR venue from a DBLP results list.

    Args:
        papers: DBLP search results.

    Returns:
        Venue name if a real (non-CoRR) venue is found, else None.
    """
    for paper in papers:
        if paper.venue and paper.venue.lower() != "corr":
            return paper.venue
    return None


# ── Venue type mapping ────────────────────────────────────────────────────

_VENUE_TYPE_MAP: dict[str, str] = {
    # Common venue keywords → BibTeX entry types
    "conference": "inproceedings",
    "workshop": "inproceedings",
    "symposium": "inproceedings",
    "proceedings": "inproceedings",
    "journal": "article",
}


# ── VenueEnricher ─────────────────────────────────────────────────────────


class VenueEnricher:
    """Enrich arXiv BibEntries with venue metadata from S2/DBLP.

    Args:
        scholar: Semantic Scholar client.
        dblp: DBLP client.
    """

    def __init__(
        self,
        scholar: SemanticScholarClient,
        dblp: DBLPClient,
    ) -> None:
        self._scholar = scholar
        self._dblp = dblp

    def enrich(self, arxiv_id: str, entry: BibEntry) -> BibEntry:
        """Enrich an arXiv BibEntry with venue metadata from S2/DBLP.

        Queries Semantic Scholar (primary) and DBLP (fallback) to find
        publication venue for the given arXiv paper. If found, promotes
        the entry type and injects venue into the raw BibTeX.

        Args:
            arxiv_id: Bare arXiv identifier (e.g. ``"2210.02406"``).
            entry: Original BibEntry from arXiv.

        Returns:
            Enriched BibEntry if venue found, otherwise the original.
        """
        venue = self._lookup_venue(arxiv_id, title=entry.title)
        if not venue:
            return entry

        new_type = infer_entry_type(venue)
        raw = patch_raw_bibtex(entry.raw, venue, entry.entry_type, new_type)
        logger.info(
            "Enriched arXiv:%s with venue '%s' (type: %s)",
            arxiv_id,
            venue,
            new_type,
        )
        return BibEntry(
            key=entry.key,
            entry_type=new_type,
            raw=raw,
            doi=entry.doi,
            title=entry.title,
            authors=entry.authors,
            year=entry.year,
        )

    def _lookup_venue(self, arxiv_id: str, *, title: str | None = None) -> str | None:
        """Try S2 then DBLP to find venue for an arXiv paper.

        Falls back to title-based DBLP search when arXiv-ID search
        returns only CoRR preprint entries.
        """
        # S2 direct lookup by arXiv ID (not search — avoids rate limits)
        try:
            paper = self._scholar.get_paper(f"ARXIV:{arxiv_id}")
            if paper and paper.venue:
                return paper.venue
        except _SEARCH_ERRORS as exc:
            logger.debug("S2 venue lookup failed for %s: %s", arxiv_id, exc)

        # DBLP fallback — search by arXiv ID, skip CoRR preprint entries
        try:
            papers = self._dblp.search(arxiv_id, limit=5)
            venue = _dblp_non_corr_venue(papers)
            if venue:
                return venue
        except _SEARCH_ERRORS as exc:
            logger.debug("DBLP venue lookup failed for %s: %s", arxiv_id, exc)

        # Title-based DBLP fallback (when arXiv-ID only found CoRR)
        if title:
            try:
                papers = self._dblp.search(title, limit=5)
                venue = _dblp_non_corr_venue(papers)
                if venue:
                    return venue
            except _SEARCH_ERRORS as exc:
                logger.debug(
                    "DBLP title venue lookup failed for '%s': %s",
                    title,
                    exc,
                )

        return None


# ── Stateless helpers (formerly static methods on BibPipeline) ────────────


def infer_entry_type(venue: str) -> str:
    """Infer BibTeX entry type from venue name.

    Args:
        venue: Publication venue name.

    Returns:
        BibTeX entry type (e.g. ``"inproceedings"``).
    """
    venue_lower = venue.lower()
    for keyword, bib_type in _VENUE_TYPE_MAP.items():
        if keyword in venue_lower:
            return bib_type
    # Default: conference-style for named venues
    return "inproceedings"


def patch_raw_bibtex(
    raw: str,
    venue: str,
    old_type: str,
    new_type: str,
) -> str:
    """Inject venue and promote entry type in raw BibTeX string.

    Args:
        raw: Original raw BibTeX.
        venue: Venue name to inject.
        old_type: Original entry type (e.g. ``"misc"``).
        new_type: New entry type (e.g. ``"inproceedings"``).

    Returns:
        Patched raw BibTeX string.
    """
    # Promote entry type: @misc{ → @inproceedings{
    patched = re.sub(
        rf"@{re.escape(old_type)}\{{",
        f"@{new_type}{{",
        raw,
        count=1,
        flags=re.IGNORECASE,
    )
    # Inject venue field before closing brace
    venue_field = f"  booktitle = {{{venue}}},\n"
    # Find last closing brace and insert before it
    last_brace = patched.rfind("}")
    if last_brace != -1:
        patched = patched[:last_brace] + venue_field + patched[last_brace:]
    return patched
