"""Strategy pattern for verse offset resolution in TextModule.

Each strategy encapsulates one offset-finding approach.  A resolver chains
them by priority and returns the first non-None result.
"""

import struct
from abc import ABC, abstractmethod


# ── Shared utilities ──────────────────────────────────────────────────────


def interpolate_cumulative_anchors(target_cum, anchor_data):
    """Bracket-anchor binary search + linear interpolation.

    anchor_data: list of (cumulative_index, byte_offset) sorted by cumulative_index.
    Returns interpolated byte_offset for target_cum, or None if no anchors.
    """
    if not anchor_data:
        return None

    before = None
    after = None
    for a_cum, a_off in anchor_data:
        if a_cum <= target_cum:
            before = (a_cum, a_off)
        if a_cum >= target_cum and after is None:
            after = (a_cum, a_off)

    if before is None:
        before = anchor_data[0]
    if after is None:
        after = anchor_data[-1]

    if before[0] == after[0]:
        return after[1]

    frac = (target_cum - before[0]) / max(after[0] - before[0], 1)
    return int(before[1] + frac * (after[1] - before[1]))


def compute_cumulative_verse(module, book, chapter, verse):
    """Compute cumulative verse index across the canon for (book, chapter, verse).

    Returns the 0-based cumulative verse index, or None if out of bounds.
    """
    cum = 0
    for bk in module._books:
        if bk == book:
            break
        vd = module._verse_counts.get(bk)
        if vd:
            cum += sum(vd)
    else:
        return None

    vdata = module._verse_counts.get(book)
    if not vdata or chapter > len(vdata):
        return None
    if verse < 1 or verse > vdata[chapter - 1]:
        return None

    cum += sum(vdata[: chapter - 1]) + (verse - 1)
    return cum


def _interpolate_offset(module, book, chapter):
    """Interpolate the byte offset for a book+chapter.

    Order of preference:
    1. T1 word-count-based interpolation (most accurate, for ESVS)
    2. Loaded JSON anchors with cross-book interpolation
       (uses next book's ch1 as upper bound for within-book chapters)
    3. Hardcoded _CHAPTER_ANCHORS with cumulative chapter index
    """
    abbr = module.module_abbr or module.abbrev
    # Prefer T1-based interpolation when available (ESVS)
    if abbr == "ESVS":
        t1_result = _t1_interpolate_offset(module, book, chapter)
        if t1_result is not None and t1_result > 0:
            return t1_result

    # KJVS-specific hardcoded anchors (high-precision for key chapters)
    if abbr == "KJVS":
        module._build_cumulative_index()
        target_cum = module._compute_cum_chapter(book, chapter)
        if target_cum is not None:
            anchor_data = []
            for a_book, a_ch, a_offset in module._KJVS_CHAPTER_ANCHORS:
                if a_book == "__END__":
                    a_cum = 1183  # cumulative chapter just past Revelation 22
                else:
                    a_cum = module._compute_cum_chapter(a_book, a_ch)
                if a_cum is not None:
                    anchor_data.append((a_cum, a_offset))
            anchor_data.sort()

            result = interpolate_cumulative_anchors(target_cum, anchor_data)
            if result is not None:
                return result

    # Loaded JSON anchors: cumulative-chapter interpolation (non-ESVS)
    if (
        abbr != "ESVS"
        and hasattr(module, "_dynamic_anchors")
        and len(module._dynamic_anchors) >= 2
    ):
        module._build_cumulative_index()
        target_cum = module._compute_cum_chapter(book, chapter)
        if target_cum is not None:
            anchor_data = []
            for (a_book, a_ch), a_off in module._dynamic_anchors.items():
                a_cum = module._compute_cum_chapter(a_book, a_ch)
                if a_cum is not None:
                    anchor_data.append((a_cum, a_off))
            anchor_data.sort()

            result = interpolate_cumulative_anchors(target_cum, anchor_data)
            if result is not None:
                return result

    # Fallback: cumulative chapter index with hardcoded anchors
    module._build_cumulative_index()
    target_ch_cum = module._compute_cum_chapter(book, chapter)
    if target_ch_cum is None:
        return None

    anchor_data = []
    for a_book, a_ch, a_offset in module._CHAPTER_ANCHORS:
        a_cum = module._compute_cum_chapter(a_book, a_ch)
        if a_cum is not None:
            anchor_data.append((a_cum, a_offset))

    if not anchor_data:
        return None

    return interpolate_cumulative_anchors(target_ch_cum, anchor_data)


