"""Minimal ORCID Public API client.

No Django deps — returns parsed dicts, raises typed exceptions. This keeps
the module unit-testable without a DB or settings context. Backoff / retry
policy lives in the service layer.
"""
from __future__ import annotations

import json
import logging
import urllib.error
import urllib.request
from dataclasses import dataclass
from typing import Any

logger = logging.getLogger(__name__)


class OrcidError(Exception):
    """Base class for ORCID client errors."""


class OrcidNotFound(OrcidError):
    """ORCID record not found (404)."""


class OrcidRateLimited(OrcidError):
    """ORCID returned 429 rate-limit error."""


class OrcidUpstreamError(OrcidError):
    """Anything else (5xx, timeout, parse error)."""


@dataclass(frozen=True)
class OrcidWork:
    put_code: str
    title: str
    publication_type: str
    doi: str
    year: int | None
    payload: dict[str, Any]


def _http_get_json(url: str, timeout: int) -> dict[str, Any]:
    req = urllib.request.Request(
        url,
        headers={
            "Accept": "application/json",
            "User-Agent": "RUFORUM-IILMP/alumni-sync/1.0",
        },
    )
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:  # noqa: S310
            body = resp.read()
            return json.loads(body.decode("utf-8"))
    except urllib.error.HTTPError as exc:
        if exc.code == 404:
            raise OrcidNotFound(str(exc)) from exc
        if exc.code == 429:
            raise OrcidRateLimited(str(exc)) from exc
        raise OrcidUpstreamError(f"HTTP {exc.code}: {exc}") from exc
    except urllib.error.URLError as exc:
        raise OrcidUpstreamError(f"URL error: {exc}") from exc
    except json.JSONDecodeError as exc:  # pragma: no cover - defensive
        raise OrcidUpstreamError(f"Bad JSON: {exc}") from exc


def _parse_work(group: dict[str, Any]) -> OrcidWork | None:
    summaries = group.get("work-summary") or []
    if not summaries:
        return None
    summary = summaries[0]
    title = ""
    title_obj = summary.get("title") or {}
    title_block = title_obj.get("title") or {}
    if isinstance(title_block, dict):
        title = title_block.get("value") or ""
    publication_type = (summary.get("type") or "other").lower()
    put_code = summary.get("put-code")
    doi = ""
    for eid in (summary.get("external-ids") or {}).get("external-id", []) or []:
        if (eid.get("external-id-type") or "").lower() == "doi":
            doi = eid.get("external-id-value") or ""
            break
    year = None
    date_block = summary.get("publication-date") or {}
    year_field = (date_block or {}).get("year") or {}
    if isinstance(year_field, dict) and year_field.get("value"):
        try:
            year = int(year_field["value"])
        except (TypeError, ValueError):
            year = None
    if not put_code or not title:
        return None
    return OrcidWork(
        put_code=str(put_code),
        title=title,
        publication_type=publication_type,
        doi=doi,
        year=year,
        payload=summary,
    )


def fetch_works(orcid_id: str, *, base_url: str, timeout: int) -> list[OrcidWork]:
    """Return parsed work summaries for an ORCID iD.

    Raises :class:`OrcidNotFound`, :class:`OrcidRateLimited`, or
    :class:`OrcidUpstreamError` on failure. Empty list on record-with-no-works.
    """
    if not orcid_id:
        raise OrcidError("orcid_id required")
    url = f"{base_url.rstrip('/')}/v3.0/{orcid_id}/works"
    data = _http_get_json(url, timeout=timeout)
    groups = (data or {}).get("group") or []
    out: list[OrcidWork] = []
    for group in groups:
        parsed = _parse_work(group)
        if parsed is not None:
            out.append(parsed)
    return out
