"""Per-book vocabulary list generator using morphology data.

Generates frequency-sorted vocabulary lists for any Bible book from
the OpenGNT (Greek NT) and OpenScriptures Hebrew Bible (OT) morphology
data files.  Supports gloss enrichment via Strong's lexicon lookups.

For deuterocanonical books (no morphology data), uses the LXX1 text
module (Greek) and bolls.life API (English).
"""

from __future__ import annotations

import json
import re
from collections import defaultdict
from pathlib import Path

from .canon import resolve_book, BOOKS_BY_OSIS

# ---------------------------------------------------------------------------
# Data paths
# ---------------------------------------------------------------------------

DATA_DIR = Path(__file__).resolve().parent.parent / "data"

_GNT_PATH = DATA_DIR / "morphology_gnt.json"
_OT_PATH = DATA_DIR / "morphology_ot.json"

# ---------------------------------------------------------------------------
# Deuterocanonical / LXX book mappings
# ---------------------------------------------------------------------------

# Maps common deuterocanonical book names to their canonical form.
DEUTEROCANONICAL_BOOKS: dict[str, str] = {
    "Tobit": "Tobit",
    "Judith": "Judith",
    "Wisdom": "Wisdom of Solomon",
    "Sirach": "Sirach",
    "Baruch": "Baruch",
    "1 Maccabees": "1 Maccabees",
    "2 Maccabees": "2 Maccabees",
}

# Which deuterocanonical books have bolls_id (for English lookups).
_DEUTERO_BOLLS_IDS: dict[str, int] = {
    "Tobit": 68,
    "Judith": 69,
    "Wisdom of Solomon": 70,
    "Sirach": 71,
    "Baruch": 73,
    "1 Maccabees": 74,
    "2 Maccabees": 75,
}

# ---------------------------------------------------------------------------
# English stop words (common words not useful for vocabulary study)
# ---------------------------------------------------------------------------

ENGLISH_STOP_WORDS: frozenset[str] = frozenset(
    {
        "the",
        "and",
        "of",
        "to",
        "in",
        "a",
        "he",
        "his",
        "i",
        "that",
        "for",
        "is",
        "it",
        "with",
        "not",
        "was",
        "be",
        "as",
        "you",
        "are",
        "him",
        "they",
        "from",
        "but",
        "her",
        "had",
        "has",
        "been",
        "by",
        "me",
        "my",
        "she",
        "them",
        "we",
        "or",
        "which",
        "will",
        "who",
        "this",
        "all",
        "their",
        "said",
        "one",
        "if",
        "so",
        "when",
        "there",
        "at",
        "what",
        "up",
        "do",
        "on",
        "out",
        "no",
        "thy",
        "thou",
        "thee",
        "hath",
        "shalt",
        "doth",
        "unto",
        "upon",
        "into",
        "also",
        "then",
        "an",
        "shall",
    }
)

# ---------------------------------------------------------------------------
# LXX1 text module cache (lazy-loaded)
# ---------------------------------------------------------------------------

_lxx_module = None


def _get_lxx_module():
    """Lazy-load the LXX1 TextModule (cached at module level)."""
    global _lxx_module
    if _lxx_module is not None:
        return _lxx_module
    try:
        from .text_module import TextModule

        _lxx_module = TextModule("modules/Texts/LXX1.atext/LXX1", "LXX1")
    except Exception:
        _lxx_module = False  # Sentinel for "tried but failed"
    return _lxx_module


# ---------------------------------------------------------------------------
# Book number mappings (morphology key prefixes → canonical book)
# ---------------------------------------------------------------------------

# These match the bolls_id values used by canon.py and the morphology key
# prefixes (e.g. 40_1_1 = Matthew 1:1).  Included for documentation and
# fallback; resolve_book().bolls_id is preferred.