def _t1_interpolate_offset(module, book, chapter):
    """Interpolate byte offset using T1 cumulative word counts for precision.

    Maps hardcoded anchors to T1 word positions, then interpolates in
    word-space (which is proportional to bytes). This is ~10x more
    accurate than chapter-count interpolation for books with uneven
    chapter sizes (e.g., Psalms 119).
    """
    module._load_word_map()
    module._build_cumulative_index()

    if not module._t1:
        return None

    # Get T1 word position for this chapter's first verse
    t1_idx = module._find_t1_verse_index(book, chapter, 1)
    if t1_idx is None or t1_idx >= len(module._t1):
        return None
    t1_val = module._t1[t1_idx]

    # Map hardcoded anchors to (word_pos, byte_pos) pairs
    anchor_pairs = []
    for a_book, a_ch, a_off in module._CHAPTER_ANCHORS:
        a_t1_idx = module._find_t1_verse_index(a_book, a_ch, 1)
        if a_t1_idx is not None and a_t1_idx < len(module._t1):
            anchor_pairs.append((module._t1[a_t1_idx], a_off))

    if len(anchor_pairs) < 2:
        return None

    anchor_pairs.sort()

    # Find bracketing anchors in word space
    before_word = before_byte = None
    after_word = after_byte = None

    for aw, ab in anchor_pairs:
        if aw <= t1_val:
            before_word, before_byte = aw, ab
        if aw >= t1_val and after_word is None:
            after_word, after_byte = aw, ab

    if before_word is None or after_word is None:
        # Try falling back to global bounds
        before_word, before_byte = anchor_pairs[0]
        after_word, after_byte = anchor_pairs[-1]

    if before_word == after_word:
        return after_byte

    # Linear interpolation in word space
    frac = (t1_val - before_word) / max(after_word - before_word, 1)
    return int(before_byte + frac * (after_byte - before_byte))


# ── Strategy ABC ───────────────────────────────────────────────────────────


class VerseOffsetStrategy(ABC):
    """Abstract base for verse-offset strategies.

    Subclasses MUST define:
      - PRIORITY: int  (lower = tried first)
      - can_handle(module) -> bool
      - find_offset(module, book, chapter, verse) -> int | None
    """

    PRIORITY: int

    @abstractmethod
    def can_handle(self, module) -> bool:
        """Return True if this strategy can attempt offset resolution."""

    @abstractmethod
    def find_offset(self, module, book: str, chapter: int, verse: int) -> int | None:
        """Return byte offset for the verse, or None if not found."""


# ── Strategy classes ───────────────────────────────────────────────────────


class HardcodedAnchorStrategy(VerseOffsetStrategy):
    """Check per-verse hardcoded Greek anchors (highest priority).

    Always runs first — returns exact anchors for known verse positions.
    """

    PRIORITY = 10

    def can_handle(self, module) -> bool:
        return True

    def find_offset(self, module, book, chapter, verse):
        # ── Hardcoded per-verse Greek anchors (highest priority) ──
        abbr = module.module_abbr or module.abbrev
        greek_anchors = type(module)._GREEK_VERSE_ANCHORS.get(abbr, {})
        va_key = (book, chapter, verse)
        if va_key in greek_anchors:
            return greek_anchors[va_key]
        return None


