"""Data source clients for academic API lookups.

Each client implements the ``DataSourceProtocol`` and maps API responses to
``CitationRecord`` objects.  HTTP calls use an injected ``httpx.Client`` and
are wrapped with ``tenacity`` retry logic for transient failures (5xx, timeout).
"""

from __future__ import annotations

from typing import Protocol

import httpx
from defusedxml import ElementTree as SafeET
from tenacity import (
    retry,
    retry_if_exception,
    stop_after_attempt,
    wait_exponential,
)

from alice_cli.cite.models import CitationRecord, Identifier, IdentifierType

# ── Retry helpers ─────────────────────────────────────────────────────────────


def _is_transient(exc: BaseException) -> bool:
    """Return True for HTTP 5xx responses and timeouts."""
    if isinstance(exc, httpx.TimeoutException):
        return True
    return isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code >= 500


_retry_transient = retry(
    retry=retry_if_exception(_is_transient),
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=0.5, max=4),
    reraise=True,
)


# ── Protocol ──────────────────────────────────────────────────────────────────


class DataSourceProtocol(Protocol):
    """Interface that all data-source clients must satisfy."""

    def lookup(self, identifier: Identifier) -> CitationRecord | None: ...
    def search(self, query: str) -> list[CitationRecord]: ...


# ── Shared helpers ────────────────────────────────────────────────────────────


def _safe_get(data: dict, *keys: str, default: str = "") -> str:  # type: ignore[type-arg]
    """Drill into nested dicts, returning *default* on any miss."""
    current: object = data
    for k in keys:
        if isinstance(current, dict):
            current = current.get(k, default)
        else:
            return default
    return str(current) if current is not None else default


def _authors_from_crossref(items: list[dict[str, str]]) -> list[str]:
    """Convert CrossRef author dicts to ``Last, First`` strings."""
    out: list[str] = []
    for a in items:
        family = a.get("family", "")
        given = a.get("given", "")
        if family and given:
            out.append(f"{family}, {given}")
        elif family:
            out.append(family)
        elif given:
            out.append(given)
    return out


# ── CrossRef ──────────────────────────────────────────────────────────────────


class CrossRefSource:
    """CrossRef REST API client (https://api.crossref.org)."""

    _BASE = "https://api.crossref.org"

    def __init__(self, client: httpx.Client) -> None:
        self._client = client

    @_retry_transient
    def lookup(self, identifier: Identifier) -> CitationRecord | None:
        url = f"{self._BASE}/works/{identifier.value}"
        resp = self._client.get(url)
        if resp.status_code == 404:
            return None
        resp.raise_for_status()
        msg = resp.json().get("message", {})
        return self._to_record(msg)

    @_retry_transient
    def search(self, query: str) -> list[CitationRecord]:
        resp = self._client.get(f"{self._BASE}/works", params={"query": query, "rows": 5})
        if resp.status_code == 404:
            return []
        resp.raise_for_status()
        items = resp.json().get("message", {}).get("items", [])
        return [self._to_record(it) for it in items]

    @staticmethod
    def _to_record(msg: dict) -> CitationRecord:  # type: ignore[type-arg]
        title_parts = msg.get("title", [])
        title = title_parts[0] if title_parts else ""
        authors = _authors_from_crossref(msg.get("author", []))
        date_parts = msg.get("issued", {}).get("date-parts", [[]])
        year = str(date_parts[0][0]) if date_parts and date_parts[0] else ""
        return CitationRecord(
            title=title,
            authors=authors,
            year=year,
            doi=msg.get("DOI", ""),
            journal=(msg.get("container-title", []) or [""])[0],
            volume=msg.get("volume", ""),
            number=msg.get("issue", ""),
            pages=msg.get("page", ""),
            publisher=msg.get("publisher", ""),
            url=msg.get("URL", ""),
            entry_type=msg.get("type", "article"),
        )


# ── arXiv ─────────────────────────────────────────────────────────────────────