NT_BOOK_NUMS: dict[str, int] = {
    "Matthew": 40,
    "Mark": 41,
    "Luke": 42,
    "John": 43,
    "Acts": 44,
    "Romans": 45,
    "1 Corinthians": 46,
    "2 Corinthians": 47,
    "Galatians": 48,
    "Ephesians": 49,
    "Philippians": 50,
    "Colossians": 51,
    "1 Thessalonians": 52,
    "2 Thessalonians": 53,
    "1 Timothy": 54,
    "2 Timothy": 55,
    "Titus": 56,
    "Philemon": 57,
    "Hebrews": 58,
    "James": 59,
    "1 Peter": 60,
    "2 Peter": 61,
    "1 John": 62,
    "2 John": 63,
    "3 John": 64,
    "Jude": 65,
    "Revelation": 66,
}

OT_BOOK_NUMS: dict[str, int] = {
    "Genesis": 1,
    "Exodus": 2,
    "Leviticus": 3,
    "Numbers": 4,
    "Deuteronomy": 5,
    "Joshua": 6,
    "Judges": 7,
    "Ruth": 8,
    "1 Samuel": 9,
    "2 Samuel": 10,
    "1 Kings": 11,
    "2 Kings": 12,
    "1 Chronicles": 13,
    "2 Chronicles": 14,
    "Ezra": 15,
    "Nehemiah": 16,
    "Esther": 17,
    "Job": 18,
    "Psalms": 19,
    "Proverbs": 20,
    "Ecclesiastes": 21,
    "Song of Solomon": 22,
    "Isaiah": 23,
    "Jeremiah": 24,
    "Lamentations": 25,
    "Ezekiel": 26,
    "Daniel": 27,
    "Hosea": 28,
    "Joel": 29,
    "Amos": 30,
    "Obadiah": 31,
    "Jonah": 32,
    "Micah": 33,
    "Nahum": 34,
    "Habakkuk": 35,
    "Zephaniah": 36,
    "Haggai": 37,
    "Zechariah": 38,
    "Malachi": 39,
}

# Reverse: book number → canonical name
_BOOK_NUM_TO_NAME: dict[int, str] = {}
for _d in (NT_BOOK_NUMS, OT_BOOK_NUMS):
    for _name, _num in _d.items():
        _BOOK_NUM_TO_NAME[_num] = _name

# Also build from canon.py for full coverage (includes deuterocanonical)
for _osis, _book in BOOKS_BY_OSIS.items():
    if _book.bolls_id is not None and _book.bolls_id not in _BOOK_NUM_TO_NAME:
        _BOOK_NUM_TO_NAME[_book.bolls_id] = _book.name


def book_num_to_name(num: int) -> str:
    """Convert a morphology book number to a canonical book name."""
    name = _BOOK_NUM_TO_NAME.get(num)
    if name:
        return name
    # Check deuterocanonical bolls_id → OSIS → name
    for _osis, _book in BOOKS_BY_OSIS.items():
        if _book.bolls_id == num:
            return _book.name
    return f"Book {num}"


# ---------------------------------------------------------------------------
# Lazy morphology loading (module-level cache)
# ---------------------------------------------------------------------------

_morph_cache: dict[str, dict] = {}


def _load_morphology(testament: str) -> dict:
    """Load morphology JSON, return ``verses`` dict.  Cached after first load.

    Args:
        testament: ``'nt'`` (Greek NT) or ``'ot'`` (Hebrew OT).

    Returns:
        The ``verses`` dict mapping ``"book_chapter_verse"`` keys to word lists,
        or an empty dict if the file is missing.
    """
    if testament in _morph_cache:
        return _morph_cache[testament]

    if testament == "nt":
        path = _GNT_PATH
    elif testament == "ot":
        path = _OT_PATH
    else:
        raise ValueError(f"Unknown testament: {testament!r}")

    if not path.exists():
        _morph_cache[testament] = {}
        return {}

    with open(str(path), "r", encoding="utf-8") as f:
        data = json.load(f)

    verses = data.get("verses", {})
    _morph_cache[testament] = verses
    return verses


# ---------------------------------------------------------------------------
# Chapter range parsing
# ---------------------------------------------------------------------------


