"""
Shared canonical module for Bible book identity, name resolution, canon
ordering, and reference parsing.

This is the single source of truth for all book-name handling in the project.
It replaces three independent, inconsistent mappings previously scattered across
bible_api.py, text_module.py, and commentary_module.py.
"""

from __future__ import annotations

import re
from dataclasses import dataclass


# =============================================================================
# 1. Book identity
# =============================================================================


@dataclass(frozen=True)
class Book:
    """Immutable representation of a single Bible book."""

    osis: str  # e.g. "Gen", "Exod", "1Macc"
    name: str  # e.g. "Genesis", "1 Maccabees"
    paratext: str  # e.g. "GEN", "1MA"
    testament: str  # "ot" or "nt"
    is_single_chapter: bool
    bolls_id: int | None = None  # bolls.life API book ID (None if absent)


# ---------------------------------------------------------------------------
# All books that appear in any canon
# ---------------------------------------------------------------------------

# fmt: off
_BOOKS: list[Book] = [
    # === Old Testament (39 proto-canonical) ===
    Book("Gen",    "Genesis",             "GEN", "ot", False,   1),
    Book("Exod",   "Exodus",              "EXO", "ot", False,   2),
    Book("Lev",    "Leviticus",           "LEV", "ot", False,   3),
    Book("Num",    "Numbers",             "NUM", "ot", False,   4),
    Book("Deut",   "Deuteronomy",         "DEU", "ot", False,   5),
    Book("Josh",   "Joshua",              "JOS", "ot", False,   6),
    Book("Judg",   "Judges",              "JDG", "ot", False,   7),
    Book("Ruth",   "Ruth",                "RUT", "ot", False,   8),
    Book("1Sam",   "1 Samuel",            "1SA", "ot", False,   9),
    Book("2Sam",   "2 Samuel",            "2SA", "ot", False,  10),
    Book("1Kgs",   "1 Kings",             "1KI", "ot", False,  11),
    Book("2Kgs",   "2 Kings",             "2KI", "ot", False,  12),
    Book("1Chr",   "1 Chronicles",        "1CH", "ot", False,  13),
    Book("2Chr",   "2 Chronicles",        "2CH", "ot", False,  14),
    Book("Ezra",   "Ezra",                "EZR", "ot", False,  15),
    Book("Neh",    "Nehemiah",            "NEH", "ot", False,  16),
    Book("Esth",   "Esther",              "EST", "ot", False,  17),
    Book("Job",    "Job",                 "JOB", "ot", False,  18),
    Book("Ps",     "Psalms",              "PSA", "ot", False,  19),
    Book("Prov",   "Proverbs",            "PRO", "ot", False,  20),
    Book("Eccl",   "Ecclesiastes",        "ECC", "ot", False,  21),
    Book("Song",   "Song of Solomon",     "SNG", "ot", False,  22),
    Book("Isa",    "Isaiah",              "ISA", "ot", False,  23),
    Book("Jer",    "Jeremiah",            "JER", "ot", False,  24),
    Book("Lam",    "Lamentations",        "LAM", "ot", False,  25),
    Book("Ezek",   "Ezekiel",             "EZK", "ot", False,  26),
    Book("Dan",    "Daniel",              "DAN", "ot", False,  27),
    Book("Hos",    "Hosea",               "HOS", "ot", False,  28),
    Book("Joel",   "Joel",                "JOL", "ot", False,  29),
    Book("Amos",   "Amos",                "AMO", "ot", False,  30),
    Book("Obad",   "Obadiah",             "OBA", "ot", True,   31),
    Book("Jonah",  "Jonah",               "JON", "ot", False,  32),
    Book("Mic",    "Micah",               "MIC", "ot", False,  33),
    Book("Nah",    "Nahum",               "NAM", "ot", False,  34),
    Book("Hab",    "Habakkuk",            "HAB", "ot", False,  35),
    Book("Zeph",   "Zephaniah",           "ZEP", "ot", False,  36),
    Book("Hag",    "Haggai",              "HAG", "ot", False,  37),
    Book("Zech",   "Zechariah",           "ZEC", "ot", False,  38),
    Book("Mal",    "Malachi",             "MAL", "ot", False,  39),

    # === New Testament (27 proto-canonical) ===
    Book("Matt",   "Matthew",             "MAT", "nt", False,  40),
    Book("Mark",   "Mark",                "MRK", "nt", False,  41),
    Book("Luke",   "Luke",                "LUK", "nt", False,  42),
    Book("John",   "John",                "JHN", "nt", False,  43),
    Book("Acts",   "Acts",                "ACT", "nt", False,  44),
    Book("Rom",    "Romans",              "ROM", "nt", False,  45),
    Book("1Cor",   "1 Corinthians",       "1CO", "nt", False,  46),
    Book("2Cor",   "2 Corinthians",       "2CO", "nt", False,  47),
    Book("Gal",    "Galatians",           "GAL", "nt", False,  48),
    Book("Eph",    "Ephesians",           "EPH", "nt", False,  49),
    Book("Phil",   "Philippians",         "PHP", "nt", False,  50),
    Book("Col",    "Colossians",          "COL", "nt", False,  51),
    Book("1Thess", "1 Thessalonians",     "1TH", "nt", False,  52),
    Book("2Thess", "2 Thessalonians",     "2TH", "nt", False,  53),
    Book("1Tim",   "1 Timothy",           "1TI", "nt", False,  54),
    Book("2Tim",   "2 Timothy",           "2TI", "nt", False,  55),
    Book("Titus",  "Titus",               "TIT", "nt", False,  56),
    Book("Phlm",   "Philemon",            "PHM", "nt", True,   57),
    Book("Heb",    "Hebrews",             "HEB", "nt", False,  58),
    Book("Jas",    "James",               "JAS", "nt", False,  59),
    Book("1Pet",   "1 Peter",             "1PE", "nt", False,  60),
    Book("2Pet",   "2 Peter",             "2PE", "nt", False,  61),
    Book("1John",  "1 John",              "1JN", "nt", False,  62),
    Book("2John",  "2 John",              "2JN", "nt", True,   63),
    Book("3John",  "3 John",              "3JN", "nt", True,   64),
    Book("Jude",   "Jude",                "JUD", "nt", True,   65),
    Book("Rev",    "Revelation",          "REV", "nt", False,  66),

    # === Deuterocanonical / Apocrypha ===
    Book("Tob",    "Tobit",               "TOB", "ot", False,  68),
    Book("Jdt",    "Judith",              "JDT", "ot", False,  69),
    Book("AddEsth","Additions to Esther", "AES", "ot", False,  None),
    Book("Wis",    "Wisdom",              "WIS", "ot", False,  70),
    Book("Sir",    "Sirach",              "SIR", "ot", False,  71),
    Book("Bar",    "Baruch",              "BAR", "ot", False,  73),
    Book("EpJer",  "Letter of Jeremiah",  "LJE", "ot", True,   72),
    Book("PrAzar", "Prayer of Azariah",   "AZA", "ot", True,   88),
    Book("Sus",    "Susanna",             "SUS", "ot", True,    78),
    Book("Bel",    "Bel and the Dragon",  "BEL", "ot", True,    79),
    Book("1Macc",  "1 Maccabees",         "1MA", "ot", False,  74),
    Book("2Macc",  "2 Maccabees",         "2MA", "ot", False,  75),
    Book("1Esd",   "1 Esdras",            "1ES", "ot", False,  67),
    Book("2Esd",   "2 Esdras",            "2ES", "ot", False,  77),
    Book("PrMan",  "Prayer of Manasseh",  "MAN", "ot", True,   76),
    Book("AddPs",  "Psalm 151",           "PS2", "ot", True,   None),
    Book("3Macc",  "3 Maccabees",         "3MA", "ot", False,  None),
    Book("4Macc",  "4 Maccabees",         "4MA", "ot", False,  None),
]
# fmt: on