class ArXivSource:
    """arXiv Atom API client (http://export.arxiv.org/api)."""

    _BASE = "http://export.arxiv.org/api/query"

    def __init__(self, client: httpx.Client) -> None:
        self._client = client

    @_retry_transient
    def lookup(self, identifier: Identifier) -> CitationRecord | None:
        resp = self._client.get(self._BASE, params={"id_list": identifier.value})
        if resp.status_code == 404:
            return None
        resp.raise_for_status()
        records = self._parse_atom(resp.text)
        return records[0] if records else None

    @_retry_transient
    def search(self, query: str) -> list[CitationRecord]:
        resp = self._client.get(
            self._BASE, params={"search_query": f"all:{query}", "max_results": 5}
        )
        if resp.status_code == 404:
            return []
        resp.raise_for_status()
        return self._parse_atom(resp.text)

    @staticmethod
    def _parse_atom(xml_text: str) -> list[CitationRecord]:
        ns = {"atom": "http://www.w3.org/2005/Atom"}
        root = SafeET.fromstring(xml_text)
        records: list[CitationRecord] = []
        for entry in root.findall("atom:entry", ns):
            title_el = entry.find("atom:title", ns)
            title = (title_el.text or "").strip() if title_el is not None else ""
            # Skip empty placeholder entries returned by arXiv for invalid IDs
            if not title:
                continue
            summary_el = entry.find("atom:summary", ns)
            abstract = (summary_el.text or "").strip() if summary_el is not None else ""
            authors = [
                (a.find("atom:name", ns).text or "").strip()
                for a in entry.findall("atom:author", ns)
                if a.find("atom:name", ns) is not None
            ]
            published_el = entry.find("atom:published", ns)
            year = (published_el.text or "")[:4] if published_el is not None else ""
            # Extract arXiv ID from the entry id URL
            id_el = entry.find("atom:id", ns)
            arxiv_url = (id_el.text or "").strip() if id_el is not None else ""
            arxiv_id = arxiv_url.rsplit("/abs/", 1)[-1] if "/abs/" in arxiv_url else ""
            records.append(
                CitationRecord(
                    title=title,
                    authors=authors,
                    year=year,
                    abstract=abstract,
                    arxiv_id=arxiv_id,
                    url=arxiv_url,
                    entry_type="article",
                )
            )
        return records


# ── PubMed ────────────────────────────────────────────────────────────────────


class PubMedSource:
    """PubMed E-utilities API client (https://eutils.ncbi.nlm.nih.gov)."""

    _BASE = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils"

    def __init__(self, client: httpx.Client) -> None:
        self._client = client

    @_retry_transient
    def lookup(self, identifier: Identifier) -> CitationRecord | None:
        resp = self._client.get(
            f"{self._BASE}/efetch.fcgi",
            params={"db": "pubmed", "id": identifier.value, "retmode": "xml"},
        )
        if resp.status_code == 404:
            return None
        resp.raise_for_status()
        records = self._parse_pubmed_xml(resp.text)
        return records[0] if records else None

    @_retry_transient
    def search(self, query: str) -> list[CitationRecord]:
        # First search for IDs
        resp = self._client.get(
            f"{self._BASE}/esearch.fcgi",
            params={"db": "pubmed", "term": query, "retmax": 5, "retmode": "json"},
        )
        resp.raise_for_status()
        id_list = resp.json().get("esearchresult", {}).get("idlist", [])
        if not id_list:
            return []
        # Fetch details
        resp = self._client.get(
            f"{self._BASE}/efetch.fcgi",
            params={"db": "pubmed", "id": ",".join(id_list), "retmode": "xml"},
        )
        resp.raise_for_status()
        return self._parse_pubmed_xml(resp.text)

    @staticmethod
    def _parse_pubmed_xml(xml_text: str) -> list[CitationRecord]:
        root = SafeET.fromstring(xml_text)
        records: list[CitationRecord] = []
        for article in root.iter("PubmedArticle"):
            medline = article.find(".//MedlineCitation")
            if medline is None:
                continue
            art = medline.find("Article")
            if art is None:
                continue
            title_el = art.find("ArticleTitle")
            title = (title_el.text or "") if title_el is not None else ""
            authors: list[str] = []
            for au in art.findall(".//Author"):
                last = au.findtext("LastName", "")
                fore = au.findtext("ForeName", "")
                if last and fore:
                    authors.append(f"{last}, {fore}")
                elif last:
                    authors.append(last)
            journal_el = art.find(".//Journal/Title")
            journal = (journal_el.text or "") if journal_el is not None else ""
            year_el = art.find(".//PubDate/Year")
            year = (year_el.text or "") if year_el is not None else ""
            volume_el = art.find(".//Volume")
            volume = (volume_el.text or "") if volume_el is not None else ""
            issue_el = art.find(".//Issue")
            issue = (issue_el.text or "") if issue_el is not None else ""
            pages_el = art.find(".//MedlinePgn")
            pages = (pages_el.text or "") if pages_el is not None else ""
            pmid_el = medline.find("PMID")
            pmid = (pmid_el.text or "") if pmid_el is not None else ""
            doi = ""
            for eid in article.findall(".//ArticleId"):
                if eid.get("IdType") == "doi":
                    doi = eid.text or ""
                    break
            records.append(
                CitationRecord(
                    title=title,
                    authors=authors,
                    year=year,
                    journal=journal,
                    volume=volume,
                    number=issue,
                    pages=pages,
                    doi=doi,
                    pmid=pmid,
                    entry_type="article",
                )
            )
        return records


