"""
DOAJ (Directory of Open Access Journals) API v4 client for Accordance Port.

DOAJ is the authoritative index of open-access journals — over 20,000 journals
and 10 million articles, all freely available. This client targets theology and
religious studies content: biblical studies, systematic theology, church history,
practical theology, and related disciplines.

Zero external dependencies. Uses stdlib only (urllib.request + json).
Rate-limited to 2 req/sec per DOAJ's documented limit.

API v4 is Elasticsearch-based with query-string syntax.
Docs: https://doaj.org/api/v4/docs
Base: https://doaj.org/api/

Endpoints used:
    GET /search/articles/{query}?pageSize=N       — full-text article search
    GET /articles/{article_id}                     — single article metadata
    GET /search/journals/{query}?pageSize=N        — journal title/publisher search
    GET /journals/{journal_id}                     — single journal metadata

Thread-safe for MCP server use.
"""

from __future__ import annotations

import json
import logging
import time
import urllib.error
import urllib.request
from typing import Any, List, Optional
from urllib.parse import quote

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

DOAJ_BASE = "https://doaj.org/api"

_SEARCH_ARTICLES_TIMEOUT = 15  # seconds
_GET_ARTICLE_TIMEOUT = 15
_SEARCH_JOURNALS_TIMEOUT = 15
_GET_JOURNAL_TIMEOUT = 15

_USER_AGENT = "AccordancePort/1.0"

# DOAJ rate limit: 2 req/sec (burst of 5 queued, but we stay conservative).
_MIN_INTERVAL = 0.5  # seconds between requests

# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

_last_request_time: float = 0.0


def _rate_limit() -> None:
    """Sleep if needed to respect DOAJ's 2 req/sec rate limit."""
    global _last_request_time
    elapsed = time.monotonic() - _last_request_time
    if elapsed < _MIN_INTERVAL:
        time.sleep(_MIN_INTERVAL - elapsed)
    _last_request_time = time.monotonic()


def _http_get(url: str, timeout: int = 15) -> Optional[bytes]:
    """Perform a GET request with rate limiting. Returns body bytes, or None on failure."""
    _rate_limit()
    try:
        req = urllib.request.Request(url, headers={"User-Agent": _USER_AGENT})
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            if resp.status != 200:
                logger.warning("DOAJ GET %s returned HTTP %d", url, resp.status)
                return None
            return resp.read()
    except (urllib.error.URLError, urllib.error.HTTPError, OSError) as e:
        logger.warning("DOAJ GET %s failed: %s", url, e)
        return None


def _parse_json(raw: Optional[bytes]) -> Optional[Any]:
    """Parse JSON bytes. Returns parsed data or None on failure."""
    if raw is None:
        return None
    try:
        return json.loads(raw.decode("utf-8"))
    except (ValueError, UnicodeDecodeError) as e:
        logger.warning("DOAJ JSON parse failed: %s", e)
        return None


# ---------------------------------------------------------------------------
# DOAJClient
# ---------------------------------------------------------------------------