# ---------------------------------------------------------------------------
# Core lookup tables
# ---------------------------------------------------------------------------

BOOKS_BY_OSIS: dict[str, Book] = {b.osis: b for b in _BOOKS}

# bolls_id → OSIS (only for books that have a bolls_id)
_BOLLS_ID_TO_OSIS: dict[int, str] = {
    b.bolls_id: b.osis for b in _BOOKS if b.bolls_id is not None
}


# =============================================================================
# 2. Verse count data (per-chapter verse counts)
# =============================================================================
# Extracted from text_module.py BOOK_VERSE_COUNTS.
# Each tuple: (chapter_num, verses_in_ch, cumulative_before) → we keep only
# the verses_in_ch (index 1).

# --- Proto-canonical verse counts (keyed by OSIS) ---
# fmt: off
_VERSE_COUNTS_PROTO: dict[str, tuple[int, ...]] = {
    # Genesis (50 chapters)
    "Gen": (31,25,24,26,32,22,24,22,29,32,32,20,18,24,21,16,27,33,38,18,
            34,24,20,67,34,35,46,22,35,43,55,33,20,31,29,43,36,30,23,23,
            57,38,34,34,28,34,31,22,33,26),
    # Exodus (40 chapters)
    "Exod": (22,25,22,31,23,30,25,32,35,29,10,51,22,31,27,36,16,27,25,26,
             36,31,33,18,40,37,21,43,46,38,18,35,23,35,35,38,29,31,43,38),
    # Leviticus (27 chapters)
    "Lev": (17,16,17,35,26,30,38,36,24,20,47,8,59,57,33,34,16,30,37,27,
            24,33,44,23,55,46,34),
    # Numbers (36 chapters)
    "Num": (54,34,51,49,31,27,89,26,23,36,35,16,33,45,41,50,13,32,22,29,
            35,41,30,25,18,65,23,31,40,17,54,42,56,29,34,13),
    # Deuteronomy (34 chapters)
    "Deut": (46,37,29,49,33,25,26,20,29,22,32,31,19,29,23,22,20,22,21,20,
             23,30,26,22,19,19,26,68,29,20,30,52,29,12),
    # Joshua (24 chapters)
    "Josh": (18,24,17,24,15,27,26,35,27,43,23,24,33,15,63,10,18,28,51,9,
             45,34,16,33),
    # Judges (21 chapters)
    "Judg": (36,23,31,24,31,40,25,35,57,18,40,15,25,20,20,31,13,31,30,48,25),
    # Ruth (4 chapters)
    "Ruth": (22,23,18,22),
    # 1 Samuel (31 chapters)
    "1Sam": (28,36,21,22,12,21,17,22,27,27,15,25,23,52,35,23,58,30,24,42,
             16,23,28,23,44,25,12,25,11,31,13),
    # 2 Samuel (24 chapters)
    "2Sam": (27,32,39,12,25,23,29,18,13,19,27,31,39,33,37,23,29,33,44,26,
             22,51,39,25),
    # 1 Kings (22 chapters)
    "1Kgs": (53,46,28,34,18,38,51,66,28,29,43,33,34,31,34,34,24,46,21,43,
             29,54),
    # 2 Kings (25 chapters)
    "2Kgs": (18,25,27,44,27,33,20,29,37,36,21,22,25,29,38,20,41,37,37,21,
             26,20,37,20,30),
    # 1 Chronicles (29 chapters)
    "1Chr": (54,55,24,43,26,81,40,40,44,14,47,41,14,17,29,43,27,17,19,8,
             30,19,32,31,31,32,34,21,30),
    # 2 Chronicles (36 chapters)
    "2Chr": (17,18,17,22,14,42,22,18,31,19,23,16,23,15,19,14,19,34,11,37,
             20,12,21,27,28,23,9,27,36,27,21,33,25,33,27,23),
    # Ezra (10 chapters)
    "Ezra": (11,70,13,24,17,22,28,36,15,44),
    # Nehemiah (13 chapters)
    "Neh": (11,20,32,23,19,19,73,18,38,40,36,47,31),
    # Esther (10 chapters, proto-canonical versification)
    "Esth": (22,23,15,17,14,14,10,17,32,3),
    # Job (42 chapters)
    "Job": (22,13,26,21,27,30,21,22,35,22,20,25,28,22,35,22,16,21,29,29,
            34,30,17,25,6,14,23,28,25,31,40,22,33,37,16,33,24,41,30,24,
            34,17),
    # Psalms (150 chapters)
    "Ps": (6,12,9,9,13,11,18,10,21,18,7,9,6,7,5,11,15,51,15,10,
           14,32,6,10,22,12,14,9,11,13,25,11,22,23,28,13,40,23,14,18,
           14,12,5,27,18,12,10,15,21,23,21,11,7,9,24,14,12,12,18,14,
           9,13,12,11,14,20,8,36,37,6,24,20,28,23,11,13,21,72,13,20,
           17,8,19,13,14,17,7,19,53,17,16,16,5,23,11,13,12,9,9,5,
           8,29,22,35,45,48,43,14,31,7,10,10,9,8,18,19,2,176,176,7,
           8,9,4,8,5,6,5,6,8,8,3,18,3,3,21,26,9,8,24,14,
           10,8,12,15,21,10,20,14,9,6),
    # Proverbs (31 chapters)
    "Prov": (33,22,35,27,23,35,27,36,18,32,31,28,25,35,33,33,28,24,29,30,
             31,29,35,34,28,28,27,28,27,33,31),
    # Ecclesiastes (12 chapters)
    "Eccl": (18,26,22,16,20,12,29,17,18,20,10,14),
    # Song of Solomon (8 chapters)
    "Song": (17,17,11,16,16,13,13,14),
    # Isaiah (66 chapters)
    "Isa": (31,22,26,6,30,13,25,22,21,34,16,6,22,32,9,14,14,7,25,6,
            17,25,18,23,12,21,13,29,24,33,9,20,24,17,10,22,38,22,8,31,
            29,25,28,28,25,13,15,22,26,11,23,15,12,17,13,12,21,14,21,22,
            11,12,19,12,25,24),
    # Jeremiah (52 chapters)
    "Jer": (19,37,25,31,31,30,34,22,26,25,23,17,27,22,21,21,27,23,15,18,
            14,30,40,10,38,24,22,17,32,24,40,44,26,22,19,32,21,28,18,16,
            18,22,13,30,5,28,7,47,39,46,64,34),
    # Lamentations (5 chapters)
    "Lam": (22,22,66,22,22),
    # Ezekiel (48 chapters)
    "Ezek": (28,10,27,17,17,14,27,18,11,22,25,28,23,23,8,63,24,32,14,49,
             37,31,49,27,17,21,36,26,21,26,18,32,33,31,15,38,28,23,29,49,
             25,20,27,31,25,24,23,35),
    # Daniel (14 chapters — Catholic; Protestant uses first 12)
    "Dan": (21,49,100,37,31,29,28,27,27,21,45,13,65,42),
    # Hosea (14 chapters)
    "Hos": (11,25,5,19,15,11,16,14,17,15,12,15,15,10),
    # Joel (3 chapters — Protestant/Catholic versification; the Accordance binary
    # uses a 4-chapter Vulgate versification, but canonically Joel has 3 chapters)
    "Joel": (20,32,21),
    # Amos (9 chapters)
    "Amos": (15,16,15,13,27,14,17,14,15),
    # Obadiah (1 chapter, single-chapter book)
    "Obad": (21,),
    # Jonah (4 chapters)
    "Jonah": (17,11,10,11),
    # Micah (7 chapters)
    "Mic": (16,13,12,14,14,16,20),
    # Nahum (3 chapters)
    "Nah": (14,14,19),
    # Habakkuk (3 chapters)
    "Hab": (17,20,19),
    # Zephaniah (3 chapters)
    "Zeph": (18,15,20),
    # Haggai (2 chapters)
    "Hag": (15,23),
    # Zechariah (14 chapters)
    "Zech": (17,17,10,14,11,15,14,23,17,12,17,14,9,21),
    # Malachi (4 chapters)
    "Mal": (14,17,18,6),
    # Matthew (28 chapters)
    "Matt": (25,23,17,25,48,34,29,34,38,42,30,50,58,36,39,28,27,35,30,34,
             46,46,39,51,46,75,66,20),
    # Mark (16 chapters)
    "Mark": (45,28,35,41,43,56,37,38,49,52,33,44,37,72,47,20),
    # Luke (24 chapters)
    "Luke": (80,52,38,44,39,49,50,56,62,42,54,59,35,35,32,31,37,43,48,47,
             38,71,56,53),
    # John (21 chapters)
    "John": (51,25,36,54,47,71,53,59,41,42,57,50,38,31,27,33,26,40,42,31,25),
    # Acts (28 chapters)
    "Acts": (26,47,26,37,42,15,60,40,43,48,30,25,52,28,41,40,34,28,41,38,
             40,30,35,27,27,32,44,31),
    # Romans (16 chapters)
    "Rom": (32,29,31,25,21,23,25,39,33,21,36,21,14,23,33,27),
    # 1 Corinthians (16 chapters)
    "1Cor": (31,16,23,21,13,20,40,13,27,33,34,31,13,40,58,24),
    # 2 Corinthians (13 chapters)
    "2Cor": (24,17,18,18,21,18,16,24,15,18,33,21,14),
    # Galatians (6 chapters)
    "Gal": (24,21,29,31,26,18),
    # Ephesians (6 chapters)
    "Eph": (23,22,21,32,33,24),
    # Philippians (4 chapters)
    "Phil": (30,30,21,23),
    # Colossians (4 chapters)
    "Col": (29,23,25,18),
    # 1 Thessalonians (5 chapters)
    "1Thess": (10,20,13,18,28),
    # 2 Thessalonians (3 chapters)
    "2Thess": (12,17,18),
    # 1 Timothy (6 chapters)
    "1Tim": (20,15,16,16,25,21),
    # 2 Timothy (4 chapters)
    "2Tim": (18,26,17,22),
    # Titus (3 chapters)
    "Titus": (16,15,15),
    # Philemon (1 chapter, single-chapter book)
    "Phlm": (25,),
    # Hebrews (13 chapters)
    "Heb": (14,18,19,16,14,20,28,13,28,39,40,29,25),
    # James (5 chapters)
    "Jas": (27,26,18,17,20),
    # 1 Peter (5 chapters)
    "1Pet": (25,25,22,19,14),
    # 2 Peter (3 chapters)
    "2Pet": (21,22,18),
    # 1 John (5 chapters)
    "1John": (10,29,24,21,21),
    # 2 John (1 chapter, single-chapter book)
    "2John": (13,),
    # 3 John (1 chapter, single-chapter book)
    "3John": (15,),
    # Jude (1 chapter, single-chapter book)
    "Jude": (25,),
    # Revelation (22 chapters)
    "Rev": (20,29,22,11,14,17,17,13,21,11,19,18,18,20,8,21,18,24,21,15,
            27,21),
}