# ── OpenLibrary ───────────────────────────────────────────────────────────────


class OpenLibrarySource:
    """OpenLibrary API client (https://openlibrary.org)."""

    _BASE = "https://openlibrary.org"

    def __init__(self, client: httpx.Client) -> None:
        self._client = client

    @_retry_transient
    def lookup(self, identifier: Identifier) -> CitationRecord | None:
        resp = self._client.get(
            f"{self._BASE}/api/books.json",
            params={"bibkeys": f"ISBN:{identifier.value}", "jscmd": "data", "format": "json"},
        )
        resp.raise_for_status()
        data = resp.json()
        key = f"ISBN:{identifier.value}"
        if key not in data:
            return None
        return self._to_record(data[key], identifier.value)

    @_retry_transient
    def search(self, query: str) -> list[CitationRecord]:
        resp = self._client.get(
            f"{self._BASE}/search.json",
            params={"q": query, "limit": 5},
        )
        resp.raise_for_status()
        docs = resp.json().get("docs", [])
        records: list[CitationRecord] = []
        for doc in docs[:5]:
            isbn_list = doc.get("isbn", [])
            isbn = isbn_list[0] if isbn_list else ""
            records.append(
                CitationRecord(
                    title=doc.get("title", ""),
                    authors=[a for a in doc.get("author_name", [])],
                    year=str(doc.get("first_publish_year", "")),
                    publisher=doc.get("publisher", [""])[0] if doc.get("publisher") else "",
                    isbn=isbn,
                    entry_type="book",
                )
            )
        return records

    @staticmethod
    def _to_record(data: dict, isbn: str) -> CitationRecord:  # type: ignore[type-arg]
        authors = [a.get("name", "") for a in data.get("authors", [])]
        publishers = data.get("publishers", [])
        publisher = publishers[0].get("name", "") if publishers else ""
        return CitationRecord(
            title=data.get("title", ""),
            authors=authors,
            year=str(data.get("publish_date", "")),
            publisher=publisher,
            isbn=isbn,
            url=data.get("url", ""),
            entry_type="book",
        )


# ── GitHub ────────────────────────────────────────────────────────────────────


class GitHubSource:
    """GitHub REST API client (https://api.github.com)."""

    _BASE = "https://api.github.com"

    def __init__(self, client: httpx.Client) -> None:
        self._client = client

    @_retry_transient
    def lookup(self, identifier: Identifier) -> CitationRecord | None:
        # identifier.value is like "https://github.com/owner/repo"
        parts = identifier.value.rstrip("/").split("/")
        if len(parts) < 2:
            return None
        owner, repo = parts[-2], parts[-1]
        resp = self._client.get(f"{self._BASE}/repos/{owner}/{repo}")
        if resp.status_code == 404:
            return None
        resp.raise_for_status()
        data = resp.json()
        return CitationRecord(
            title=data.get("name", ""),
            authors=[data.get("owner", {}).get("login", "")],
            year=data.get("created_at", "")[:4],
            url=data.get("html_url", ""),
            abstract=data.get("description", "") or "",
            entry_type="misc",
        )

    @_retry_transient
    def search(self, query: str) -> list[CitationRecord]:
        resp = self._client.get(
            f"{self._BASE}/search/repositories",
            params={"q": query, "per_page": 5},
        )
        resp.raise_for_status()
        items = resp.json().get("items", [])
        return [
            CitationRecord(
                title=it.get("name", ""),
                authors=[it.get("owner", {}).get("login", "")],
                year=it.get("created_at", "")[:4],
                url=it.get("html_url", ""),
                abstract=it.get("description", "") or "",
                entry_type="misc",
            )
            for it in items[:5]
        ]


