import logging
from urllib.parse import quote

import httpx

logger = logging.getLogger(__name__)

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


class OpenAlexClient:
    def __init__(self, http_client: httpx.AsyncClient, mailto: str = ""):
        self._client = http_client
        self._mailto = mailto

    def _params(self, extra: dict | None = None) -> dict:
        params: dict[str, str] = {}
        if self._mailto:
            params["mailto"] = self._mailto
        if extra:
            params.update(extra)
        return params

    async def search(
        self,
        query: str,
        max_results: int = 10,
        year_from: int | None = None,
        year_to: int | None = None,
        open_access_only: bool = False,
    ) -> list[dict]:
        params = self._params({"search": query, "per_page": str(max_results)})

        filters = []
        if year_from and year_to:
            filters.append(f"publication_year:{year_from}-{year_to}")
        elif year_from:
            filters.append(f"publication_year:{year_from}-")
        elif year_to:
            filters.append(f"publication_year:-{year_to}")
        if open_access_only:
            filters.append("open_access.is_oa:true")
        if filters:
            params["filter"] = ",".join(filters)

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

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

    @staticmethod
    def decode_abstract(inverted_index: dict | None) -> str:
        """Reconstruct abstract from OpenAlex inverted index format.

        OpenAlex stores abstracts as {word: [positions]} — we rebuild the
        original text by placing each word at its position(s).
        """
        if not inverted_index:
            return ""
        position_word: list[tuple[int, str]] = []
        for word, positions in inverted_index.items():
            for pos in positions:
                position_word.append((pos, word))
        position_word.sort(key=lambda x: x[0])
        return " ".join(w for _, w in position_word)

    @staticmethod
    def parse_authors(item: dict) -> list[str]:
        authors = []
        for authorship in item.get("authorships", []):
            author = authorship.get("author", {})
            name = author.get("display_name", "")
            if name:
                authors.append(name)
        return authors

    @staticmethod
    def parse_year(item: dict) -> int | None:
        return item.get("publication_year")

    @staticmethod
    def parse_title(item: dict) -> str:
        return item.get("title", "") or ""

    @staticmethod
    def parse_journal(item: dict) -> str:
        source = item.get("primary_location", {})
        if source:
            src = source.get("source")
            if src:
                return src.get("display_name", "")
        return ""

    @staticmethod
    def parse_doi(item: dict) -> str:
        doi = item.get("doi", "") or ""
        return doi.replace("https://doi.org/", "")

    @staticmethod
    def get_oa_url(item: dict) -> str:
        oa = item.get("open_access", {})
        return oa.get("oa_url", "") or ""

    @staticmethod
    def is_oa(item: dict) -> bool:
        oa = item.get("open_access", {})
        return oa.get("is_oa", False)
