"""
Sefaria REST API adapter for Accordance Port.

Zero external dependencies. Uses stdlib only (urllib.request).
Provides access to Sefaria's open Jewish text library:
Tanakh, Talmud, Midrash, commentaries, and daily learning schedules.

Thread-safe for MCP server use.  In-memory LRU caches keep API
responses so repeated calls for the same ref/query are instantaneous.
"""

from __future__ import annotations

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

logger = logging.getLogger(__name__)

SEFARIA_BASE = "https://www.sefaria.org"
SEFARIA_API = f"{SEFARIA_BASE}/api"

_SEARCH_TIMEOUT = 15  # seconds
_TEXT_TIMEOUT = 15
_LINKS_TIMEOUT = 15
_AUTOCOMPLETE_TIMEOUT = 10
_CALENDAR_TIMEOUT = 10

_USER_AGENT = "AccordancePort/1.0"

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


def _http_get(url: str, timeout: int) -> Optional[bytes]:
    """Perform a GET request. Returns response body bytes, or None on failure."""
    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("Sefaria 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("Sefaria GET %s failed: %s", url, e)
        return None


def _http_post(url: str, payload: dict, timeout: int) -> Optional[bytes]:
    """Perform a POST request with JSON body. Returns body bytes, or None on failure."""
    try:
        data = json.dumps(payload).encode("utf-8")
        req = urllib.request.Request(
            url,
            data=data,
            headers={
                "User-Agent": _USER_AGENT,
                "Content-Type": "application/json",
            },
        )
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            if resp.status != 200:
                logger.warning("Sefaria POST %s returned HTTP %d", url, resp.status)
                return None
            return resp.read()
    except (urllib.error.URLError, urllib.error.HTTPError, OSError) as e:
        logger.warning("Sefaria POST %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("Sefaria JSON parse failed: %s", e)
        return None


# ---------------------------------------------------------------------------
# SefariaClient
# ---------------------------------------------------------------------------


class SefariaClient:
    """Client for Sefaria's open Jewish text library.

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

    Usage::

        from accordance_parser.sefaria_api import sefaria
        results = sefaria.search("Moses")
        text = sefaria.get_text("Genesis.1.1")
    """

    def search(self, query: str, max_results: int = 10) -> list:
        """Search Sefaria's text corpus.

        Args:
            query: Search query string.
            max_results: Maximum number of results to return (default 10).

        Returns:
            List of hit dicts (may be empty on failure or no results).
        """
        try:
            url = f"{SEFARIA_API}/search-wrapper"
            body = _http_post(
                url, {"query": query, "size": max_results}, _SEARCH_TIMEOUT
            )
            if body is None:
                return []
            data = json.loads(body.decode("utf-8"))
            return data.get("hits", {}).get("hits", [])[:max_results]
        except Exception as e:
            logger.warning("Sefaria search failed for query=%r: %s", query, e)
            return []

    def get_text(self, ref: str, context: int = 1) -> Optional[dict]:
        """Get text from Sefaria by reference.

        Args:
            ref: Sefaria reference string (e.g. 'Genesis.1.1', 'Berakhot.2a').
            context: Number of surrounding verses/sections to include.

        Returns:
            Text data dict, or None on failure.
        """
        try:
            encoded_ref = quote(ref, safe="")
            url = f"{SEFARIA_API}/texts/{encoded_ref}?context={context}"
            body = _http_get(url, _TEXT_TIMEOUT)
            data = _parse_json(body)
            return data if isinstance(data, dict) else None
        except Exception as e:
            logger.warning("Sefaria text retrieval failed for ref=%r: %s", ref, e)
            return None

    def get_links(self, ref: str) -> list:
        """Get commentary connections for a Sefaria text reference.

        Args:
            ref: Sefaria reference string (e.g. 'Genesis.1.1').

        Returns:
            List of link dicts (may be empty on failure or no links).
        """
        try:
            encoded_ref = quote(ref, safe="")
            url = f"{SEFARIA_API}/links/{encoded_ref}"
            body = _http_get(url, _LINKS_TIMEOUT)
            data = _parse_json(body)
            return data if isinstance(data, list) else []
        except Exception as e:
            logger.warning("Sefaria links retrieval failed for ref=%r: %s", ref, e)
            return []

    def autocomplete(self, query: str) -> list:
        """Autocomplete Sefaria book titles, references, and topics.

        Args:
            query: Partial name or reference string.

        Returns:
            List of completion dicts (max 10), empty list on failure.
        """
        try:
            encoded_query = quote(query, safe="")
            url = f"{SEFARIA_API}/name/{encoded_query}"
            body = _http_get(url, _AUTOCOMPLETE_TIMEOUT)
            if body is None:
                return []
            data = json.loads(body.decode("utf-8"))
            return data.get("completion_objects", [])[:10]
        except Exception as e:
            logger.warning("Sefaria autocomplete failed for query=%r: %s", query, e)
            return []

    def get_calendar(self) -> dict:
        """Get today's Torah portion, Daf Yomi, and daily learning schedules.

        Returns:
            Calendar data dict (may be empty on failure).
        """
        try:
            url = f"{SEFARIA_API}/calendars"
            body = _http_get(url, _CALENDAR_TIMEOUT)
            data = _parse_json(body)
            return data if isinstance(data, dict) else {}
        except Exception as e:
            logger.warning("Sefaria calendar retrieval failed: %s", e)
            return {}

    def get_categories(self) -> list:
        """Get Sefaria's full category tree (Tanakh, Talmud, Midrash, etc.).

        Calls /api/index/ which returns every available text title organized
        by category.  Results are cached in-process for 5 minutes.

        Returns:
            List of top-level category dicts with ``"contents"`` sub-lists.
            Empty list on failure.
        """
        try:
            url = f"{SEFARIA_API}/index/"
            body = _http_get(url, _TEXT_TIMEOUT)
            data = _parse_json(body)
            if isinstance(data, list):
                return data
            if isinstance(data, dict):
                # Sometimes /api/index returns a dict with a "books" or
                # similar key; handle both.
                return data.get("books", data.get("results", []))  # type: ignore[return-value]
            return []
        except Exception as e:
            logger.warning("Sefaria categories retrieval failed: %s", e)
            return []


# ---------------------------------------------------------------------------
# In-memory response cache (module-level, shared by the singleton)
# ---------------------------------------------------------------------------

# Simple TTL-based cache dict:  {(method, key): (timestamp, data)}
_cache: dict = {}
_cache_ttl: float = 300.0  # 5 minutes


def _cached_call(key: str, factory, ttl: float = _cache_ttl):
    """Return cached value for *key*, calling *factory()* on cache miss."""
    now = time.monotonic()
    entry = _cache.get(key)
    if entry is not None:
        ts, data = entry
        if now - ts < ttl:
            return data
    data = factory()
    if data is not None and data not in (None, [], {}):
        _cache[key] = (now, data)
    return data


def _cache_key(method: str, *args) -> str:
    """Produce a stable cache key like 'get_text:Genesis.1.1'."""
    return f"{method}:" + ":".join(str(a) for a in args)


# ---------------------------------------------------------------------------
# Module-level convenience functions (cached)
# ---------------------------------------------------------------------------

# Singleton — import and use directly.  Also available via the module-level
# functions below which add in-memory caching.
sefaria = SefariaClient()


def search_sefaria(query: str, max_results: int = 10) -> list:
    """Cached wrapper for ``sefaria.search(query, max_results)``."""
    return _cached_call(
        _cache_key("search", query, max_results),
        lambda: sefaria.search(query, max_results),
    )


def get_sefaria_text(ref: str, context: int = 1) -> Optional[dict]:
    """Cached wrapper for ``sefaria.get_text(ref, context)``."""
    return _cached_call(
        _cache_key("get_text", ref, context),
        lambda: sefaria.get_text(ref, context),
    )


def get_sefaria_links(ref: str) -> list:
    """Cached wrapper for ``sefaria.get_links(ref)``."""
    return _cached_call(
        _cache_key("get_links", ref),
        lambda: sefaria.get_links(ref),
    )


def sefaria_suggest(query: str) -> list:
    """Cached wrapper for ``sefaria.autocomplete(query)``."""
    return _cached_call(
        _cache_key("autocomplete", query),
        lambda: sefaria.autocomplete(query),
    )


def get_sefaria_categories() -> list:
    """Cached wrapper for ``sefaria.get_categories()``."""
    return _cached_call(
        "categories",
        lambda: sefaria.get_categories(),
    )