# ── Zenodo ────────────────────────────────────────────────────────────────────


class ZenodoSource:
    """Zenodo REST API client (https://zenodo.org/api)."""

    _BASE = "https://zenodo.org/api"

    def __init__(self, client: httpx.Client) -> None:
        self._client = client

    @_retry_transient
    def lookup(self, identifier: Identifier) -> CitationRecord | None:
        # Extract record ID from Zenodo DOI (10.5281/zenodo.NNNNN)
        parts = identifier.value.split("zenodo.")
        if len(parts) < 2:
            return None
        record_id = parts[-1]
        resp = self._client.get(f"{self._BASE}/records/{record_id}")
        if resp.status_code == 404:
            return None
        resp.raise_for_status()
        return self._to_record(resp.json())

    @_retry_transient
    def search(self, query: str) -> list[CitationRecord]:
        resp = self._client.get(
            f"{self._BASE}/records",
            params={"q": query, "size": 5},
        )
        resp.raise_for_status()
        hits = resp.json().get("hits", {}).get("hits", [])
        return [self._to_record(h) for h in hits[:5]]

    @staticmethod
    def _to_record(data: dict) -> CitationRecord:  # type: ignore[type-arg]
        metadata = data.get("metadata", {})
        creators = metadata.get("creators", [])
        authors = [c.get("name", "") for c in creators]
        return CitationRecord(
            title=metadata.get("title", ""),
            authors=authors,
            year=metadata.get("publication_date", "")[:4],
            doi=metadata.get("doi", ""),
            url=data.get("links", {}).get("html", ""),
            publisher="Zenodo",
            entry_type=metadata.get("resource_type", {}).get("type", "misc"),
        )


# ── DataCite ──────────────────────────────────────────────────────────────────


class DataCiteSource:
    """DataCite REST API client (https://api.datacite.org)."""

    _BASE = "https://api.datacite.org"

    def __init__(self, client: httpx.Client) -> None:
        self._client = client

    @_retry_transient
    def lookup(self, identifier: Identifier) -> CitationRecord | None:
        resp = self._client.get(f"{self._BASE}/dois/{identifier.value}")
        if resp.status_code == 404:
            return None
        resp.raise_for_status()
        data = resp.json().get("data", {}).get("attributes", {})
        return self._to_record(data)

    @_retry_transient
    def search(self, query: str) -> list[CitationRecord]:
        resp = self._client.get(
            f"{self._BASE}/dois",
            params={"query": query, "page[size]": 5},
        )
        resp.raise_for_status()
        items = resp.json().get("data", [])
        return [self._to_record(it.get("attributes", {})) for it in items[:5]]

    @staticmethod
    def _to_record(attrs: dict) -> CitationRecord:  # type: ignore[type-arg]
        creators = attrs.get("creators", [])
        authors = [c.get("name", "") for c in creators]
        titles = attrs.get("titles", [])
        title = titles[0].get("title", "") if titles else ""
        return CitationRecord(
            title=title,
            authors=authors,
            year=str(attrs.get("publicationYear", "")),
            doi=attrs.get("doi", ""),
            publisher=attrs.get("publisher", ""),
            url=attrs.get("url", ""),
            entry_type=attrs.get("types", {}).get("bibtex", "misc"),
        )


# ── Semantic Scholar ──────────────────────────────────────────────────────────