class HebrewPageTableStrategy(VerseOffsetStrategy):
    """Find verse offset in Hebrew modules using the u16_BE page table."""

    PRIORITY = 20

    def can_handle(self, module) -> bool:
        abbr = module.module_abbr or module.abbrev
        return module._is_hebrew and abbr in type(module)._HEBREW_PT

    def find_offset(self, module, book, chapter, verse):
        abbr = module.module_abbr or module.abbrev
        he_pt = type(module)._HEBREW_PT.get(abbr)
        if he_pt is None:
            return None
        pt_offset, gen1_idx = he_pt
        # Compute cumulative verse index within the canon
        cum = 0
        for bk in module._books:
            if bk == book:
                break
            vd = module._verse_counts.get(bk)
            if vd:
                cum += sum(vd)
        else:
            return None
        vdata = module._verse_counts.get(book)
        if not vdata or chapter > len(vdata):
            return None
        if verse < 1 or verse > vdata[chapter - 1]:
            return None
        cum += sum(vdata[: chapter - 1]) + (verse - 1)
        # Proportional page estimation: ~17.5 verses per text page
        # (1326 pages / 23145 Hebrew verses). Avoids overflow beyond
        # the text page region that the old 1-verse-per-entry formula caused.
        idx = gen1_idx + int(cum / 17.5)
        eoff = pt_offset + idx * 4
        if eoff + 8 > len(module.mm):
            return None
        page = (module.mm[eoff] << 8) | module.mm[eoff + 1]
        result = page * 0x100
        # HMT-DEMO pages have a 00 00 XX XX sub-table prefix; skip it
        if abbr == "HMT-DEMO":
            skip = 0
            while (
                result + skip + 4 <= len(module.mm)
                and module.mm[result + skip] == 0
                and module.mm[result + skip + 1] == 0
            ):
                skip += 4
            result += skip
        # MT-ETCBC-A chapter-first pages have an index block (4-byte
        # records of form 00 XX YY ZZ) before the verse text. The block
        # ends with a duplicate record terminator. Skip past the index.
        if abbr == "MT-ETCBC-A":
            pos = result
            end = min(result + 0x800, len(module.mm))
            # Some pages have a 1-byte prefix before the 4-byte records
            if pos + 5 < end and module.mm[pos] != 0x00 and module.mm[pos + 1] == 0x00:
                pos += 1
            # Skip 4-byte records (00 XX YY ZZ) until terminator or end
            skipped = 0
            while pos + 4 <= end:
                if module.mm[pos] != 0x00:
                    break
                # Duplicate consecutive record = block terminator
                if (
                    pos + 8 <= end
                    and module.mm[pos : pos + 4] == module.mm[pos + 4 : pos + 8]
                ):
                    pos += 8
                    skipped += 8
                    break
                pos += 4
                skipped += 4
            # Only apply the skip if we found a substantial header
            # (>= 16 bytes). Otherwise the page has no header index.
            if skipped >= 16:
                result = pos
        # Scan forward for 0x0D verse delimiters to reach the target
        # verse within a multi-verse page.  Each page holds multiple
        # verses; the page table gives us a page-level starting point,
        # not a verse-level one.
        est_start = int((idx - gen1_idx) * 17.5)
        skip_verses = max(0, cum - est_start)
        if skip_verses > 0:
            pos = result
            limit = min(result + 0x3000, len(module.mm))
            for _ in range(int(skip_verses)):
                nl = module.mm.find(b"\x0d", pos, limit)
                if nl == -1:
                    break
                pos = nl + 1
            result = pos
        return result


