import asyncio
import logging
import time
from urllib.parse import quote

import httpx

logger = logging.getLogger(__name__)

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


class AsyncRateLimiter:
    """Token-bucket rate limiter for async contexts."""

    def __init__(self, rate: float = 10.0, burst: int = 10):
        self._rate = rate
        self._burst = burst
        self._tokens = float(burst)
        self._last = time.monotonic()
        self._lock = asyncio.Lock()

    async def acquire(self) -> None:
        wait = 0.0
        async with self._lock:
            now = time.monotonic()
            elapsed = now - self._last
            self._tokens = min(self._burst, self._tokens + elapsed * self._rate)
            self._last = now
            if self._tokens < 1:
                wait = (1 - self._tokens) / self._rate
                self._tokens = 0
            else:
                self._tokens -= 1
        # Sleep outside the lock so other callers aren't blocked
        if wait > 0:
            await asyncio.sleep(wait)


class CrossRefClient:
    def __init__(self, http_client: httpx.AsyncClient, mailto: str = ""):
        self._client = http_client
        self._mailto = mailto
        self._limiter = AsyncRateLimiter(rate=10.0, burst=10)

    def _headers(self) -> dict[str, str]:
        headers = {"Accept": "application/json"}
        if self._mailto:
            headers["User-Agent"] = f"mcsci-hub/1.0 (mailto:{self._mailto})"
        return headers

    async def search(
        self,
        query: str,
        max_results: int = 10,
        year_from: int | None = None,
        year_to: int | None = None,
    ) -> list[dict]:
        await self._limiter.acquire()
        params: dict[str, str | int] = {
            "query": query,
            "rows": max_results,
            "select": "DOI,title,author,published,container-title,is-referenced-by-count,type",
        }
        filters = []
        if year_from:
            filters.append(f"from-pub-date:{year_from}")
        if year_to:
            filters.append(f"until-pub-date:{year_to}")
        if filters:
            params["filter"] = ",".join(filters)

        try:
            resp = await self._client.get(
                f"{BASE_URL}/works",
                params=params,
                headers=self._headers(),
            )
            resp.raise_for_status()
            data = resp.json()
            return data.get("message", {}).get("items", [])
        except httpx.HTTPError as e:
            logger.warning("CrossRef search failed: %s", e)
            return []

    async def get_work(self, doi: str) -> dict | None:
        await self._limiter.acquire()
        try:
            resp = await self._client.get(
                f"{BASE_URL}/works/{quote(doi, safe='/')}",
                headers=self._headers(),
            )
            resp.raise_for_status()
            data = resp.json()
            return data.get("message")
        except httpx.HTTPError as e:
            logger.warning("CrossRef lookup failed for %s: %s", doi, e)
            return None

    @staticmethod
    def parse_authors(item: dict) -> list[str]:
        authors = []
        for a in item.get("author", []):
            given = a.get("given", "")
            family = a.get("family", "")
            name = f"{given} {family}".strip()
            if name:
                authors.append(name)
        return authors

    @staticmethod
    def parse_year(item: dict) -> int | None:
        published = item.get("published") or item.get("published-print") or item.get("created")
        if published:
            parts = published.get("date-parts", [[]])
            if parts and parts[0]:
                return parts[0][0]
        return None

    @staticmethod
    def parse_title(item: dict) -> str:
        titles = item.get("title", [])
        return titles[0] if titles else ""

    @staticmethod
    def parse_journal(item: dict) -> str:
        journals = item.get("container-title", [])
        return journals[0] if journals else ""