class SemanticScholarSource:
    """Semantic Scholar Academic Graph API client (https://api.semanticscholar.org).

    Supports lookup by S2 corpus ID and free-text search.  The API is free
    and requires no key for moderate request volumes.
    """

    _BASE = "https://api.semanticscholar.org/graph/v1"
    _FIELDS = "title,authors,year,externalIds,journal,venue,url,abstract"

    def __init__(self, client: httpx.Client) -> None:
        self._client = client

    @_retry_transient
    def lookup(self, identifier: Identifier) -> CitationRecord | None:
        url = f"{self._BASE}/paper/CorpusID:{identifier.value}"
        resp = self._client.get(url, params={"fields": self._FIELDS})
        if resp.status_code == 404:
            return None
        resp.raise_for_status()
        return self._to_record(resp.json())

    @_retry_transient
    def search(self, query: str) -> list[CitationRecord]:
        resp = self._client.get(
            f"{self._BASE}/paper/search",
            params={"query": query, "limit": 5, "fields": self._FIELDS},
        )
        if resp.status_code == 404:
            return []
        resp.raise_for_status()
        papers = resp.json().get("data", [])
        return [self._to_record(p) for p in papers if p]

    @staticmethod
    def _to_record(data: dict) -> CitationRecord:  # type: ignore[type-arg]
        authors = [a.get("name", "") for a in data.get("authors", [])]
        ext_ids = data.get("externalIds") or {}
        journal_info = data.get("journal") or {}
        return CitationRecord(
            title=data.get("title", "") or "",
            authors=authors,
            year=str(data.get("year", "") or ""),
            doi=ext_ids.get("DOI", ""),
            arxiv_id=ext_ids.get("ArXiv", ""),
            pmid=ext_ids.get("PubMed", ""),
            journal=journal_info.get("name", "") or data.get("venue", "") or "",
            volume=str(journal_info.get("volume", "") or ""),
            pages=journal_info.get("pages", "") or "",
            url=data.get("url", "") or "",
            abstract=data.get("abstract", "") or "",
            entry_type="article",
        )


# ── DBLP ──────────────────────────────────────────────────────────────────────


class DBLPSource:
    """DBLP computer science bibliography API client (https://dblp.org).

    Supports lookup by DBLP key (e.g. ``journals/corr/VaswaniSPUJGKP17``)
    and free-text search.  The API is free and requires no key.
    """

    _BASE = "https://dblp.org"

    def __init__(self, client: httpx.Client) -> None:
        self._client = client

    @_retry_transient
    def lookup(self, identifier: Identifier) -> CitationRecord | None:
        url = f"{self._BASE}/rec/{identifier.value}.xml"
        resp = self._client.get(url)
        if resp.status_code == 404:
            return None
        resp.raise_for_status()
        records = self._parse_dblp_xml(resp.text)
        return records[0] if records else None

    @_retry_transient
    def search(self, query: str) -> list[CitationRecord]:
        resp = self._client.get(
            f"{self._BASE}/search/publ/api",
            params={"q": query, "h": 5, "format": "xml"},
        )
        if resp.status_code == 404:
            return []
        resp.raise_for_status()
        return self._parse_dblp_search_xml(resp.text)

    @staticmethod
    def _parse_dblp_xml(xml_text: str) -> list[CitationRecord]:
        root = SafeET.fromstring(xml_text)
        records: list[CitationRecord] = []
        for tag in (
            "article",
            "inproceedings",
            "proceedings",
            "book",
            "incollection",
            "phdthesis",
            "mastersthesis",
        ):
            for entry in root.iter(tag):
                title_el = entry.find("title")
                title = (title_el.text or "").strip() if title_el is not None else ""
                authors = [(a.text or "").strip() for a in entry.findall("author") if a.text]
                year_el = entry.find("year")
                year = (year_el.text or "").strip() if year_el is not None else ""
                journal_el = entry.find("journal")
                journal = (journal_el.text or "").strip() if journal_el is not None else ""
                booktitle_el = entry.find("booktitle")
                if not journal and booktitle_el is not None:
                    journal = (booktitle_el.text or "").strip()
                volume_el = entry.find("volume")
                volume = (volume_el.text or "").strip() if volume_el is not None else ""
                pages_el = entry.find("pages")
                pages = (pages_el.text or "").strip() if pages_el is not None else ""
                number_el = entry.find("number")
                number = (number_el.text or "").strip() if number_el is not None else ""
                doi_el = entry.find("ee")
                doi = ""
                url = ""
                if doi_el is not None and doi_el.text:
                    ee_text = doi_el.text.strip()
                    if "doi.org/" in ee_text:
                        doi = ee_text.split("doi.org/", 1)[-1]
                    url = ee_text
                records.append(
                    CitationRecord(
                        title=title,
                        authors=authors,
                        year=year,
                        journal=journal,
                        volume=volume,
                        number=number,
                        pages=pages,
                        doi=doi,
                        url=url,
                        entry_type=tag if tag != "inproceedings" else "conference",
                    )
                )
        return records

    @staticmethod
    def _parse_dblp_search_xml(xml_text: str) -> list[CitationRecord]:
        root = SafeET.fromstring(xml_text)
        records: list[CitationRecord] = []
        for hit in root.findall(".//hit"):
            info = hit.find("info")
            if info is None:
                continue
            title_el = info.find("title")
            title = (title_el.text or "").strip() if title_el is not None else ""
            authors_el = info.find("authors")
            authors: list[str] = []
            if authors_el is not None:
                for a in authors_el.findall("author"):
                    if a.text:
                        authors.append(a.text.strip())
            year_el = info.find("year")
            year = (year_el.text or "").strip() if year_el is not None else ""
            venue_el = info.find("venue")
            venue = (venue_el.text or "").strip() if venue_el is not None else ""
            doi_el = info.find("doi")
            doi = (doi_el.text or "").strip() if doi_el is not None else ""
            url_el = info.find("url")
            url = (url_el.text or "").strip() if url_el is not None else ""
            type_el = info.find("type")
            entry_type = (
                (type_el.text or "article").strip().lower() if type_el is not None else "article"
            )
            if entry_type == "conference and workshop papers":
                entry_type = "conference"
            elif entry_type == "journal articles":
                entry_type = "article"
            records.append(
                CitationRecord(
                    title=title,
                    authors=authors,
                    year=year,
                    journal=venue,
                    doi=doi,
                    url=url,
                    entry_type=entry_type,
                )
            )
        return records