class DOAJClient:
    """Client for DOAJ API v4 — open-access theology article discovery.

    DOAJ is the authoritative directory of peer-reviewed open-access journals.
    It indexes over 20,000 journals and 10+ million articles, all freely
    available. Theology and religious studies content includes biblical
    exegesis, systematic theology, church history, practical theology,
    comparative religion, and more — from journals like Acta Theologica,
    HTS Teologiese Studies, and Verbum et Ecclesia.

    All methods degrade gracefully: empty list / None on any failure
    (network error, timeout, bad response, JSON parse error).

    Rate-limited internally to respect DOAJ's 2 req/sec limit.

    Usage::

        from accordance_parser.doaj_api import doaj
        articles = doaj.search_articles("eschatology")
        article = doaj.get_article("0003eb3771ba4667b63769a81b575eda")
        journals = doaj.search_journals("biblical studies")
        journal = doaj.get_journal("01d0883ca40a423282086f304b80e361")
    """

    BASE = DOAJ_BASE

    # ------------------------------------------------------------------
    # Article methods
    # ------------------------------------------------------------------

    def search_articles(
        self,
        query: str,
        subject: str = "Theology",
        max_results: int = 20,
    ) -> List[dict]:
        """Search DOAJ for open-access articles.

        Filters results to theology/religious studies by appending the
        ``subject`` as a required Elasticsearch query term. DOAJ uses
        Library of Congress Classification (LCC) for subject metadata
        (e.g. BR=Christianity, BS=Bible, BT=Doctrinal Theology).

        Args:
            query: Free-text search query (title, abstract, keywords).
            subject: Subject filter appended as AND clause.
                     Default ``"Theology"`` also matches "Practical Theology",
                     "Doctrinal Theology", etc.
            max_results: Maximum results to return (1–100). DOAJ page cap is 100.

        Returns:
            List of article dicts with keys: ``id``, ``bibjson`` (title,
            abstract, authors, journal, subjects, identifiers, links),
            ``created_date``, ``last_updated``. Empty list on failure.
        """
        try:
            max_results = max(1, min(max_results, 100))
            # Build Elasticsearch query: user query AND subject filter.
            full_query = f"({query}) AND ({subject})"
            encoded_query = quote(full_query, safe="")

            url = (
                f"{DOAJ_BASE}/search/articles/{encoded_query}"
                f"?pageSize={max_results}&page=1"
            )
            data = _parse_json(_http_get(url, timeout=_SEARCH_ARTICLES_TIMEOUT))
            if not isinstance(data, dict):
                return []
            return data.get("results", [])[:max_results]
        except Exception as e:
            logger.warning("DOAJ article search failed for query=%r: %s", query, e)
            return []

    def get_article(self, article_id: str) -> Optional[dict]:
        """Get full article metadata by DOAJ internal ID.

        The DOAJ article ID is a 32-character hex string (UUID without
        hyphens), obtainable from ``search_articles()`` result ``id`` fields.

        Returns:
            Article metadata dict with ``bibjson`` (title, abstract, authors,
            journal, subjects, identifiers, full-text links), or None on failure.
        """
        try:
            url = f"{DOAJ_BASE}/articles/{quote(article_id, safe='')}"
            data = _parse_json(_http_get(url, timeout=_GET_ARTICLE_TIMEOUT))
            if not isinstance(data, dict):
                return None
            # The detail endpoint wraps the article; detect error responses.
            if "error" in data:
                logger.warning("DOAJ article %s: %s", article_id, data.get("error"))
                return None
            return data
        except Exception as e:
            logger.warning("DOAJ get_article failed for id=%r: %s", article_id, e)
            return None

    # ------------------------------------------------------------------
    # Journal methods
    # ------------------------------------------------------------------

    def search_journals(self, query: str, max_results: int = 20) -> List[dict]:
        """Search DOAJ for open-access journals.

        Args:
            query: Search by journal title, publisher, or ISSN.
            max_results: Maximum results to return (1–100).

        Returns:
            List of journal dicts with keys: ``id``, ``bibjson`` (title,
            publisher, ISSNs, subject classifications, license, etc.),
            ``created_date``, ``last_updated``. Empty list on failure.
        """
        try:
            max_results = max(1, min(max_results, 100))
            encoded_query = quote(query, safe="")
            url = (
                f"{DOAJ_BASE}/search/journals/{encoded_query}"
                f"?pageSize={max_results}&page=1"
            )
            data = _parse_json(_http_get(url, timeout=_SEARCH_JOURNALS_TIMEOUT))
            if not isinstance(data, dict):
                return []
            return data.get("results", [])[:max_results]
        except Exception as e:
            logger.warning("DOAJ journal search failed for query=%r: %s", query, e)
            return []

    def get_journal(self, journal_id: str) -> Optional[dict]:
        """Get full journal metadata by DOAJ internal ID.

        The DOAJ journal ID is a 32-character hex string, obtainable from
        ``search_journals()`` result ``id`` fields.

        Returns:
            Journal metadata dict with ``bibjson`` (title, publisher, ISSNs,
            subjects, license, APCs, review process, etc.), or None on failure.
        """
        try:
            url = f"{DOAJ_BASE}/journals/{quote(journal_id, safe='')}"
            data = _parse_json(_http_get(url, timeout=_GET_JOURNAL_TIMEOUT))
            if not isinstance(data, dict):
                return None
            if "error" in data:
                logger.warning("DOAJ journal %s: %s", journal_id, data.get("error"))
                return None
            return data
        except Exception as e:
            logger.warning("DOAJ get_journal failed for id=%r: %s", journal_id, e)
            return None


# Module-level singleton — import and use directly.
doaj = DOAJClient()


# ===========================================================================
# MCP Tool Registration Snippets
# ===========================================================================
#
# These are ready-to-copy snippets for registering DOAJ tools in an MCP
# server (e.g. FastMCP).  Paste into your tool registration file — do NOT
# modify the existing mcp_*.py files unless you are intentionally adding
# these tools to the server.
#
# --- Tool 1: search_theology_articles ---
#
# @mcp.tool()
# def search_theology_articles(query: str, max_results: int = 20) -> str:
#     """Search DOAJ for open-access theology/religion articles.
#
#     DOAJ indexes 20K+ journals and 10M+ articles. This tool searches
#     across titles, abstracts, and keywords, filtered to theology and
#     religious studies (LCC classes BL-BX).
#
#     Args:
#         query: Search terms (e.g. "eschatology", "Pauline epistles").
#         max_results: Max results (1-100, default 20).
#
#     Returns:
#         JSON string with list of articles (title, authors, journal,
#         DOI, abstract snippet, full-text links).
#     """
#     from accordance_parser.doaj_api import doaj
#     results = doaj.search_articles(query, max_results=max_results)
#     return json.dumps(results, indent=2, ensure_ascii=False)
#
# --- Tool 2: get_journal_info ---
#
# @mcp.tool()
# def get_journal_info(journal_id: str) -> str:
#     """Get full metadata for a DOAJ-indexed journal.
#
#     Returns publisher, ISSNs, subject classifications, open-access
#     license, APC information, and review process details.
#
#     Args:
#         journal_id: DOAJ internal journal ID (32-char hex string).
#
#     Returns:
#         JSON string with journal metadata.
#     """
#     from accordance_parser.doaj_api import doaj
#     journal = doaj.get_journal(journal_id)
#     if journal is None:
#         return json.dumps({"error": "Journal not found or API error"})
#     return json.dumps(journal, indent=2, ensure_ascii=False)