class LXXOffsetStrategy(VerseOffsetStrategy):
    """Find verse byte offset in LXX1 using hardcoded book-start offsets.

    Uses byte-proportional interpolation within each book.
    """

    PRIORITY = 25

    def can_handle(self, module) -> bool:
        abbr = module.module_abbr or module.abbrev
        return abbr == "LXX1"

    def find_offset(self, module, book, chapter, verse):
        # Hardcoded first-verse byte offsets (empirically verified)
        _KNOWN = {
            "Genesis": 0xC2CD85,
            "Psalms": 0xEAB22C,
            "Proverbs": 0xEF4F99,
            "Ecclesiastes": 0xF0BBEF,
            "Isaiah": 0xF981CC,
        }

        if book not in module._books:
            return None

        vdata = module._verse_counts.get(book)
        if not vdata or chapter > len(vdata) or verse < 1 or verse > vdata[chapter - 1]:
            return None

        book_order = module._books
        book_idx = book_order.index(book)

        # Find the nearest known anchors (before and after this book)
        known_before = None
        known_after = None
        for bk, off in sorted(
            _KNOWN.items(),
            key=lambda x: book_order.index(x[0]) if x[0] in book_order else 999,
        ):
            if bk not in book_order:
                continue
            bi = book_order.index(bk)
            if bi <= book_idx:
                known_before = (bk, off, bi)
            if bi >= book_idx and known_after is None:
                known_after = (bk, off, bi)

        verse_in_book = sum(vdata[: chapter - 1]) + (verse - 1)

        # Use byte-proportional estimate within the book
        # based on known anchors' byte spans
        if known_before and known_after and known_before[0] != known_after[0]:
            # Compute total verses between the two anchors
            total_v_between = 0
            total_v_before_book = 0
            for bi in range(known_before[2], known_after[2] + 1):
                bn = book_order[bi]
                vc = module._verse_counts.get(bn)
                bv = sum(vc) if vc else 0
                total_v_between += bv
                if bn == book:
                    total_v_before_book = total_v_between - bv

            byte_span = known_after[1] - known_before[1]
            if total_v_between > 0:
                bytes_per_verse = byte_span / total_v_between
                return int(
                    known_before[1]
                    + (total_v_before_book + verse_in_book) * bytes_per_verse
                )

        # Fallback: use only the before-anchor, estimate forward
        if known_before and known_before[0] == book:
            # This book has a known first-verse offset
            bk_start = known_before[1]
            # Estimate bytes per verse based on typical LXX ratio (~130 bytes/verse)
            bytes_per_verse = 130
            return int(bk_start + verse_in_book * bytes_per_verse)

        # Last resort: bytes-per-verse estimate from known start
        if known_before:
            cum_v = 0
            for bi in range(known_before[2], book_idx):
                vc = module._verse_counts.get(book_order[bi])
                if vc:
                    cum_v += sum(vc)
            est = known_before[1] + min(int(cum_v * 130), 5000000)
            est = min(est, len(module.mm) - 200)
            return est

        return None


class GreekVTOffsetStrategy(VerseOffsetStrategy):
    """Lookup using chapter-anchor byte-linear interpolation, then VT and
    book-level fallbacks."""

    PRIORITY = 30

    def can_handle(self, module) -> bool:
        return module._is_greek

    def find_offset(self, module, book, chapter, verse):
        # ── 1. Chapter-anchor byte-linear interpolation (primary) ──
        ch_anchors = getattr(module, "_greek_chapter_anchors", None)
        if ch_anchors and (book, chapter) in ch_anchors:
            ch_start = ch_anchors[(book, chapter)]
            vdata = module._verse_counts.get(book, ())
            ch_v = vdata[chapter - 1] if chapter <= len(vdata) else 1

            # Chapter end: next chapter anchor, next book anchor, or EOF
            ch_end = ch_anchors.get((book, chapter + 1))
            if ch_end is None:
                books_in_order = [b for b in module._books if b in module._book_anchors]
                idx = books_in_order.index(book) if book in books_in_order else -1
                if idx >= 0 and idx + 1 < len(books_in_order):
                    ch_end = module._book_anchors[books_in_order[idx + 1]]
                elif (
                    hasattr(module, "_greek_para_markers")
                    and module._greek_para_markers
                ):
                    ch_end = module._greek_para_markers[-1] + 200
                else:
                    ch_end = len(module.mm) - 100

            if ch_v <= 1 or verse == 1:
                return ch_start
            # Byte-linear interpolation within chapter, backward-snapped
            frac = (verse - 1) / max(ch_v - 1, 1)
            frac = min(frac, 0.95)
            pos = ch_start + int(frac * (ch_end - ch_start))
            # Snap within this book's paragraph markers
            book_pm = getattr(module, "_greek_book_pm", {}).get(book)
            if book_pm:
                pos = module._snap_backward_in_list(pos, book_pm)
            return pos

        # ── 2. VT-derived offsets (fallback when no chapter anchors) ──
        vt = getattr(module, "_greek_vt", None)
        vt_ok = bool(vt and max(vt[: min(100, len(vt))]) < 500)
        offsets = getattr(module, "_greek_verse_offsets", None)
        if vt_ok and offsets:
            result = offsets.get((book, chapter, verse))
            if result is not None:
                return result

        # ── 3. Book-level fallback (GNT-TRS style) ──
        if hasattr(module, "_book_anchors") and book in module._book_anchors:
            start = module._book_anchors[book]
            books_in_order = [b for b in module._books if b in module._book_anchors]
            idx = books_in_order.index(book)
            if idx + 1 < len(books_in_order):
                end = module._book_anchors[books_in_order[idx + 1]]
            elif hasattr(module, "_greek_para_markers") and module._greek_para_markers:
                end = module._greek_para_markers[-1] + 200
            else:
                end = len(module.mm) - 100
            total_v = sum(module._verse_counts.get(book, [0]))
            cum_before = sum(module._verse_counts.get(book, [0])[: chapter - 1])
            pos = start + int((cum_before + verse - 1) / total_v * (end - start))
            if hasattr(module, "_greek_para_markers") and module._greek_para_markers:
                pos = module._snap_to_nearest_greek_marker(pos, book, chapter)
            return pos
        return None


