"""
Papyri.info access — papyrological texts and manuscript data.

Data source: https://github.com/papyri/idp.data (EpiDoc XML)
Search: https://papyri.info/search (Solr-based, no auth)

Zero external dependencies. Uses stdlib only.
Thread-safe for MCP server use.
"""

from __future__ import annotations

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

logger = logging.getLogger(__name__)

PAPYRI_SEARCH = "https://papyri.info/search"
PAPYRI_BASE = "https://papyri.info"
GITHUB_RAW = "https://raw.githubusercontent.com/papyri/idp.data/master"

_SEARCH_TIMEOUT = 15  # seconds
_TEXT_TIMEOUT = 15
_AVAIL_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("Papyri 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("Papyri 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("Papyri JSON parse failed: %s", e)
        return None


def _extract_text_from_xml(xml_bytes: Optional[bytes]) -> Optional[str]:
    """Extract plain text from an EpiDoc XML response.

    Papyri.info text endpoints return EpiDoc TEI XML.  We strip all tags
    and normalise whitespace to produce a readable plain-text transcription.
    """
    if xml_bytes is None:
        return None
    try:
        import re

        text = xml_bytes.decode("utf-8", errors="replace")
        # Remove XML processing instructions and comments
        text = re.sub(r"<\?.*?\?>", "", text)
        text = re.sub(r"<!--.*?-->", "", text, flags=re.DOTALL)
        # Collapse tags to a single space
        text = re.sub(r"<[^>]*>", " ", text)
        # Normalise whitespace
        text = re.sub(r"\s+", " ", text)
        return text.strip() or None
    except Exception as e:
        logger.warning("Papyri XML parse failed: %s", e)
        return None


# ---------------------------------------------------------------------------
# PapyriClient
# ---------------------------------------------------------------------------


class PapyriClient:
    """Client for the Papyri.info papyrological corpus.

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

    Usage::

        from accordance_parser.papyri_api import papyri
        results = papyri.search("P.Oxy. gospel")
        text = papyri.get_text("p.oxy.1.1")
        biblical = papyri.search_biblical("John 1:1")
    """

    # ------------------------------------------------------------------
    # search
    # ------------------------------------------------------------------

    def search(self, query: str, max_results: int = 20) -> list[dict]:
        """Search papyrological texts via the Papyri.info Solr index.

        Args:
            query: Free-text Solr query (supports Lucene syntax).
                   Examples: ``"gospel"``, ``"series:P.Oxy"``, ``"John 1:1"``.
            max_results: Maximum number of results to return (default 20).

        Returns:
            List of hit dicts, each with at least ``id``, ``title``, and
            ``snippet`` keys.  Empty list on failure or no results.
        """
        try:
            params = urllib.parse.urlencode(
                {
                    "q": query,
                    "format": "json",
                    "rows": str(max_results),
                }
            )
            url = f"{PAPYRI_SEARCH}?{params}"
            body = _http_get(url, _SEARCH_TIMEOUT)
            data = _parse_json(body)
            if not isinstance(data, dict):
                return []

            # Solr JSON response envelope
            response = data.get("response", {})
            docs = response.get("docs", [])
            if not isinstance(docs, list):
                return []

            results: list[dict] = []
            for doc in docs:
                if not isinstance(doc, dict):
                    continue
                # Build a stable result shape from known Solr fields
                result: dict[str, Any] = {
                    "id": doc.get("id", ""),
                    "title": doc.get("title_display", doc.get("title", "")),
                    "snippet": "",
                }
                # Prefer a content/transcription snippet if available
                content = doc.get("content", "") or doc.get("transcription", "")
                if isinstance(content, str) and len(content) > 300:
                    result["snippet"] = content[:300] + "…"
                elif isinstance(content, str):
                    result["snippet"] = content

                # Carry along metadata fields useful for downstream display
                for meta_key in ("series", "volume", "date", "language", "inventory"):
                    val = doc.get(meta_key)
                    if val:
                        result[meta_key] = val

                results.append(result)

            return results[:max_results]
        except Exception as e:
            logger.warning("Papyri search failed for query=%r: %s", query, e)
            return []

    # ------------------------------------------------------------------
    # get_text
    # ------------------------------------------------------------------

    def get_text(self, identifier: str) -> Optional[str]:
        """Get the full plain-text transcription of a papyrus by identifier.

        Tries two strategies in order:
          1. Papyri.info text API (``/ddbdp/{id}/text``)
          2. GitHub raw EpiDoc XML (``raw.githubusercontent.com/papyri/idp.data/…``)

        Args:
            identifier: Papyrus identifier (e.g. ``"p.oxy.1.1"``,
                        ``"hgv/12345"``, or a full papyri.info ID like
                        ``"ddbdp:p.oxy;1;1"``).

        Returns:
            Plain-text transcription, or ``None`` on failure.
        """
        if not identifier or not identifier.strip():
            return None

        # Normalise the identifier: papyri.info uses semicolons in path segments
        ident = identifier.strip().replace(":", ";")

        # Strategy 1 — papyri.info text endpoint
        try:
            encoded = quote(ident, safe=";/")
            url = f"{PAPYRI_BASE}/ddbdp/{encoded}/text"
            body = _http_get(url, _TEXT_TIMEOUT)
            if body:
                text = _extract_text_from_xml(body)
                if text:
                    return text
        except Exception as e:
            logger.debug("Papyri text API failed for id=%r: %s", identifier, e)

        # Strategy 2 — GitHub raw EpiDoc fallback
        try:
            encoded = quote(ident, safe=";/")
            url = f"{GITHUB_RAW}/DDBDP/{encoded}.xml"
            body = _http_get(url, _TEXT_TIMEOUT)
            if body:
                text = _extract_text_from_xml(body)
                if text:
                    return text
        except Exception as e:
            logger.debug("Papyri GitHub fallback failed for id=%r: %s", identifier, e)

        logger.warning("Papyri text retrieval failed for id=%r", identifier)
        return None

    # ------------------------------------------------------------------
    # search_biblical
    # ------------------------------------------------------------------

    def search_biblical(self, reference: str) -> list[dict]:
        """Search for papyri containing a biblical reference.

        Constructs a Solr query that searches for the reference string
        (e.g. ``"John 1:1"``) combined with topic keywords like *gospel*,
        *scripture*, or the book name itself.  Falls back to a broad
        biblical-corpus search.

        Args:
            reference: Biblical reference string (e.g. ``"John 1:1"``,
                       ``"Matthew 5"``, ``"Genesis 1"``).

        Returns:
            List of hit dicts (same shape as :meth:`search`).  Empty list
            on failure or no results.
        """
        if not reference or not reference.strip():
            return []

        ref = reference.strip()
        # Extract the book name for a broader fallback
        book = ref.split()[0] if " " in ref else ref
        book = book.rstrip(".")

        # Primary query: exact reference + biblical context keywords
        try:
            # Solr phrase query for the reference, boosted by manuscript keywords
            q = f'"{ref}" AND (gospel OR scripture OR biblical OR {book})'
            encoded = urllib.parse.urlencode(
                {
                    "q": q,
                    "format": "json",
                    "rows": "30",
                }
            )
            url = f"{PAPYRI_SEARCH}?{encoded}"
            body = _http_get(url, _SEARCH_TIMEOUT)
            data = _parse_json(body)
            if isinstance(data, dict):
                response = data.get("response", {})
                docs = response.get("docs", [])
                if isinstance(docs, list) and docs:
                    return self.search(ref, max_results=30)
        except Exception:
            pass

        # Fallback: plain search for the reference text
        return self.search(ref, max_results=30)


# ---------------------------------------------------------------------------
# Availability check
# ---------------------------------------------------------------------------


def papyri_available() -> bool:
    """Check if papyri.info is reachable.

    Returns ``True`` if the search endpoint responds within the timeout;
    ``False`` otherwise.
    """
    try:
        url = f"{PAPYRI_SEARCH}?q=test&format=json&rows=1"
        req = urllib.request.Request(url, headers={"User-Agent": _USER_AGENT})
        with urllib.request.urlopen(req, timeout=_AVAIL_TIMEOUT) as resp:
            return resp.status == 200
    except Exception as e:
        logger.debug("Papyri availability check failed: %s", e)
        return False


# ---------------------------------------------------------------------------
# Module-level singleton
# ---------------------------------------------------------------------------

papyri = PapyriClient()


# ---------------------------------------------------------------------------
# MCP tool snippets — copy into accordance_mcp_server.py to register tools
# ---------------------------------------------------------------------------

# --- search_papyri ---
"""
@mcp.tool()
def search_papyri(query: str, max_results: int = 20) -> str:
    \"""Search papyrological texts via Papyri.info.

    Searches the full papyrological corpus (documentary papyri,
    literary papyri, ostraca, etc.) for the given query string.

    Args:
        query: Free-text query (supports Lucene syntax).
               Examples: 'gospel', 'series:P.Oxy', 'John 1:1'.
        max_results: Maximum results to return (default 20).

    Returns:
        Markdown-formatted list of matching papyri with identifiers
        and text snippets.
    \"""
    try:
        from accordance_parser.papyri_api import papyri
        results = papyri.search(query, max_results=max_results)

        if not results:
            return f\"**No papyri found** for query: `{query}`\"

        lines = [
            f\"# Papyri.info — {len(results)} result(s) for `{query}`\",
            \"\",
        ]
        for r in results:
            lines.append(f\"## {r.get('title', r.get('id', 'Unknown'))}\")
            lines.append(f\"- **ID:** `{r.get('id', 'N/A')}`\")
            if r.get('date'):
                lines.append(f\"- **Date:** {r['date']}\")
            if r.get('language'):
                lines.append(f\"- **Language:** {r['language']}\")
            if r.get('series'):
                lines.append(f\"- **Series:** {r['series']}\")
            if r.get('inventory'):
                lines.append(f\"- **Inventory:** {r['inventory']}\")
            snippet = r.get('snippet', '')
            if snippet:
                lines.append(f\"\n> {snippet}\")
            lines.append(\"\")
        return \"\\n\".join(lines)
    except Exception as e:
        return f\"**Papyri search error:** {e}\"
"""

# --- get_papyrus_text ---
"""
@mcp.tool()
def get_papyrus_text(identifier: str) -> str:
    \"""Get the full transcription of a papyrus from Papyri.info.

    Fetches the plain-text EpiDoc transcription for the given
    papyrus identifier.

    Args:
        identifier: Papyrus identifier (e.g. 'p.oxy.1.1',
                    'hgv/12345', or 'ddbdp:p.oxy;1;1').

    Returns:
        Markdown-formatted transcription of the papyrus text,
        or an error message if not found.
    \"""
    try:
        from accordance_parser.papyri_api import papyri
        text = papyri.get_text(identifier)

        if text is None:
            return (
                f\"**Papyrus not found:** `{identifier}`\\n\\n\"
                f\"Please verify the identifier.  Try searching first: \"
                f\"`search_papyri('{identifier}')`\"
            )

        return (
            f\"# Papyrus: {identifier}\\n\\n\"
            f\"{text}\"
        )
    except Exception as e:
        return f\"**Papyrus text retrieval error:** {e}\"
"""