# --- Deuterocanonical verse counts (keyed by OSIS) ---
_VERSE_COUNTS_DEUTERO: dict[str, tuple[int, ...]] = {
    "Tob":  (22,14,17,21,23,18,17,21,6,14,18,22,18,15),
    "Jdt":  (16,28,10,15,24,21,32,36,14,23,23,20,20,19,14,25),
    "1Macc": (64,70,60,61,68,63,50,32,73,89,74,53,53,49,41,24),
    "2Macc": (36,32,40,50,27,31,42,7,29,38,33,46,26,46,40,),  # 15 chapters typical
    "Sir":  (30,18,31,31,15,37,36,19,18,31,34,18,26,27,20,30,32,33,30,32,
             28,27,28,34,26,29,30,26,28,25,31, 23,33,31,26,31,31,34,35,30,
             27,25,33,23,26,20,25,25,16,29,30),
    "Wis":  (16,24,19,20,23,25,22,21,18,21,26,27,20,31,19,31,21,20,22),
    "Bar":  (22,35,38,37,9,73),
    # Deuterocanonical books with no established chapter/verse data in our
    # existing sources — placeholder empty tuples.
    "AddEsth": (),
    "EpJer":   (),
    "PrAzar":  (),
    "Sus":     (),
    "Bel":     (),
    "1Esd":    (),
    "2Esd":    (),
    "PrMan":   (),
    "AddPs":   (),
    "3Macc":   (),
    "4Macc":   (),
}
# fmt: on

_VERSE_COUNTS: dict[str, tuple[int, ...]] = {}
_VERSE_COUNTS.update(_VERSE_COUNTS_PROTO)
_VERSE_COUNTS.update(_VERSE_COUNTS_DEUTERO)


# =============================================================================
# 3. Canons
# =============================================================================


@dataclass
class Canon:
    """Ordered collection of books plus per-book chapter-verse metadata."""

    name: str  # e.g. "protestant", "catholic"
    books: tuple[Book, ...]  # ordered
    last_verse: dict[str, tuple[int, ...]]  # OSIS → (v1_ch1, v2_ch2, ...)