def _parse_chapters(spec: str | None) -> set[int] | None:
    """Parse a chapter range specification into a set of chapter numbers.

    Supports: "1-3", "1,3,5-7", "1", None (whole book).
    Returns None for whole-book (no filtering needed).
    """
    if spec is None:
        return None
    result: set[int] = set()
    for part in spec.replace(" ", "").split(","):
        if not part:
            continue
        if "-" in part:
            lo_str, hi_str = part.split("-", 1)
            lo, hi = int(lo_str), int(hi_str)
            result.update(range(lo, hi + 1))
        else:
            result.add(int(part))
    return result if result else None


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


def _resolve_book_info(book: str) -> tuple[int, str] | None:
    """Resolve a book name to (book_num, testament).

    Returns None if the book cannot be resolved.

    For deuterocanonical books without morphology data, returns
    (bolls_id, 'lxx') to trigger LXX1 text-based extraction.
    """
    book_obj = resolve_book(book)
    if book_obj is None:
        return None
    if book_obj.bolls_id is not None:
        # Check if this is a deuterocanonical book WITHOUT morphology data
        if book_obj.bolls_id >= 68:
            return (book_obj.bolls_id, "lxx")
        return (book_obj.bolls_id, book_obj.testament)
    # Deuterocanonical books without bolls_id (unlikely but handle)
    return None


def get_chapter_vocab(
    book: str,
    chapters: str | None = None,
    min_count: int = 1,
    max_results: int = 100,
) -> list[dict]:
    """Return frequency-sorted vocabulary for a range of chapters in a Bible book.

    Each item is a dict with keys ``lemma``, ``strongs``, and ``count``,
    sorted by count descending (most frequent first).

    For Hebrew books the ``lemma`` field may be empty; the ``strongs``
    field always contains the representative Strong's number for gloss
    lookups.

    For deuterocanonical books, falls back to LXX1 text-based word counts
    (word-form level, not lemmas — no morphology data available).

    Args:
        book: Book name (e.g. ``'John'``, ``'Genesis'``, ``'Romans'``).
        chapters: Chapter range (e.g. ``'1-3'``, ``'1,3,5-7'``).
                  None returns results for the whole book.
        min_count: Minimum occurrence count to include (default 1).
        max_results: Maximum number of vocabulary items (default 100).

    Returns:
        List of dicts, each with ``lemma``, ``strongs``, ``count``.
        Empty list if morphology data is unavailable.
    """
    info = _resolve_book_info(book)
    if info is None:
        return []

    book_num, testament = info

    # Deuterocanonical books: fall back to LXX1 text extraction
    if testament == "lxx":
        return get_book_vocab_text(
            book, chapters=chapters, min_count=min_count, max_results=max_results
        )

    verses = _load_morphology(testament)
    if not verses:
        return []

    allowed_chapters = _parse_chapters(chapters)

    # Count lemma occurrences. Key: lemma if non-empty else strongs.
    # Track first strongs seen per key for the output.
    prefix = f"{book_num}_"
    counts: dict[str, int] = defaultdict(int)
    key_to_strongs: dict[str, str] = {}  # first strongs seen per key
    key_to_lemma: dict[str, str] = {}  # lemma for each key

    for verse_key, word_list in verses.items():
        if not verse_key.startswith(prefix):
            continue
        if allowed_chapters is not None:
            parts = verse_key.split("_")
            if len(parts) < 2:
                continue
            try:
                ch = int(parts[1])
            except ValueError:
                continue
            if ch not in allowed_chapters:
                continue
        for entry in word_list:
            if len(entry) < 2:
                continue
            surface = entry[0]  # noqa: F841  (surface form, unused)
            strongs = entry[1]  # e.g. "G3056", "H7225"
            lemma = entry[2] if len(entry) >= 3 else ""

            # Use lemma as key if available, otherwise Strong's number
            key = lemma if lemma else strongs
            if not key:
                continue

            counts[key] += 1
            if key not in key_to_strongs:
                key_to_strongs[key] = strongs
            if lemma and key not in key_to_lemma:
                key_to_lemma[key] = lemma

    if not counts:
        return []

    # Sort by count descending, then alphabetically for ties
    sorted_keys = sorted(counts.keys(), key=lambda k: (-counts[k], k))

    # Filter and limit
    results: list[dict] = []
    for key in sorted_keys:
        cnt = counts[key]
        if cnt < min_count:
            continue
        results.append(
            {
                "lemma": key_to_lemma.get(
                    key, key if key.startswith(("G", "H")) else ""
                ),
                "strongs": key_to_strongs.get(
                    key, key if key.startswith(("G", "H")) else ""
                ),
                "count": cnt,
            }
        )
        if len(results) >= max_results:
            break

    return results