class T1InterpolationStrategy(VerseOffsetStrategy):
    """ESVS fast path: T1 word-based interpolation."""

    PRIORITY = 40

    def can_handle(self, module) -> bool:
        abbr = module.module_abbr or module.abbrev
        return abbr == "ESVS"

    def find_offset(self, module, book, chapter, verse):
        # ── Hardcoded per-verse anchors (ESVS-specific) ──
        for a_book, a_ch, a_v, a_off in module._ESVS_VERSE_ANCHORS:
            if a_book == book and a_ch == chapter and a_v == verse:
                return a_off

        # Check for hardcoded anchor first
        has_hardcoded = False
        for a_book, a_ch, a_off in module._CHAPTER_ANCHORS:
            if a_book == book and a_ch == chapter:
                ch_offset = a_off
                has_hardcoded = True
                break
        if not has_hardcoded:
            ch_offset = _t1_interpolate_offset(module, book, chapter)

        if ch_offset is not None and ch_offset > 0x100000:
            # Refine chapter anchor unless hardcoded and very close match
            refined = module._refine_chapter_anchor(ch_offset)
            if has_hardcoded and abs(refined - ch_offset) > 50:
                pass  # keep hardcoded value, reject large refinement
            else:
                ch_offset = refined
            if verse == 1:
                return ch_offset

            # Try T1-based verse positioning first
            result = module._estimate_verse_via_t1(book, chapter, verse, ch_offset)
            if result is not None:
                refined_v = module._refine_verse_position(
                    result, target_verse=verse, chapter_anchor=ch_offset
                )
                # Only accept refinement if it's close to the T1 estimate.
                if refined_v is not None and abs(refined_v - result) <= 80:
                    return refined_v
                # Only trust T1 if the result is within a reasonable range
                vdata = module._verse_counts.get(book)
                if vdata and chapter <= len(vdata):
                    ch_vcount = vdata[chapter - 1]
                    # Sanity: verse estimate should be between chapter start
                    # and roughly chapter_end (max 20KB per chapter)
                    if result > ch_offset and result < ch_offset + 20000:
                        return result

            # Fallback: simple verse fraction within chapter
            vdata = module._verse_counts.get(book)
            if vdata and chapter <= len(vdata):
                ch_vcount = vdata[chapter - 1]
                # Estimate chapter size: ~130 bytes per verse (average across Bible)
                ch_byte_est = min(ch_vcount * 130, 8000)
                if verse == ch_vcount:
                    # Last verse: search forward for the next 0xA6 marker.
                    result = ch_offset + ch_byte_est
                    # Clamp
                    result = min(result, len(module.mm) - 100)
                    result = max(result, ch_offset)
                    # Forward-only 0xA6 search: find the next verse boundary
                    search_limit = min(result + 600, len(module.mm))
                    for sc_pos in range(result, search_limit):
                        if module.mm[sc_pos] == 0xA6:
                            text_start = module._find_text_after_marker(sc_pos + 1)
                            if text_start is not None:
                                return text_start
                    # Fallback: try standard refinement
                    refined_v = module._refine_verse_position(result)
                    if refined_v is not None:
                        return refined_v
                    return result
                else:
                    verse_frac = (verse - 1) / max(ch_vcount - 1, 1)
                    verse_frac = min(verse_frac, 0.95)
                    result = ch_offset + int(verse_frac * ch_byte_est)
                # Clamp to end of file / text area
                result = min(result, len(module.mm) - 100)
                result = max(result, ch_offset)
                # Try to refine to nearest verse/sentence boundary
                refined_v = module._refine_verse_position(result)
                if refined_v is not None:
                    return refined_v
                return result
            return ch_offset

        # Fallback: chapter-cumulative interpolation
        fallback = _interpolate_offset(module, book, chapter)
        if fallback:
            ch_off = module._refine_chapter_anchor(fallback)
            vdata = module._verse_counts.get(book)
            if vdata and chapter <= len(vdata):
                ch_vcount = vdata[chapter - 1]
                ch_byte_est = min(ch_vcount * 130, 8000)
                if verse == ch_vcount:
                    return ch_off + ch_byte_est
                verse_frac = (verse - 1) / max(ch_vcount - 1, 1)
                verse_frac = min(verse_frac, 0.95)
                return ch_off + int(verse_frac * 1200)
            return ch_off
        return None