# --- Protestant canon: 66 books in standard KJV order ---
_PROTESTANT_ORDER: tuple[str, ...] = (
    # OT (39)
    "Gen",
    "Exod",
    "Lev",
    "Num",
    "Deut",
    "Josh",
    "Judg",
    "Ruth",
    "1Sam",
    "2Sam",
    "1Kgs",
    "2Kgs",
    "1Chr",
    "2Chr",
    "Ezra",
    "Neh",
    "Esth",
    "Job",
    "Ps",
    "Prov",
    "Eccl",
    "Song",
    "Isa",
    "Jer",
    "Lam",
    "Ezek",
    "Dan",
    "Hos",
    "Joel",
    "Amos",
    "Obad",
    "Jonah",
    "Mic",
    "Nah",
    "Hab",
    "Zeph",
    "Hag",
    "Zech",
    "Mal",
    # NT (27)
    "Matt",
    "Mark",
    "Luke",
    "John",
    "Acts",
    "Rom",
    "1Cor",
    "2Cor",
    "Gal",
    "Eph",
    "Phil",
    "Col",
    "1Thess",
    "2Thess",
    "1Tim",
    "2Tim",
    "Titus",
    "Phlm",
    "Heb",
    "Jas",
    "1Pet",
    "2Pet",
    "1John",
    "2John",
    "3John",
    "Jude",
    "Rev",
)

_PROTESTANT_LAST_VERSE: dict[str, tuple[int, ...]] = {}
for _osis in _PROTESTANT_ORDER:
    _vc = _VERSE_COUNTS.get(_osis, ())
    if _osis == "Dan":
        # Protestant Daniel has 12 chapters (drop chapters 13-14)
        _vc = _vc[:12]
    _PROTESTANT_LAST_VERSE[_osis] = _vc

CANON_PROTESTANT = Canon(
    name="protestant",
    books=tuple(BOOKS_BY_OSIS[o] for o in _PROTESTANT_ORDER),
    last_verse=_PROTESTANT_LAST_VERSE,
)


# --- Catholic canon: 73 books ---
# Order: Gen–Neh (16), Tob, Jdt, Esth, 1Macc, 2Macc, Job–Song (5),
#        Wis, Sir, Isa–Lam (5), Bar, Ezek, Dan (14 ch), Hos–Mal (12),
#        Matt–Rev (27)
_CATHOLIC_ORDER: tuple[str, ...] = (
    # Gen through Nehemiah (same as Protestant) — 16 books
    "Gen",
    "Exod",
    "Lev",
    "Num",
    "Deut",
    "Josh",
    "Judg",
    "Ruth",
    "1Sam",
    "2Sam",
    "1Kgs",
    "2Kgs",
    "1Chr",
    "2Chr",
    "Ezra",
    "Neh",
    # Deuterocanonical insertions
    "Tob",
    "Jdt",
    "Esth",
    "1Macc",
    "2Macc",
    # Job through Song of Solomon
    "Job",
    "Ps",
    "Prov",
    "Eccl",
    "Song",
    # Deuterocanonical: Wisdom, Sirach
    "Wis",
    "Sir",
    # Isaiah through Lamentations
    "Isa",
    "Jer",
    "Lam",
    # Baruch (6 chapters; ch 6 = Letter of Jeremiah)
    "Bar",
    # Ezekiel
    "Ezek",
    # Daniel (14 chapters in Catholic canon)
    "Dan",
    # Hosea through Malachi
    "Hos",
    "Joel",
    "Amos",
    "Obad",
    "Jonah",
    "Mic",
    "Nah",
    "Hab",
    "Zeph",
    "Hag",
    "Zech",
    "Mal",
    # New Testament (27)
    "Matt",
    "Mark",
    "Luke",
    "John",
    "Acts",
    "Rom",
    "1Cor",
    "2Cor",
    "Gal",
    "Eph",
    "Phil",
    "Col",
    "1Thess",
    "2Thess",
    "1Tim",
    "2Tim",
    "Titus",
    "Phlm",
    "Heb",
    "Jas",
    "1Pet",
    "2Pet",
    "1John",
    "2John",
    "3John",
    "Jude",
    "Rev",
)

_CATHOLIC_LAST_VERSE: dict[str, tuple[int, ...]] = {}
for _osis in _CATHOLIC_ORDER:
    # Catholic Daniel uses all 14 chapters
    _CATHOLIC_LAST_VERSE[_osis] = _VERSE_COUNTS.get(_osis, ())

CANON_CATHOLIC = Canon(
    name="catholic",
    books=tuple(BOOKS_BY_OSIS[o] for o in _CATHOLIC_ORDER),
    last_verse=_CATHOLIC_LAST_VERSE,
)

DEFAULT_CANON: Canon = CANON_PROTESTANT

_CANONS_BY_NAME: dict[str, Canon] = {
    "protestant": CANON_PROTESTANT,
    "catholic": CANON_CATHOLIC,
}


# =============================================================================
# 4. Name resolution
# =============================================================================

# --- Build NAME_TO_OSIS ---
_NAME_TO_OSIS: dict[str, str] = {}

# 1. OSIS keys lowercased
for b in _BOOKS:
    _NAME_TO_OSIS[b.osis.lower()] = b.osis

# 2. Paratext codes lowercased
for b in _BOOKS:
    _NAME_TO_OSIS[b.paratext.lower()] = b.osis

# 3. Full names with spaces, lowercased
for b in _BOOKS:
    _NAME_TO_OSIS[b.name.lower()] = b.osis

# 4. Full names despaced (lowercased)
for b in _BOOKS:
    _NAME_TO_OSIS[b.name.lower().replace(" ", "")] = b.osis

# 5. Abbreviations from bible_api.py _ABBREVIATIONS
#    Keys are lowercased, spaces removed. Values are bolls_id.
#    Map bolls_id → OSIS via _BOLLS_ID_TO_OSIS.
_ABBREVIATIONS_RAW: dict[str, int] = {
    # Old Testament
    "gen": 1,
    "exo": 2,
    "exod": 2,
    "lev": 3,
    "num": 4,
    "deut": 5,
    "dt": 5,
    "josh": 6,
    "judg": 7,
    "jdg": 7,
    "ruth": 8,
    "1sam": 9,
    "2sam": 10,
    "1kgs": 11,
    "2kgs": 12,
    "1chr": 13,
    "1chron": 13,
    "2chr": 14,
    "2chron": 14,
    "ezra": 15,
    "neh": 16,
    "esth": 17,
    "est": 17,
    "job": 18,
    "ps": 19,
    "psa": 19,
    "pss": 19,
    "psalm": 19,
    "prov": 20,
    "prv": 20,
    "eccl": 21,
    "ecc": 21,
    "song": 22,
    "sos": 22,
    "songofsongs": 22,
    "canticles": 22,
    "isa": 23,
    "jer": 24,
    "lam": 25,
    "ezek": 26,
    "ezk": 26,
    "dan": 27,
    "hos": 28,
    "joel": 29,
    "amos": 30,
    "obad": 31,
    "obadiah": 31,
    "jonah": 32,
    "jon": 32,
    "mic": 33,
    "nah": 34,
    "hab": 35,
    "zeph": 36,
    "zep": 36,
    "hag": 37,
    "zech": 38,
    "zec": 38,
    "mal": 39,
    # New Testament
    "matt": 40,
    "mt": 40,
    "mark": 41,
    "mk": 41,
    "luke": 42,
    "lk": 42,
    "john": 43,
    "jn": 43,
    "acts": 44,
    "rom": 45,
    "1cor": 46,
    "2cor": 47,
    "gal": 48,
    "eph": 49,
    "phil": 50,
    "php": 50,
    "col": 51,
    "1thess": 52,
    "1th": 52,
    "2thess": 53,
    "2th": 53,
    "1tim": 54,
    "2tim": 55,
    "titus": 56,
    "tit": 56,
    "philem": 57,
    "phlm": 57,
    "heb": 58,
    "jas": 59,
    "1pet": 60,
    "1pt": 60,
    "2pet": 61,
    "2pt": 61,
    "1john": 62,
    "1jn": 62,
    "2john": 63,
    "2jn": 63,
    "3john": 64,
    "3jn": 64,
    "jude": 65,
    "jud": 65,
    "rev": 66,
    # Deuterocanonical
    "tob": 68,
    "tobit": 68,
    "jdt": 69,
    "jth": 69,
    "wis": 70,
    "wisdom": 70,
    "sir": 71,
    "sirach": 71,
    "ecclesiasticus": 71,
    "bar": 73,
    "baruch": 73,
    "1macc": 74,
    "1mac": 74,
    "1maccabees": 74,
    "2macc": 75,
    "2mac": 75,
    "2maccabees": 75,
}
for _abbr, _bid in _ABBREVIATIONS_RAW.items():
    _osis = _BOLLS_ID_TO_OSIS.get(_bid)
    if _osis:
        _NAME_TO_OSIS[_abbr] = _osis