def get_book_vocab(
    book: str,
    chapters: str | None = None,
    min_count: int = 1,
    max_results: int = 100,
) -> list[dict]:
    """Return frequency-sorted vocabulary for a Bible book.

    Delegates to get_chapter_vocab.

    Args:
        book: Book name (e.g. ``'John'``, ``'Genesis'``, ``'Romans'``).
        chapters: Chapter range (e.g. ``'1-3'``, ``'1,3,5-7'``).
                  None returns results for the whole book.
        min_count: Minimum occurrence count to include (default 1).
        max_results: Maximum number of vocabulary items (default 100).

    Returns:
        List of dicts, each with ``lemma``, ``strongs``, ``count``.
        Empty list if morphology data is unavailable.
    """
    return get_chapter_vocab(
        book, chapters=chapters, min_count=min_count, max_results=max_results
    )


def get_book_vocab_text(
    book: str,
    chapters: str | None = None,
    min_count: int = 1,
    max_results: int = 100,
) -> list[dict]:
    """Return frequency-sorted Greek word forms for a book using LXX1 text.

    For deuterocanonical books where morphology data isn't available,
    tokenizes the Greek text directly to count word forms (not lemmas).

    Each item is a dict with keys ``lemma`` (the word form), ``strongs``
    (empty string), and ``count``.

    Args:
        book: Book name (e.g. ``'Wisdom'``, ``'Tobit'``).
        chapters: Chapter range (e.g. ``'1-3'``, ``'1,3,5-7'``).
                  None returns results for the whole book.
        min_count: Minimum occurrence count to include (default 1).
        max_results: Maximum number of vocabulary items (default 100).

    Returns:
        List of dicts, each with ``lemma``, ``strongs``, ``count``.
        Empty list if LXX1 data is unavailable.
    """
    _lxx = _get_lxx_module()
    if not _lxx:
        return []

    # Resolve book to canonical name
    book_obj = resolve_book(book)
    if book_obj is None:
        return []
    book_name = book_obj.name

    allowed_chapters = _parse_chapters(chapters)

    counts: dict[str, int] = defaultdict(int)

    try:
        for _book, _ch, _vs, text in _lxx.iter_canon_verses():
            if _book != book_name:
                continue
            if allowed_chapters is not None and _ch not in allowed_chapters:
                continue
            # Tokenize Greek text: split on spaces and common punctuation
            words = re.split(r"[\s,.;:·\u0387\u00B7]+", text)
            for w in words:
                w = w.strip()
                if not w:
                    continue
                # Skip pure punctuation/digits
                if re.match(r"^[\d\W_]+$", w, re.UNICODE):
                    continue
                counts[w] += 1
    except Exception:
        return []

    if not counts:
        return []

    # Sort by count descending, then alphabetically
    sorted_words = sorted(counts.keys(), key=lambda k: (-counts[k], k))

    results: list[dict] = []
    for w in sorted_words:
        cnt = counts[w]
        if cnt < min_count:
            continue
        results.append({"lemma": w, "strongs": "", "count": cnt})
        if len(results) >= max_results:
            break

    return results