class KJVSInterpolationStrategy(VerseOffsetStrategy):
    """KJVS: cumulative chapter anchors + T1-based verse positioning."""

    PRIORITY = 50

    def can_handle(self, module) -> bool:
        abbr = module.module_abbr or module.abbrev
        return abbr == "KJVS"

    def find_offset(self, module, book, chapter, verse):
        # Check exact anchor first
        ref_key = f"{book} {chapter}:{verse}"
        if ref_key in module._anchors_map:
            return module._anchors_map[ref_key]

        anchor_offset = None

        if hasattr(module, "_book_first_offset"):
            anchor_offset = _interpolate_offset(module, book, chapter)

        if anchor_offset is not None:
            # Refine the chapter anchor to the nearest 0xA6 boundary
            text_start = module._refine_chapter_anchor(anchor_offset)
            if verse == 1:
                return text_start

            # ── Check for exact verse-level anchor (most accurate) ──
            ref_key = f"{book} {chapter}:{verse}"
            if ref_key in module._anchors_map:
                return module._anchors_map[ref_key]

            vdata = module._verse_counts.get(book)
            if vdata and chapter <= len(vdata):
                ch_vcount = vdata[chapter - 1]

                # ── KJVS per-verse hardcoded anchors (corrects T1 drift) ──
                for a_book, a_ch, a_v, a_off in module._KJVS_VERSE_ANCHORS:
                    if a_book == book and a_ch == chapter and a_v == verse:
                        return a_off

                # ── T1-based verse positioning (KJVS, where T1 covers this verse) ──
                result = module._estimate_verse_via_t1(book, chapter, verse, text_start)
                if result is not None and result > text_start:
                    refined_v = module._refine_verse_position(
                        result, target_verse=verse, chapter_anchor=text_start
                    )
                    if refined_v is not None:
                        return refined_v
                    if result < text_start + 20000:
                        return result

                # ── Fraction-based positioning with boundary refinement ──
                ch_byte_est = min(ch_vcount * 130, 15000)
                if verse == ch_vcount:
                    # Last verse: estimate near end of chapter.
                    # For KJVS, compute actual chapter size from the next anchor
                    # in _KJVS_CHAPTER_ANCHORS and use a fraction of it.
                    next_off = None
                    for a_book, a_ch, a_off in module._KJVS_CHAPTER_ANCHORS:
                        if a_off > text_start + 10:
                            next_off = a_off
                            break
                    if next_off is not None and next_off > text_start:
                        result = text_start + int((next_off - text_start) * 0.95)
                    else:
                        result = text_start + int(ch_byte_est * 0.92)
                    result = min(result, len(module.mm) - 100)
                    result = max(result, text_start)
                    # Fallback: refine to nearest sentence/verse boundary
                    refined_v = module._refine_verse_position(result)
                    if refined_v is not None:
                        return refined_v
                    return result
                else:
                    verse_frac = (verse - 1) / max(ch_vcount - 1, 1)
                    verse_frac = min(verse_frac, 0.95)
                    result = text_start + int(verse_frac * ch_byte_est)
                    result = min(result, len(module.mm) - 100)
                    result = max(result, text_start)
                    # Refine to nearest verse/sentence boundary
                    refined_v = module._refine_verse_position(result)
                    if refined_v is not None:
                        return refined_v
                    return result
            return text_start

        return None