# 6. Aliases from text_module.py BOOK_ALIASES
#    Normalize the right-side values (spaced names) → OSIS via Book lookup.
_TEXT_BOOK_ALIASES: dict[str, str] = {
    "psalm": "Psalms",
    "psalms": "Psalms",
    "ps": "Psalms",
    "genesis": "Genesis",
    "gen": "Genesis",
    "exodus": "Exodus",
    "ex": "Exodus",
    "leviticus": "Leviticus",
    "lev": "Leviticus",
    "numbers": "Numbers",
    "num": "Numbers",
    "deuteronomy": "Deuteronomy",
    "deut": "Deuteronomy",
    "joshua": "Joshua",
    "josh": "Joshua",
    "judges": "Judges",
    "ruth": "Ruth",
    "1samuel": "1 Samuel",
    "2samuel": "2 Samuel",
    "1sam": "1 Samuel",
    "2sam": "2 Samuel",
    "1kings": "1 Kings",
    "2kings": "2 Kings",
    "1chronicles": "1 Chronicles",
    "2chronicles": "2 Chronicles",
    "1chr": "1 Chronicles",
    "2chr": "2 Chronicles",
    "ezra": "Ezra",
    "nehemiah": "Nehemiah",
    "neh": "Nehemiah",
    "tobit": "Tobit",
    "tob": "Tobit",
    "judith": "Judith",
    "esther": "Esther",
    "1maccabees": "1 Maccabees",
    "2maccabees": "2 Maccabees",
    "1macc": "1 Maccabees",
    "2macc": "2 Maccabees",
    "job": "Job",
    "proverbs": "Proverbs",
    "prov": "Proverbs",
    "ecclesiastes": "Ecclesiastes",
    "sirach": "Sirach",
    "isaiah": "Isaiah",
    "isa": "Isaiah",
    "jeremiah": "Jeremiah",
    "jer": "Jeremiah",
    "lamentations": "Lamentations",
    "ezekiel": "Ezekiel",
    "daniel": "Daniel",
    "dan": "Daniel",
    "hosea": "Hosea",
    "hos": "Hosea",
    "joel": "Joel",
    "amos": "Amos",
    "obadiah": "Obadiah",
    "jonah": "Jonah",
    "micah": "Micah",
    "nahum": "Nahum",
    "habakkuk": "Habakkuk",
    "zephaniah": "Zephaniah",
    "haggai": "Haggai",
    "zechariah": "Zechariah",
    "malachi": "Malachi",
    "matthew": "Matthew",
    "matt": "Matthew",
    "mark": "Mark",
    "luke": "Luke",
    "john": "John",
    "acts": "Acts",
    "romans": "Romans",
    "rom": "Romans",
    "1corinthians": "1 Corinthians",
    "2corinthians": "2 Corinthians",
    "1cor": "1 Corinthians",
    "2cor": "2 Corinthians",
    "galatians": "Galatians",
    "gal": "Galatians",
    "ephesians": "Ephesians",
    "eph": "Ephesians",
    "philippians": "Philippians",
    "colossians": "Colossians",
    "1thessalonians": "1 Thessalonians",
    "2thessalonians": "2 Thessalonians",
    "1timothy": "1 Timothy",
    "2timothy": "2 Timothy",
    "titus": "Titus",
    "philemon": "Philemon",
    "hebrews": "Hebrews",
    "james": "James",
    "1peter": "1 Peter",
    "2peter": "2 Peter",
    "1john": "1 John",
    "2john": "2 John",
    "3john": "3 John",
    "jude": "Jude",
    "revelation": "Revelation",
    "rev": "Revelation",
}
for _key, _spaced_name in _TEXT_BOOK_ALIASES.items():
    # Resolve the spaced name to OSIS via the Book name→OSIS mapping
    _osis = _NAME_TO_OSIS.get(_spaced_name.lower())
    if _osis:
        _NAME_TO_OSIS[_key] = _osis

# 7. Additional special cases
_SPECIAL_CASES: dict[str, str] = {
    "canticles": "Song",
    "songofsongs": "Song",
    "ecclesiasticus": "Sir",
    "qoheleth": "Eccl",
    "qoh": "Eccl",
    "sirach": "Sir",
    "tobias": "Tob",
    "1mc": "1Macc",
    "2mc": "2Macc",
    "wisdomofsolomon": "Wis",
}
_NAME_TO_OSIS.update(_SPECIAL_CASES)

# =========================================================================
# Latin Vulgate + German abbreviations (Catholic scholarship conventions)
# =========================================================================