# ── ORCID ─────────────────────────────────────────────────────────────────────


class ORCIDSource:
    """ORCID public API client (https://pub.orcid.org).

    Supports lookup by ORCID iD to retrieve an author's works, and free-text
    search across the ORCID registry.  The public API requires no key.
    """

    _BASE = "https://pub.orcid.org/v3.0"

    def __init__(self, client: httpx.Client) -> None:
        self._client = client

    @_retry_transient
    def lookup(self, identifier: Identifier) -> CitationRecord | None:
        """Retrieve the most recent work from an ORCID profile."""
        resp = self._client.get(
            f"{self._BASE}/{identifier.value}/works",
            headers={"Accept": "application/json"},
        )
        if resp.status_code == 404:
            return None
        resp.raise_for_status()
        groups = resp.json().get("group", [])
        if not groups:
            return None
        return self._work_summary_to_record(groups[0], identifier.value)

    @_retry_transient
    def search(self, query: str) -> list[CitationRecord]:
        """Search ORCID for researchers and return their most recent work."""
        resp = self._client.get(
            f"{self._BASE}/search",
            params={"q": query, "rows": 5},
            headers={"Accept": "application/json"},
        )
        if resp.status_code == 404:
            return []
        resp.raise_for_status()
        results = resp.json().get("result", []) or []
        records: list[CitationRecord] = []
        for r in results[:5]:
            orcid_id = r.get("orcid-identifier", {}).get("path", "")
            if not orcid_id:
                continue
            rec = self.lookup(Identifier(type=IdentifierType.ORCID, value=orcid_id))
            if rec:
                records.append(rec)
        return records

    @staticmethod
    def _work_summary_to_record(group: dict, orcid_id: str) -> CitationRecord:  # type: ignore[type-arg]
        summaries = group.get("work-summary", [])
        if not summaries:
            return CitationRecord(entry_type="article")
        summary = summaries[0]
        title_obj = summary.get("title", {})
        title_val = title_obj.get("title", {}).get("value", "") if title_obj else ""
        pub_date = summary.get("publication-date") or {}
        year = str(pub_date.get("year", {}).get("value", "")) if pub_date.get("year") else ""
        journal_el = summary.get("journal-title")
        journal = journal_el.get("value", "") if journal_el else ""
        doi = ""
        ext_ids = summary.get("external-ids", {}).get("external-id", [])
        for eid in ext_ids:
            if eid.get("external-id-type") == "doi":
                doi = eid.get("external-id-value", "")
                break
        entry_type = summary.get("type", "journal-article")
        if entry_type == "journal-article":
            entry_type = "article"
        elif entry_type == "conference-paper":
            entry_type = "conference"
        elif entry_type in ("book", "book-chapter"):
            entry_type = "book"
        else:
            entry_type = "misc"
        return CitationRecord(
            title=title_val,
            year=year,
            journal=journal,
            doi=doi,
            url=f"https://orcid.org/{orcid_id}",
            entry_type=entry_type,
        )