def get_book_vocab_english(
    book: str,
    translation: str = "kjv",
    chapters: str | None = None,
    min_count: int = 1,
    max_results: int = 100,
) -> list[dict]:
    """Return frequency-sorted English words for a book via bolls.life API.

    Tokenizes English text, filters out common stop words, and returns
    frequency-sorted word counts.

    Each item is a dict with keys ``lemma`` (the word), ``strongs``
    (empty string), and ``count``.

    Args:
        book: Book name (e.g. ``'Genesis'``, ``'Wisdom'``).
        translation: Translation code (default ``'kjv'``).  See
                     bolls.life API for available translations.
        chapters: Chapter range (e.g. ``'1-3'``, ``'1,3,5-7'``).
                  None returns results for the whole book.
        min_count: Minimum occurrence count to include (default 1).
        max_results: Maximum number of vocabulary items (default 100).

    Returns:
        List of dicts, each with ``lemma``, ``strongs``, ``count``.
        Empty list on failure.
    """
    try:
        from .bible_api import fetch_bolls_chapter
    except ImportError:
        return []

    book_obj = resolve_book(book)
    if book_obj is None or book_obj.bolls_id is None:
        return []

    allowed_chapters = _parse_chapters(chapters)

    counts: dict[str, int] = defaultdict(int)

    try:
        # Iterate through chapters using canon data
        from .canon import canon_by_name

        canon = canon_by_name("catholic" if book_obj.bolls_id >= 68 else "protestant")
        _vc = canon.last_verse.get(book_obj.osis, ())
        chapter_count_val = len(_vc)

        for chapter in range(1, chapter_count_val + 1):
            if allowed_chapters is not None and chapter not in allowed_chapters:
                continue
            verses = fetch_bolls_chapter(translation, book_obj.bolls_id, chapter)
            for v in verses:
                if not v.text:
                    continue
                # Tokenize: lowercase, split on non-alpha chars
                words = re.split(r"[^a-zA-Z]+", v.text.lower())
                for w in words:
                    w = w.strip()
                    if not w or len(w) < 2:
                        continue
                    if w in ENGLISH_STOP_WORDS:
                        continue
                    counts[w] += 1
    except Exception:
        return []

    if not counts:
        return []

    sorted_words = sorted(counts.keys(), key=lambda k: (-counts[k], k))

    results: list[dict] = []
    for w in sorted_words:
        cnt = counts[w]
        if cnt < min_count:
            continue
        results.append({"lemma": w, "strongs": "", "count": cnt})
        if len(results) >= max_results:
            break

    return results


def get_book_vocab_markdown(
    book: str,
    min_count: int = 1,
    max_results: int = 50,
) -> str:
    """Return a formatted markdown table of per-book vocabulary.

    Args:
        book: Book name (e.g. ``'John'``, ``'Genesis'``).
        min_count: Minimum occurrence count to include.
        max_results: Maximum vocabulary rows in the table.

    Returns:
        Markdown string with a table of lemma, Strong's, count, and
        optionally a gloss column (if a ``gloss_callback`` was passed
        to the internal builder).
    """
    info = _resolve_book_info(book)
    if info is None:
        return f"*(Book '{book}' not found)*"

    book_num, testament = info
    book_name = book_num_to_name(book_num)
    vocab = get_book_vocab(book, min_count=min_count, max_results=max_results)

    lang_map = {"nt": "Greek", "ot": "Hebrew", "lxx": "Greek (LXX)"}
    lang = lang_map.get(testament, "Greek")
    total_words = sum(v["count"] for v in vocab)

    lines = [f"# {book_name} Vocabulary", ""]
    lines.append(f"**Language:** {lang}  |  **Book number:** {book_num}")
    lines.append(
        f"**Unique lemmas:** {len(vocab)}  |  **Total word count (sampled):** {total_words}"
    )
    lines.append(f"**Minimum count:** {min_count}  |  **Max results:** {max_results}")
    lines.append("")

    if not vocab:
        lines.append("*(No vocabulary data available for this book)*")
        return "\n".join(lines)

    lines.append("| # | Lemma | Strong's | Count |")
    lines.append("|---|-------|----------|-------|")
    for i, v in enumerate(vocab, 1):
        lemma = v["lemma"] if v["lemma"] else "\u2014"
        strongs = v["strongs"] if v["strongs"] else "\u2014"
        lines.append(f"| {i} | {lemma} | {strongs} | {v['count']}\u00d7 |")

    return "\n".join(lines)