_NAME_TO_OSIS.update(
    {
        # Latin Vulgate — OT
        "ios": "Josh",
        "iosue": "Josh",
        "iudc": "Judg",
        "iudicum": "Judg",
        "ireg": "1Kgs",
        "iregum": "1Kgs",
        "iireg": "2Kgs",
        "iiregum": "2Kgs",
        "iiireg": "1Kgs",
        "iiiregum": "1Kgs",
        "ivreg": "2Kgs",
        "ivregum": "2Kgs",
        "ipar": "1Chr",
        "iparalipomenon": "1Chr",
        "iipar": "2Chr",
        "iiparalipomenon": "2Chr",
        "iesd": "Ezra",
        "iesdrae": "Ezra",
        "iiesd": "Neh",
        "iiesdrae": "Neh",
        "tob": "Tob",
        "tobiae": "Tob",
        "iudth": "Jdt",
        "iob": "Job",
        "cant": "Song",
        "cantici": "Song",
        "canticum": "Song",
        "canticumcanticorum": "Song",
        "sap": "Wis",
        "sapientiae": "Wis",
        "eccli": "Sir",
        "ecclesiastici": "Sir",
        "is": "Isa",
        "isaias": "Isa",
        "ier": "Jer",
        "ieremias": "Jer",
        "bar": "Bar",
        "ez": "Ezek",
        "ezechiel": "Ezek",
        "os": "Hos",
        "osee": "Hos",
        "ioel": "Joel",
        "am": "Amos",
        "abd": "Obad",
        "abdias": "Obad",
        "ion": "Jonah",
        "ionas": "Jonah",
        "mich": "Mic",
        "michaeas": "Mic",
        "nah": "Nah",
        "hab": "Hab",
        "habacuc": "Hab",
        "soph": "Zeph",
        "sophonias": "Zeph",
        "agg": "Hag",
        "aggaeus": "Hag",
        "zach": "Zech",
        "zacharias": "Zech",
        "imac": "1Macc",
        "imachabaeorum": "1Macc",
        "iimac": "2Macc",
        "iimachabaeorum": "2Macc",
        # Latin Vulgate — NT
        "marc": "Mark",
        "luc": "Luke",
        "ioan": "John",
        "ioannes": "John",
        "act": "Acts",
        "actus": "Acts",
        "icor": "1Cor",
        "iicor": "2Cor",
        "hebr": "Heb",
        "hebraeos": "Heb",
        "iac": "Jas",
        "iacobi": "Jas",
        "ipetr": "1Pet",
        "iipetr": "2Pet",
        "iioan": "1John",
        "iiioan": "2John",
        "iiiiioan": "3John",
        "iud": "Jude",
        "apoc": "Rev",
        "apocalypse": "Rev",
        "apocalypsis": "Rev",
        # German
        "1mose": "Gen",
        "2mose": "Exod",
        "3mose": "Lev",
        "4mose": "Num",
        "5mose": "Deut",
        "jesaja": "Isa",
        "jeremia": "Jer",
        "hes": "Ezek",
        "hesk": "Ezek",
        "hesekiel": "Ezek",
        "psalter": "Ps",
        "prediger": "Eccl",
        "pred": "Eccl",
        "hohelied": "Song",
        "hld": "Song",
        "offb": "Rev",
        "offenbarung": "Rev",
        "weisheit": "Wis",
        "weish": "Wis",
        "jesussirach": "Sir",
        "klgl": "Lam",
        "klagelieder": "Lam",
        "spr": "Prov",
        "sprueche": "Prov",
        "sprichwoerter": "Prov",
    }
)

# 8. Commentary-module spaceless names (lowercased despaced full names)
#    These are already covered by step 4, but ensure coverage of all
#    proto-canonical books' despaced forms (which matches the commentary
#    module's convention when lowercased).

# --- Expose as NAME_TO_OSIS ---
NAME_TO_OSIS: dict[str, str] = _NAME_TO_OSIS


# --- Build SPACELESS_TO_OSIS and OSIS_TO_SPACELESS ---
# Commentary module convention: TitleCase spaceless names
# e.g. "1Corinthians" → "1Cor", "SongofSolomon" → "Song"
SPACELESS_TO_OSIS: dict[str, str] = {}
OSIS_TO_SPACELESS: dict[str, str] = {}
for b in _BOOKS:
    if b.osis not in OSIS_TO_SPACELESS:
        _spaceless = b.name.replace(" ", "")
        SPACELESS_TO_OSIS[_spaceless] = b.osis
        OSIS_TO_SPACELESS[b.osis] = _spaceless


# =============================================================================
# 5. Psalm numbering: LXX/Greek (Catholic) vs MT/Hebrew (Protestant)
# =============================================================================
# The Septuagint (LXX) numbering of Psalms differs from the Masoretic Text
# (standard Protestant) numbering for Psalms 9-147.  Catholic lectionaries
# and NABRE printed Bibles use LXX numbers; most commentaries and the
# bolls.life API use MT numbers.
#
# Summary of the divergence:
#   LXX  1-8    = MT  1-8    (identical)
#   LXX  9      = MT  9+10   (LXX combines two MT Psalms into one)
#   LXX 10-112  = MT 11-113  (shift by +1)
#   LXX 113     = MT 114+115 (LXX combines two MT Psalms into one)
#   LXX 114-115 = MT 116     (LXX splits MT 116 into two)
#   LXX 116-145 = MT 117-146 (shift by +1)
#   LXX 146-147 = MT 147     (LXX splits MT 147 into two)
#   LXX 148-150 = MT 148-150 (identical)
#
# For chapter-level lookups, the mapping resolves to the primary MT chapter.
# The combined/split cases (9, 113, 114-115, 146-147) require verse-level
# awareness for perfect accuracy.  See psalm_lxx_to_mt() docstring.
# =============================================================================

# fmt: off
# LXX Psalm chapter → MT Psalm chapter (primary chapter number)
_LXX_TO_MT_PSALM_MAP: dict[int, int] = {}
# 1-8: identical
for _ch in range(1, 9):
    _LXX_TO_MT_PSALM_MAP[_ch] = _ch
# 9: LXX combines MT 9+10 → map to MT 9 (primary)
_LXX_TO_MT_PSALM_MAP[9] = 9
# 10-112: LXX is one behind MT (+1)
for _ch in range(10, 113):
    _LXX_TO_MT_PSALM_MAP[_ch] = _ch + 1
# 113: LXX combines MT 114+115 → map to MT 114 (primary)
_LXX_TO_MT_PSALM_MAP[113] = 114
# 114-115: LXX splits MT 116 → both map to MT 116
_LXX_TO_MT_PSALM_MAP[114] = 116
_LXX_TO_MT_PSALM_MAP[115] = 116
# 116-145: LXX is one behind MT (+1)
for _ch in range(116, 146):
    _LXX_TO_MT_PSALM_MAP[_ch] = _ch + 1
# 146-147: LXX splits MT 147 → both map to MT 147
_LXX_TO_MT_PSALM_MAP[146] = 147
_LXX_TO_MT_PSALM_MAP[147] = 147
# 148-150: identical
for _ch in range(148, 151):
    _LXX_TO_MT_PSALM_MAP[_ch] = _ch
# fmt: on

LXX_TO_MT_PSALM: dict[int, int] = _LXX_TO_MT_PSALM_MAP
"""Mapping from LXX/Greek Psalm chapter numbers to MT/standard chapter numbers.

For the combined/split cases:
  - LXX 9  → MT 9  (primary; LXX 9 combines MT 9+10)
  - LXX 113 → MT 114 (primary; LXX 113 combines MT 114+115)
  - LXX 114 → MT 116 (LXX splits MT 116 into 114+115)
  - LXX 146 → MT 147 (LXX splits MT 147 into 146+147)
These cases need verse-level awareness for perfect accuracy in boundary regions.
For all other chapters the mapping is a straightforward chapter-number shift.
"""


