"""
bolls.life local caching module for Accordance Port.

Downloads and caches bolls.life static translation JSON files to a local
directory for offline use.  Uses stdlib only — no external dependencies.

Usage::

    from accordance_parser.bolls_cache import BollsCache, PUBLIC_DOMAIN_TRANSLATIONS

    cache = BollsCache()
    cache.download_translation("KJV")              # fetch + save to disk
    print(cache.get_cached_verse("KJV", "Genesis", 1, 1))  # read from cache
    print(cache.list_cached_translations())        # ['KJV']
    print(cache.is_cached("KJV"))                   # True
"""

from __future__ import annotations

import json
import os
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any, Dict, List, Optional

from .canon import resolve_book_id

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

# Suggested translations for first-time cache population.
# These are freely available via bolls.life for personal study.
# Download more with: cache.download_translation("CODE")
CACHEABLE_SUGGESTIONS: List[str] = [
    "KJV",  # King James Version
    "ASV",  # American Standard Version
    "WEB",  # World English Bible
    "YLT",  # Young's Literal Translation
    "DRC",  # Douay-Rheims Challoner
    "JUB",  # Jubilee Bible
    "WLC",  # Westminster Leningrad Codex (Hebrew OT)
    "MorphGNT",  # Morphological Greek New Testament
    "LXX",  # Septuagint (Greek OT)
    "TR",  # Textus Receptus (Greek NT)
    "ESV",  # English Standard Version
    "NASB",  # New American Standard Bible
    "NIV",  # New International Version
    "NKJV",  # New King James Version
    "NRSV",  # New Revised Standard Version
    "CSB",  # Christian Standard Bible
    "NET",  # New English Translation
    "NLT",  # New Living Translation
]

# Backward compatibility alias
PUBLIC_DOMAIN_TRANSLATIONS = CACHEABLE_SUGGESTIONS

_BOLLS_STATIC_BASE = "https://bolls.life/static/translations"
_REQUEST_TIMEOUT = 120  # seconds — some translation JSONs are large (5-20 MB)

# ---------------------------------------------------------------------------
# BollsCache
# ---------------------------------------------------------------------------


class BollsCache:
    """Local JSON cache of bolls.life translations.

    Downloads full-translation JSON files from ``bolls.life/static/translations/``
    and stores them in a user-writable directory.  Provides verse-level lookup
    without network calls once cached.

    Parameters
    ----------
    cache_dir:
        Path to the cache directory.  Defaults to
        ``~/.accordance-port/cache/bolls/``.  Created automatically.
    """

    def __init__(self, cache_dir: Optional[str] = None) -> None:
        if cache_dir is None:
            cache_dir = os.path.expanduser("~/.accordance-port/cache/bolls")
        self.cache_dir = Path(cache_dir)
        self.cache_dir.mkdir(parents=True, exist_ok=True)

        # In-memory index: translation_code -> full parsed JSON (list of verse dicts)
        self._data: Dict[str, List[Dict[str, Any]]] = {}

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def download_translation(self, code: str) -> bool:
        """Download a full translation from bolls.life and cache it locally.

        Parameters
        ----------
        code:
            Translation code (case-insensitive), e.g. ``"KJV"``, ``"ASV"``.

        Returns
        -------
        ``True`` on success, ``False`` on any failure (network, HTTP, JSON).
        """
        code = code.upper()
        url = f"{_BOLLS_STATIC_BASE}/{code}.json"
        dest = self.cache_dir / f"{code}.json"

        try:
            req = urllib.request.Request(
                url, headers={"User-Agent": "AccordancePort/1.0"}
            )
            with urllib.request.urlopen(req, timeout=_REQUEST_TIMEOUT) as resp:  # type: ignore[attr-defined]
                if resp.status != 200:
                    return False
                raw = resp.read()
        except (urllib.error.URLError, urllib.error.HTTPError, OSError):
            # Network / HTTP / timeout errors
            return False

        # Validate JSON before writing to disk
        try:
            data = json.loads(raw.decode("utf-8"))
            if not isinstance(data, list):
                return False
        except (json.JSONDecodeError, UnicodeDecodeError, ValueError):
            return False

        # Atomic-ish write: write to temp file, then rename
        tmp = dest.with_suffix(".tmp")
        try:
            tmp.write_bytes(raw)
            tmp.rename(dest)
        except OSError:
            # Clean up temp file if rename fails
            try:
                tmp.unlink(missing_ok=True)
            except OSError:
                pass
            return False

        # Cache in memory
        self._data[code] = data
        return True

    def get_cached_verse(
        self,
        translation: str,
        book: int | str,
        chapter: int,
        verse: int,
    ) -> Optional[str]:
        """Retrieve a single verse from the local cache.

        Parameters
        ----------
        translation:
            Translation code, e.g. ``"KJV"``.
        book:
            Book identifier.  Either a bolls book ID (int, 1–66) or a
            book-name string (``"Genesis"``, ``"Gen"``, ``"1Cor"``, …).
        chapter:
            Chapter number (1‑based).
        verse:
            Verse number (1‑based).

        Returns
        -------
        Verse text as a string, or ``None`` if the translation isn't cached,
        the book/chapter/verse isn't found, or the cache file is corrupted.
        """
        code = translation.upper()

        # Resolve book name to bolls book ID if needed
        if isinstance(book, str):
            book_id = resolve_book_id(book)
            if book_id is None:
                return None
        else:
            book_id = book

        # Load translation data (from memory or disk)
        data = self._load(code)
        if data is None:
            return None

        # Linear scan — acceptable for 31K-verse files on modern hardware.
        # A future optimization could build a dict index on first load.
        for verse_obj in data:
            if (
                verse_obj.get("book") == book_id
                and verse_obj.get("chapter") == chapter
                and verse_obj.get("verse") == verse
            ):
                return verse_obj.get("text")
        return None

    def list_cached_translations(self) -> List[str]:
        """Return a sorted list of cached translation codes (uppercase)."""
        if not self.cache_dir.exists():
            return []
        codes: List[str] = []
        for f in self.cache_dir.iterdir():
            if f.suffix == ".json" and not f.name.startswith("."):
                codes.append(f.stem.upper())
        return sorted(codes)

    def is_cached(self, translation: str) -> bool:
        """Return ``True`` if *translation* exists in the local cache."""
        code = translation.upper()
        return (self.cache_dir / f"{code}.json").exists()

    def clear_memory_cache(self) -> None:
        """Release in-memory cached data (disk files are untouched)."""
        self._data.clear()

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

    def _load(self, code: str) -> Optional[List[Dict[str, Any]]]:
        """Load a cached translation into memory, returning the verse list."""
        if code in self._data:
            return self._data[code]

        path = self.cache_dir / f"{code}.json"
        if not path.exists():
            return None

        try:
            raw = path.read_bytes()
            data = json.loads(raw.decode("utf-8"))
            if not isinstance(data, list):
                return None
        except (json.JSONDecodeError, UnicodeDecodeError, OSError, ValueError):
            # Corrupted or unreadable file
            return None

        self._data[code] = data
        return data