class StandardInterpolationStrategy(VerseOffsetStrategy):
    """Generic anchor interpolation for modules with loaded anchors."""

    PRIORITY = 60

    def can_handle(self, module) -> bool:
        return hasattr(module, "_book_first_offset") or hasattr(
            module, "_dynamic_anchors"
        )

    def find_offset(self, module, book, chapter, verse):
        # Check exact anchor first
        ref_key = f"{book} {chapter}:{verse}"
        if ref_key in module._anchors_map:
            return module._anchors_map[ref_key]

        anchor_offset = None

        if hasattr(module, "_book_first_offset"):
            anchor_offset = _interpolate_offset(module, book, chapter)

        if anchor_offset is not None:
            # Refine the chapter anchor to the nearest 0xA6 boundary
            text_start = module._refine_chapter_anchor(anchor_offset)
            if verse == 1:
                return text_start

            # ── Check for exact verse-level anchor (most accurate) ──
            ref_key = f"{book} {chapter}:{verse}"
            if ref_key in module._anchors_map:
                return module._anchors_map[ref_key]

            vdata = module._verse_counts.get(book)
            if vdata and chapter <= len(vdata):
                ch_vcount = vdata[chapter - 1]

                # ── Fraction-based positioning with boundary refinement ──
                ch_byte_est = min(ch_vcount * 130, 15000)
                if verse == ch_vcount:
                    # Last verse: estimate near end of chapter.
                    result = text_start + int(ch_byte_est * 0.92)
                    result = min(result, len(module.mm) - 100)
                    result = max(result, text_start)
                    # Fallback: refine to nearest sentence/verse boundary
                    refined_v = module._refine_verse_position(result)
                    if refined_v is not None:
                        return refined_v
                    return result
                else:
                    verse_frac = (verse - 1) / max(ch_vcount - 1, 1)
                    verse_frac = min(verse_frac, 0.95)
                    result = text_start + int(verse_frac * ch_byte_est)
                    result = min(result, len(module.mm) - 100)
                    result = max(result, text_start)
                    # Refine to nearest verse/sentence boundary
                    refined_v = module._refine_verse_position(result)
                    if refined_v is not None:
                        return refined_v
                    return result
            return text_start

        return None


class PageTableFallbackStrategy(VerseOffsetStrategy):
    """Auto-detected page table lookup for modules without explicit anchors."""

    PRIORITY = 70

    def can_handle(self, module) -> bool:
        return module._page_table_offset is not None

    def find_offset(self, module, book, chapter, verse):
        # ── Page-table-based lookup for auto-detected modules ──
        result = _find_verse_via_page_table(module, book, chapter, verse)
        if result is not None:
            return result

        approx = _interpolate_offset(module, book, chapter)
        if approx is None:
            return None

        page_base = module._find_page_base_via_table(approx)
        if page_base is not None and page_base > 0x100000:
            text_start = page_base
            for i in range(0x300):
                off = page_base + i
                if off + 1 >= len(module.mm):
                    break
                b = module.mm[off]
                if 0x41 <= b <= 0x5A:
                    if i > 32:
                        text_start = off
                        break
            vdata = module._verse_counts.get(book)
            if vdata and 1 <= chapter <= len(vdata):
                ch_vcount = vdata[chapter - 1]
                verse_frac = (verse - 1) / max(ch_vcount - 1, 1)
                if verse < ch_vcount:
                    verse_frac = min(verse_frac, 0.95)
                result = text_start + int(verse_frac * 1200)
                return module._clamp_to_book_end(result, book)
            return text_start

        vdata = module._verse_counts.get(book)
        if vdata and 1 <= chapter <= len(vdata):
            ch_vcount = vdata[chapter - 1]
            verse_frac = (verse - 1) / max(ch_vcount - 1, 1)
            return approx + int(verse_frac * 4000)

        return approx


# ── Page-table helper (module-level) ───────────────────────────────────────