def psalm_lxx_to_mt(lxx_chapter: int) -> int:
    """Convert an LXX/Greek Psalm chapter number to the MT/standard equivalent.

    Returns the MT chapter number as an integer.  The returned value is the
    *primary* MT chapter for combined/split cases (see LXX_TO_MT_PSALM docs).

    Raises ValueError if the chapter is not a valid Psalm chapter (1-150).
    """
    if lxx_chapter < 1 or lxx_chapter > 150:
        raise ValueError(f"Psalm chapter must be 1-150, got {lxx_chapter}")
    return LXX_TO_MT_PSALM[lxx_chapter]


def psalm_mt_to_lxx(mt_chapter: int) -> list[int]:
    """Convert an MT/standard Psalm chapter number to its LXX equivalent(s).

    Returns a list of LXX chapter numbers because some MT Psalms are split
    across multiple LXX chapters (e.g., MT 147 → LXX [146, 147]).

    Raises ValueError if the chapter is not a valid Psalm chapter (1-150).
    """
    if mt_chapter < 1 or mt_chapter > 150:
        raise ValueError(f"Psalm chapter must be 1-150, got {mt_chapter}")
    result: list[int] = []
    for lxx, mt in LXX_TO_MT_PSALM.items():
        if mt == mt_chapter and lxx not in result:
            result.append(lxx)
    return sorted(result)


# =============================================================================
# 6. Reference parser (see section 5 for psalm numbering context)
# =============================================================================

# Regex: non-greedy book name (no commas) + space + digits (chapter/verse) +
#        optional colon+digits (verse) + optional dash+digits (end verse).
# Dashes include hyphens, en-dashes (U+2013), and em-dashes (U+2014).
# [^,]+? prevents the regex from consuming comma-separated skip ranges
# (e.g. "Exodus 34:4-6, 8-9") as part of the book name.
_REF_RE = re.compile(
    r"^([^,]+?)\s+(\d+)(?::(\d+))?(?:[\u2013\u2014\-](\d+)(?::(\d+))?)?$"
)


# Regex for trailing verse segments in comma-separated refs: "V" or "V-V".
_SEG_RE = re.compile(r"^(\d+)(?:[\u2013\u2014\-](\d+))?$")

# Regex for semicolon-separated trailing segment: "C:V" or "C:V-V" (no book name).
_SEMICOLON_SEG_RE = re.compile(r"^(\d+):(\d+)(?:[\u2013\u2014\-](\d+))?$")

# Regex to strip verse sub-part suffixes (a, b, c, d, e) from digits.
# \b ensures we don't match inside abbreviations like "1cor".
_SUBPART_RE = re.compile(r"(\d+)[a-f]+\b")


def _parse_comma_segments(
    first_match: re.Match[str],
    trailing_segments: list[str],
) -> ReferenceResult | None:
    """Parse a comma-separated Bible reference from its first match and
    trailing verse segments (e.g. ``['8-9']`` for ``Exodus 34:4-6, 8-9``)."""
    book_name = first_match.group(1).strip()
    num1_str = first_match.group(2)
    num2_str = first_match.group(3)
    num3_str = first_match.group(4)

    book = resolve_book(book_name)
    if book is None:
        return None

    num1 = int(num1_str)
    num2 = int(num2_str) if num2_str else None
    num3 = int(num3_str) if num3_str else None

    # Determine chapter and first-segment verse range
    # (same disambiguation as the main parse_reference body)
    if book.is_single_chapter:
        if num2 is None:
            chapter = 1
            first_start = num1
            first_end = num3 if num3 is not None else num1
        else:
            chapter = num1  # always 1
            first_start = num2
            first_end = num3 if num3 is not None else num2
    else:
        chapter = num1
        if num2 is None:
            # Chapter-only first segment (unusual but handle it)
            first_start = None
            first_end = None
        else:
            first_start = num2
            first_end = num3 if num3 is not None else num2

    # Collect all verses
    all_verses: list[int] = []
    if first_start is not None and first_end is not None:
        all_verses.extend(range(first_start, first_end + 1))

    for seg in trailing_segments:
        sm = _SEG_RE.match(seg)
        if not sm:
            continue
        sv1 = int(sm.group(1))
        sv2_str = sm.group(2)
        sv2 = int(sv2_str) if sv2_str else sv1
        all_verses.extend(range(sv1, sv2 + 1))

    if not all_verses:
        return None

    return ReferenceResult(
        book_osis=book.osis,
        chapter=chapter,
        verse=all_verses[0],
        end_verse=all_verses[-1],
        verse_list=all_verses,
    )


@dataclass
class ReferenceResult:
    """Parsed Bible reference."""

    book_osis: str
    chapter: int
    verse: int | None  # None when only chapter given (e.g. "Psalm 119")
    end_verse: int | None
    verse_list: list[int] | None = None  # All verses in comma-separated skip-ranges