def compare_book_vocab(
    book1: str,
    book2: str,
    min_count: int = 1,
    max_results: int = 50,
) -> dict:
    """Compare vocabulary between two books.

    Returns a dict with three categories of words:
    - ``only_first``: words unique to ``book1``
    - ``only_second``: words unique to ``book2``
    - ``shared``: words appearing in both books

    Each category is a list of dicts with ``lemma``, ``strongs``,
    ``count_book1``, and ``count_book2`` (0 for unique items).

    Args:
        book1: First book name (e.g. ``'John'``).
        book2: Second book name (e.g. ``'Matthew'``).
        min_count: Minimum count in at least one book to include.
        max_results: Maximum results per category.

    Returns:
        Dict with keys ``book1``, ``book2``, ``only_first``,
        ``only_second``, ``shared``.
    """
    info1 = _resolve_book_info(book1)
    info2 = _resolve_book_info(book2)
    if info1 is None or info2 is None:
        return {
            "error": f"Could not resolve: {book1!r} or {book2!r}",
            "only_first": [],
            "only_second": [],
            "shared": [],
        }

    num1, test1 = info1
    num2, test2 = info2
    name1 = book_num_to_name(num1)
    name2 = book_num_to_name(num2)

    vocab1 = get_book_vocab(book1, min_count=1, max_results=99999)
    vocab2 = get_book_vocab(book2, min_count=1, max_results=99999)

    # Build lookup dicts by lemma key
    d1: dict[str, dict] = {}
    d2: dict[str, dict] = {}
    for v in vocab1:
        key = v["lemma"] if v["lemma"] else v["strongs"]
        d1[key] = v
    for v in vocab2:
        key = v["lemma"] if v["lemma"] else v["strongs"]
        d2[key] = v

    all_keys = set(d1) | set(d2)

    only_first: list[dict] = []
    only_second: list[dict] = []
    shared: list[dict] = []

    for key in sorted(all_keys):
        in1 = key in d1
        in2 = key in d2
        c1 = d1[key]["count"] if in1 else 0
        c2 = d2[key]["count"] if in2 else 0
        if max(c1, c2) < min_count:
            continue

        entry = {
            "lemma": d1[key]["lemma"] if in1 else d2[key]["lemma"],
            "strongs": d1[key]["strongs"] if in1 else d2[key]["strongs"],
            "count_book1": c1,
            "count_book2": c2,
        }

        if in1 and not in2:
            only_first.append(entry)
        elif in2 and not in1:
            only_second.append(entry)
        else:
            shared.append(entry)

    # Sort by total count descending, limit
    only_first.sort(key=lambda x: -x["count_book1"])
    only_second.sort(key=lambda x: -x["count_book2"])
    shared.sort(key=lambda x: -(x["count_book1"] + x["count_book2"]))

    return {
        "book1": name1,
        "book2": name2,
        "only_first": only_first[:max_results],
        "only_second": only_second[:max_results],
        "shared": shared[:max_results],
    }


def list_books_with_morphology() -> dict:
    """Return a summary of books that have vocabulary data available.

    Returns:
        Dict ``{"nt": [...], "ot": [...], "lxx": [...]}``
        where each list entry is ``(name, num, verses)``.
        The ``lxx`` key contains deuterocanonical books available
        via the LXX1 text module (no lemmas, word-form counts only).
    """
    result: dict[str, list] = {"nt": [], "ot": [], "lxx": []}

    # Morphology-based books (66-book Protestant canon)
    for testament, label in [("nt", "nt"), ("ot", "ot")]:
        verses = _load_morphology(testament)
        if not verses:
            continue

        # Count verse entries per book number
        book_verses: dict[int, int] = defaultdict(int)
        for key in verses:
            parts = key.split("_")
            if len(parts) >= 1:
                try:
                    bnum = int(parts[0])
                    book_verses[bnum] += 1
                except ValueError:
                    pass

        # Map to canonical names
        for bnum in sorted(book_verses):
            name = book_num_to_name(bnum)
            vcount = book_verses[bnum]
            result[label].append((name, bnum, vcount))

    # Deuterocanonical books via LXX1
    if _get_lxx_module():
        for name in DEUTEROCANONICAL_BOOKS.values():
            bolls_id = _DEUTERO_BOLLS_IDS.get(name)
            if bolls_id:
                result["lxx"].append((name, bolls_id, 0))  # verse count unknown

    return result