def _find_verse_via_page_table(module, book, chapter, verse):
    """Find verse offset using auto-detected page table with proportional mapping.

    Maps the target verse's cumulative position in the canon to the
    corresponding page table entry, retrieves the page base byte offset,
    then scans forward for the correct verse boundary.
    """
    if (
        not hasattr(module, "_gen1_pt_index")
        or module._gen1_pt_index is None
        or module._pt_entry_count <= 0
    ):
        return None

    pt = module._page_table_offset
    is_u16 = module._page_table_format == "u16"
    gen1_idx = module._gen1_pt_index

    # Compute cumulative verse index
    module._build_cumulative_index()
    cum = 0
    for bk in module._books:
        if bk == book:
            break
        vd = module._verse_counts.get(bk)
        if vd:
            cum += sum(vd)
    else:
        return None

    vdata = module._verse_counts.get(book)
    if not vdata or chapter > len(vdata):
        return None
    if verse < 1 or verse > vdata[chapter - 1]:
        return None

    cum += sum(vdata[: chapter - 1]) + (verse - 1)

    # ── Piecewise-proportional mapping: cumulative verse -> page table entry ──
    total_verses = module._total_verses
    has_john316 = (
        hasattr(module, "_john316_pt_index")
        and module._john316_pt_index is not None
        and hasattr(module, "_john316_cum")
        and module._john316_cum > 0
    )
    last_entry = gen1_idx + module._pt_entry_count - 1

    if has_john316 and cum <= module._john316_cum:
        # Between Gen 1:1 and John 3:16: linear interpolation
        frac = cum / max(module._john316_cum, 1)
        est_entry = gen1_idx + int(frac * (module._john316_pt_index - gen1_idx))
    elif has_john316:
        # Between John 3:16 and end of text: linear interpolation
        remaining_verses = total_verses - module._john316_cum
        remaining_entries = last_entry - module._john316_pt_index
        if remaining_verses > 0 and remaining_entries > 0:
            frac = (cum - module._john316_cum) / max(remaining_verses, 1)
            est_entry = module._john316_pt_index + int(frac * remaining_entries)
        else:
            est_entry = last_entry
    else:
        # Single-point mapping (Gen 1:1 -> end) — used when John 3:16
        # text could not be found (non-English modules, etc.)
        frac = cum / max(total_verses, 1)
        est_entry = gen1_idx + int(frac * module._pt_entry_count)

    est_entry = max(gen1_idx, min(last_entry, est_entry))

    # Get page base from estimated entry
    eoff = pt + est_entry * 4
    if eoff + 4 > len(module.mm):
        return None

    if is_u16:
        u16_val = struct.unpack_from(">H", module.mm, eoff)[0]
        page_base = u16_val * 0x100
    else:
        page_base = module._read_u24_be_after_tag(eoff)

    # If the estimated entry points before the text area, walk forward
    if page_base < 0x100000:
        for idx in range(est_entry + 1, gen1_idx + module._pt_entry_count + 1):
            eoff2 = pt + idx * 4
            if eoff2 + 4 > len(module.mm):
                break
            if is_u16:
                u16_val = struct.unpack_from(">H", module.mm, eoff2)[0]
                pb = u16_val * 0x100
            else:
                pb = module._read_u24_be_after_tag(eoff2)
            if pb > 0x100000:
                page_base = pb
                break
        else:
            return None

    if page_base < 0x100000:
        return None

    # Snap to nearest paragraph/section boundary (0xA6 marker)
    refined = module._refine_chapter_anchor(page_base)

    if verse == 1:
        return refined

    # Fractional verse positioning from the refined anchor
    ch_vcount = vdata[chapter - 1]
    ch_byte_est = min(ch_vcount * 130, 15000)
    verse_frac = (verse - 1) / max(ch_vcount - 1, 1)
    verse_frac = min(verse_frac, 0.95)
    est = refined + int(verse_frac * ch_byte_est)
    est = min(est, len(module.mm) - 100)
    est = max(est, refined)

    result = module._refine_verse_position(est)
    if result is not None:
        return result
    return est


# ── Resolver ────────────────────────────────────────────────────────────────


class VerseOffsetResolver:
    """Chain strategies by priority and return the first non-None result."""

    def __init__(self):
        self._strategies = [
            HardcodedAnchorStrategy(),
            HebrewPageTableStrategy(),
            LXXOffsetStrategy(),
            GreekVTOffsetStrategy(),
            T1InterpolationStrategy(),
            KJVSInterpolationStrategy(),
            StandardInterpolationStrategy(),
            PageTableFallbackStrategy(),
        ]
        self._strategies.sort(key=lambda s: s.PRIORITY)

    def resolve(self, module, book: str, chapter: int, verse: int) -> int | None:
        """Find the byte offset for (book, chapter, verse) in the module.

        Returns the byte offset, or None if no strategy could determine it.
        """
        for strategy in self._strategies:
            if strategy.can_handle(module):
                result = strategy.find_offset(module, book, chapter, verse)
                if result is not None:
                    return result
        return None