def parse_reference(
    ref: str,
    canon: str = "protestant",
) -> ReferenceResult | None:
    """Parse a Bible reference string.

    Handles:
      - "Genesis 1:1" → chapter=1, verse=1
      - "Genesis 1:1-3" → chapter=1, verse=1, end_verse=3
      - "Genesis 1:1-3, 5-7" → chapter=1, verse_list=[1,2,3,5,6,7]
      - "Ps 119" → chapter=119, verse=None (Psalms is multi-chapter)
      - "Jude 4" → chapter=1, verse=4 (single-chapter book, no colon)
      - "Jude 1:4" → chapter=1, verse=4 (single-chapter, explicit colon)
      - "1 Samuel 3:4" → non-greedy regex handles "1 Samuel" correctly
      - "Song of Solomon 1:1" → multi-word book names
      - "Canticles 1:1" → alias resolution
      - "Psalms 67:2-3, 5, 6+8" → '+' treated as comma, sub-parts stripped
      - "Psalms 16:5, 8, 9-10, 11b" → '11b' → verse 11
      - "Acts 22:30; 23:6-11" → semicolons: first segment returned if chapters differ
    """
    ref = ref.strip()
    if not ref:
        return None
    # ---- Preprocessing: Psalm verse sub-part notation (US lectionary) ----
    # "+" means "and also" (separate verse), treat as comma (with space
    # for clean splitting downstream).
    ref = ref.replace("+", ", ")
    # Strip verse sub-part suffixes (a, b, c, d, e) from verse numbers.
    ref = _SUBPART_RE.sub(r"\1", ref)
    # Collapse multiple white-space chars (including non-breaking spaces).
    ref = re.sub(r"\s+", " ", ref)

    # ---- Semicolon handling: e.g. "Acts 22:30; 23:6-11" ----
    # Must come BEFORE _REF_RE.match because ; can be consumed by [^,]+?
    # inside the book-name capture group.
    if "; " in ref:
        segments = [s.strip() for s in ref.split("; ")]
        if len(segments) < 2:
            return None

        # Parse first segment to get the book.
        first_result = parse_reference(segments[0])
        if first_result is None:
            return None

        # Collect verses from the first segment.
        all_verses: set[int] = set()
        if first_result.verse_list:
            all_verses.update(first_result.verse_list)
        elif first_result.verse is not None:
            if first_result.end_verse is not None:
                all_verses.update(range(first_result.verse, first_result.end_verse + 1))
            else:
                all_verses.add(first_result.verse)

        # Parse subsequent segments as "chapter:verse-range" (no book).
        same_chapter = True
        for seg in segments[1:]:
            sm = _SEMICOLON_SEG_RE.match(seg)
            if not sm:
                same_chapter = False
                break
            ch = int(sm.group(1))
            if ch != first_result.chapter:
                same_chapter = False
                break
            v1 = int(sm.group(2))
            v2_str = sm.group(3)
            v2 = int(v2_str) if v2_str else v1
            all_verses.update(range(v1, v2 + 1))

        if same_chapter and all_verses:
            sorted_verses = sorted(all_verses)
            return ReferenceResult(
                book_osis=first_result.book_osis,
                chapter=first_result.chapter,
                verse=sorted_verses[0],
                end_verse=sorted_verses[-1],
                verse_list=sorted_verses,
            )
        else:
            # Chapters differ or parse failed — return first segment only.
            return first_result

    match = _REF_RE.match(ref)
    if not match:
        # ---- Comma-separated skip-range handling ----
        # Try splitting on ", " to handle refs like "Exodus 34:4-6, 8-9"
        if ", " not in ref:
            return None

        segments = [s.strip() for s in ref.split(", ")]
        if len(segments) < 2:
            return None

        # Parse first segment (contains book name and chapter)
        first_match = _REF_RE.match(segments[0])
        if not first_match:
            return None

        result = _parse_comma_segments(first_match, segments[1:])
        return result

    book_name = match.group(1).strip()
    num1_str = match.group(2)
    num2_str = match.group(3)  # verse (if colon present)
    num3_str = match.group(4)  # after dash: end-verse (same-ch) or end-ch (cross-ch)
    num4_str = match.group(5)  # end-verse when cross-chapter (C:V-C:V)

    book = resolve_book(book_name)
    if book is None:
        return None

    num1 = int(num1_str)
    num2 = int(num2_str) if num2_str else None
    num3 = int(num3_str) if num3_str else None
    num4 = int(num4_str) if num4_str else None

    # Single-chapter disambiguation
    if book.is_single_chapter:
        if num2 is None:
            # No colon: the number is the verse, chapter is implicitly 1
            return ReferenceResult(
                book_osis=book.osis,
                chapter=1,
                verse=num1,
                end_verse=num3,
            )
        else:
            # Explicit colon: num1 is chapter (always 1), num2 is verse
            return ReferenceResult(
                book_osis=book.osis,
                chapter=num1,
                verse=num2,
                end_verse=num3,
            )
    else:
        # Multi-chapter book: num1 is always chapter
        if num4 is not None and num2 is not None:
            # Cross-chapter range: "C:V-C:V" — end verse is num4 (num3 = end chapter)
            return ReferenceResult(
                book_osis=book.osis,
                chapter=num1,
                verse=num2,
                end_verse=num4,
            )
        else:
            return ReferenceResult(
                book_osis=book.osis,
                chapter=num1,
                verse=num2,
                end_verse=num3,
            )


# =============================================================================
# 7. Public API functions
# =============================================================================


def canon_by_name(name: str) -> Canon:
    """Return the Canon with the given name."""
    if name not in _CANONS_BY_NAME:
        raise ValueError(f"Unknown canon: {name!r}. Available: {list(_CANONS_BY_NAME)}")
    return _CANONS_BY_NAME[name]


def chapter_count(book_osis: str, canon: Canon = DEFAULT_CANON) -> int:
    """Return the number of chapters for a book in the given canon."""
    return len(canon.last_verse.get(book_osis, ()))


def last_verse_of_chapter(
    book_osis: str,
    chapter: int,
    canon: Canon = DEFAULT_CANON,
) -> int:
    """Return the last verse number of a given chapter in a book.

    Returns 0 if the chapter is out of range.
    """
    verses = canon.last_verse.get(book_osis, ())
    if 1 <= chapter <= len(verses):
        return verses[chapter - 1]
    return 0


def resolve_book(name: str) -> Book | None:
    """Resolve a book name (or abbreviation) to a Book instance.

    Returns None if the name cannot be resolved.

    Fallback: if the internal resolver fails and the optional biblelib
    package is installed, biblelib's multi-scheme findbook is tried as
    a secondary resolution backend (BCVID, USFM, OSIS, Biblia, and
    full-name lookups across supported languages).
    """
    key = name.strip().lower().replace(" ", "")
    osis = NAME_TO_OSIS.get(key)
    # Slash-separated names: try each alternative (e.g. "Eccl/Qoh")
    if osis is None and "/" in key:
        for part in key.split("/"):
            part = part.strip()
            if not part:
                continue
            candidate = NAME_TO_OSIS.get(part)
            if candidate:
                osis = candidate
                break

    if osis:
        return BOOKS_BY_OSIS.get(osis)

    # Optional biblelib fallback (lazy import — no hard dependency)
    try:
        from accordance_parser.biblelib_bridge import (  # type: ignore[import-untyped]
            BibleLibBridge,
            biblelib_available,
        )

        if biblelib_available():
            bridge = BibleLibBridge()
            osis = bridge.resolve_book(name)
            if osis:
                return BOOKS_BY_OSIS.get(osis)
    except ImportError:
        pass

    return None


def resolve_book_id(name: str) -> int | None:
    """Resolve a book name to its bolls.life ID."""
    book = resolve_book(name)
    return book.bolls_id if book else None


def osis_to_spaceless(osis: str) -> str:
    """Convert OSIS to commentary-module TitleCase spaceless name."""
    return OSIS_TO_SPACELESS.get(osis, osis)


def spaceless_to_osis(spaceless: str) -> str | None:
    """Convert commentary-module TitleCase spaceless name to OSIS."""
    return SPACELESS_TO_OSIS.get(spaceless)


def is_old_testament(osis: str) -> bool:
    """Return True if the book is in the Old Testament (including deuterocanon)."""
    book = BOOKS_BY_OSIS.get(osis)
    return book.testament == "ot" if book else False


def is_new_testament(osis: str) -> bool:
    """Return True if the book is in the New Testament."""
    book = BOOKS_BY_OSIS.get(osis)
    return book.testament == "nt" if book else False


# =============================================================================
# 8. Module exports
# =============================================================================

__all__ = [
    # Data structures
    "Book",
    "Canon",
    "ReferenceResult",
    # Book lookup
    "BOOKS_BY_OSIS",
    "NAME_TO_OSIS",
    "SPACELESS_TO_OSIS",
    "OSIS_TO_SPACELESS",
    # Canons
    "CANON_PROTESTANT",
    "CANON_CATHOLIC",
    "DEFAULT_CANON",
    # Psalm numbering
    "LXX_TO_MT_PSALM",
    "psalm_lxx_to_mt",
    "psalm_mt_to_lxx",
    # Functions
    "resolve_book",
    "resolve_book_id",
    "parse_reference",
    "osis_to_spaceless",
    "spaceless_to_osis",
    "canon_by_name",
    "chapter_count",
    "last_verse_of_chapter",
    "is_old_testament",
    "is_new_testament",
]
