"""
Corpus registry for non-Bible ancient text reference systems.

Maps Accordance module abbreviations to their parent corpus and provides
work-abbreviation recognition, reference formatting, and reference parsing
for the Apostolic Fathers, Philo, Q source, and (future) Josephus, Qumran,
Pseudepigrapha corpora.

Design doc: research/non-bible-reference-systems.md
"""

from __future__ import annotations

import re
from dataclasses import dataclass


def _safe_group(match, name: str) -> str | None:
    """Return named group or None if the group doesn't exist in the pattern."""
    try:
        return match.group(name)
    except IndexError:
        return None


# =============================================================================
# 1. Corpus + work data structures
# =============================================================================


@dataclass(frozen=True)
class CorpusWork:
    """A single work / treatise within a non-Bible corpus."""

    abbrev: str  # e.g. "IgnEph", "Leg", "Q"
    name: str  # e.g. "Ignatius, To the Ephesians"
    has_book: bool  # True if multi-book (e.g. Philo's Leg. 1-3)
    levels: int  # how many numeric levels (1=section, 2=ch.vs, 3=book.ch.vs)


@dataclass(frozen=True)
class CorpusDef:
    """Definition of a non-Bible text corpus."""

    corpus_id: str  # e.g. "apostolic_fathers"
    name: str  # human-readable
    prefix: str  # canonical ref prefix, e.g. "af", "philo", "q"
    works: dict[str, CorpusWork]  # work_abbrev → CorpusWork
    # Regex pattern that matches a work abbreviation followed by numeric ref
    # with named groups: work, ch, vs, sub
    extract_pattern: re.Pattern


# =============================================================================
# 2. Apostolic Fathers corpus
# =============================================================================

_AF_WORKS: dict[str, CorpusWork] = {
    # Standard SBL abbreviations (without periods, for programmatic matching)
    "1Clem": CorpusWork("1Clem", "1 Clement", False, 2),
    "2Clem": CorpusWork("2Clem", "2 Clement", False, 2),
    "IgnEph": CorpusWork("IgnEph", "Ignatius, To the Ephesians", False, 2),
    "IgnMagn": CorpusWork("IgnMagn", "Ignatius, To the Magnesians", False, 2),
    "IgnTr": CorpusWork("IgnTr", "Ignatius, To the Trallians", False, 2),
    "IgnRom": CorpusWork("IgnRom", "Ignatius, To the Romans", False, 2),
    "IgnPhld": CorpusWork("IgnPhld", "Ignatius, To the Philadelphians", False, 2),
    "IgnSm": CorpusWork("IgnSm", "Ignatius, To the Smyrnaeans", False, 2),
    "IgnPol": CorpusWork("IgnPol", "Ignatius, To Polycarp", False, 2),
    "PolPhil": CorpusWork("PolPhil", "Polycarp, To the Philippians", False, 2),
    "MartPol": CorpusWork("MartPol", "Martyrdom of Polycarp", False, 2),
    "Did": CorpusWork("Did", "Didache", False, 2),
    "Barn": CorpusWork("Barn", "Epistle of Barnabas", False, 2),
    "HermVis": CorpusWork("HermVis", "Shepherd of Hermas, Visions", False, 3),
    "HermMan": CorpusWork("HermMan", "Shepherd of Hermas, Mandates", False, 3),
    "HermSim": CorpusWork("HermSim", "Shepherd of Hermas, Similitudes", False, 3),
    # Generic Herm reference (for Hermeneia commentary index where the parser
    # doesn't differentiate sub-works; always sorted after specific abbreviations)
    "Herm": CorpusWork("Herm", "Shepherd of Hermas", False, 2),
    "Diogn": CorpusWork("Diogn", "Epistle to Diognetus", False, 2),
    "Papias": CorpusWork("Papias", "Fragments of Papias", False, 1),
    "Quad": CorpusWork("Quad", "Quadratus (fragment)", False, 1),
    # Accordance internal / alternative abbreviations
    "1Cl": CorpusWork("1Cl", "1 Clement (alt)", False, 2),
    "2Cl": CorpusWork("2Cl", "2 Clement (alt)", False, 2),
    "IgnE": CorpusWork("IgnE", "Ignatius, Ephesians (alt)", False, 2),
    "IgnM": CorpusWork("IgnM", "Ignatius, Magnesians (alt)", False, 2),
    "IgnT": CorpusWork("IgnT", "Ignatius, Trallians (alt)", False, 2),
    "IgnR": CorpusWork("IgnR", "Ignatius, Romans (alt)", False, 2),
    "IgnP": CorpusWork("IgnP", "Ignatius, Philadelphians (alt)", False, 2),
    "IgnS": CorpusWork("IgnS", "Ignatius, Smyrnaeans (alt)", False, 2),
    "IgnPo": CorpusWork("IgnPo", "Ignatius, Polycarp (alt)", False, 2),
    "PolP": CorpusWork("PolP", "Polycarp, Philippians (alt)", False, 2),
    "MartP": CorpusWork("MartP", "Martyrdom of Polycarp (alt)", False, 2),
    "EpBarn": CorpusWork("EpBarn", "Epistle of Barnabas (alt)", False, 2),
    "EpDiog": CorpusWork("EpDiog", "Epistle to Diognetus (alt)", True, 2),
}

# Build regex alternation from all work abbreviations.
# Order longer abbreviations first so "HermVis" matches before "Herm".
_AF_WORK_NAMES = sorted(_AF_WORKS.keys(), key=len, reverse=True)
_AF_WORK_ALT = "|".join(re.escape(w) for w in _AF_WORK_NAMES)

# Pattern: WorkAbbrev.Ch.Verse[.SubVerse]
# e.g. "IgnEph.1.1", "1Clem.5.1", "HermVis.2.1.1", "Did.8.1"
_AF_EXTRACT = re.compile(
    rf"(?P<work>{_AF_WORK_ALT})"
    rf"\.(?P<ch>\d+)"
    rf"\.(?P<vs>\d+)"
    rf"(?:\.(?P<sub>\d+))?"
)

# Pattern for extracting from "§42" style citations or bare work refs in text
_AF_BARE_WORK = re.compile(rf"\b(?P<work>{_AF_WORK_ALT})\b")

APOSTOLIC_FATHERS = CorpusDef(
    corpus_id="apostolic_fathers",
    name="Apostolic Fathers",
    prefix="af",
    works=_AF_WORKS,
    extract_pattern=_AF_EXTRACT,
)


# =============================================================================
# 3. Philo corpus
# =============================================================================

_PHILO_WORKS: dict[str, CorpusWork] = {
    "Opif": CorpusWork("Opif", "On the Creation", False, 1),
    "Leg": CorpusWork("Leg", "Allegorical Interpretation", True, 2),
    "Cher": CorpusWork("Cher", "On the Cherubim", False, 1),
    "Sacr": CorpusWork("Sacr", "On the Sacrifices of Abel and Cain", False, 1),
    "Det": CorpusWork("Det", "That the Worse Attacks the Better", False, 1),
    "Post": CorpusWork("Post", "On the Posterity of Cain", False, 1),
    "Gig": CorpusWork("Gig", "On the Giants", False, 1),
    "Deus": CorpusWork("Deus", "On the Unchangeableness of God", False, 1),
    "Agr": CorpusWork("Agr", "On Husbandry", False, 1),
    "Plant": CorpusWork("Plant", "On Noah's Work as a Planter", False, 1),
    "Ebr": CorpusWork("Ebr", "On Drunkenness", False, 1),
    "Sobr": CorpusWork("Sobr", "On Sobriety", False, 1),
    "Conf": CorpusWork("Conf", "On the Confusion of Tongues", False, 1),
    "Migr": CorpusWork("Migr", "On the Migration of Abraham", False, 1),
    "Her": CorpusWork("Her", "Who Is the Heir?", False, 1),
    "Congr": CorpusWork("Congr", "On Mating with Preliminary Studies", False, 1),
    "Fug": CorpusWork("Fug", "On Flight and Finding", False, 1),
    "Mut": CorpusWork("Mut", "On the Change of Names", False, 1),
    "Mos": CorpusWork("Mos", "On the Life of Moses", True, 2),
    "Decal": CorpusWork("Decal", "On the Decalogue", False, 1),
    "Spec": CorpusWork("Spec", "On the Special Laws", True, 2),
    "Virt": CorpusWork("Virt", "On the Virtues", False, 1),
    "Praem": CorpusWork("Praem", "On Rewards and Punishments", False, 1),
    "Prob": CorpusWork("Prob", "Every Good Man Is Free", False, 1),
    "Contempl": CorpusWork("Contempl", "On the Contemplative Life", False, 1),
    "Aet": CorpusWork("Aet", "On the Eternity of the World", False, 1),
    "Flacc": CorpusWork("Flacc", "Flaccus", False, 1),
    "Legat": CorpusWork("Legat", "Embassy to Gaius", False, 1),
    "Hypoth": CorpusWork("Hypoth", "Hypothetica", False, 1),
    "Prov": CorpusWork("Prov", "On Providence", True, 2),
    "QG": CorpusWork("QG", "Questions and Answers on Genesis", True, 2),
    "QE": CorpusWork("QE", "Questions and Answers on Exodus", True, 2),
}

_PHILO_WORK_NAMES = sorted(_PHILO_WORKS.keys(), key=len, reverse=True)
_PHILO_WORK_ALT = "|".join(re.escape(w) for w in _PHILO_WORK_NAMES)

# Pattern: WorkAbbrev[.Book].Section[.Subsection]
# e.g. "Opif.1", "Leg.1.245", "Spec.1.1", "Mos.2.1", "QG.1.1"
_PHILO_EXTRACT = re.compile(
    rf"(?P<work>{_PHILO_WORK_ALT})"
    rf"\.(?P<ch>\d+)"
    rf"(?:\.(?P<vs>\d+))?"
    rf"(?:\.(?P<sub>\d+))?"
)

PHILO = CorpusDef(
    corpus_id="philo",
    name="Philo of Alexandria",
    prefix="philo",
    works=_PHILO_WORKS,
    extract_pattern=_PHILO_EXTRACT,
)


# =============================================================================
# 4. Q source corpus
# =============================================================================

_Q_WORKS: dict[str, CorpusWork] = {
    "Q": CorpusWork("Q", "Q Source (Synoptic Sayings Source)", False, 2),
}

_Q_EXTRACT = re.compile(
    r"(?P<work>Q)"
    r"\s*(?P<ch>\d+)"
    r"[:.]\s*(?P<vs>\d+)"
    r"(?:-(?P<vs_end>\d+))?"
)

# Also match dot-notation: Q.3.5
_Q_EXTRACT_DOT = re.compile(
    r"(?P<work>Q)"
    r"\.(?P<ch>\d+)"
    r"\.(?P<vs>\d+)"
)

Q_SOURCE = CorpusDef(
    corpus_id="q_source",
    name="Q Source (Synoptic Sayings)",
    prefix="q",
    works=_Q_WORKS,
    extract_pattern=_Q_EXTRACT,
)


# =============================================================================
# 5. Registry + module-to-corpus mapping
# =============================================================================

CORPUS_REGISTRY: dict[str, CorpusDef] = {
    "apostolic_fathers": APOSTOLIC_FATHERS,
    "philo": PHILO,
    "q_source": Q_SOURCE,
}

# Module abbreviation (full, as found in Accordance) → corpus_id.
# Case-insensitive matching via detect_corpus().
MODULE_TO_CORPUS: dict[str, str] = {
    # Apostolic Fathers (atool companions)
    "AF Reader": "apostolic_fathers",
    "AF Crossrefs": "apostolic_fathers",
    "AF-E Notes": "apostolic_fathers",
    "AF-T Notes": "apostolic_fathers",
    "APOL-T Notes": "apostolic_fathers",
    "Hermeneia AF": "apostolic_fathers",
    # Apostolic Fathers (atext primary texts)
    "AF-E": "apostolic_fathers",
    "AF-L": "apostolic_fathers",
    "AF-T": "apostolic_fathers",
    "AFL-E": "apostolic_fathers",
    "AFL-T": "apostolic_fathers",
    # Apologists (atext primary texts)
    "APOL-E": "apologists",
    "APOL-T": "apologists",
    # Origen (atext primary texts)
    "ORIGEN-E": "origen",
    "ORIGEN-T": "origen",
    # Philo
    "Philo Crossrefs": "philo",
    # Q source
    "Hermeneia Q": "q_source",
}


# =============================================================================
# 6. Public API
# =============================================================================


def detect_corpus(abbrev: str) -> str | None:
    """Return the corpus_id for a module abbreviation, or None.

    Matching is case-insensitive.  Returns the first corpus whose name
    appears as a substring of the lowercased abbreviation.
    """
    if not abbrev:
        return None
    # Direct lookup first (case-insensitive)
    abbr_lower = abbrev.strip().lower()
    for mod_abbr, cid in MODULE_TO_CORPUS.items():
        if mod_abbr.lower() == abbr_lower:
            return cid
    # Substring match as fallback
    for mod_abbr, cid in MODULE_TO_CORPUS.items():
        if mod_abbr.lower() in abbr_lower:
            return cid
    return None


def extract_work_ref(
    corpus_id: str,
    text: str,
) -> tuple[str, int, int, int | None] | None:
    """Try to extract a (work_abbrev, chapter, verse, sub_verse) from *text*.

    Returns None if no known work reference is found in the text.
    Handles corpus-specific reference patterns.
    """
    corpus = CORPUS_REGISTRY.get(corpus_id)
    if corpus is None:
        return None

    if not text or not text.strip():
        return None

    # Try the primary extract pattern for this corpus
    m = corpus.extract_pattern.search(text)
    if m:
        work = m.group("work")
        if work not in corpus.works:
            return None
        ch = int(m.group("ch"))
        vs_str = m.group("vs")
        vs = int(vs_str) if vs_str else 0
        sub_str = m.group("sub") if "sub" in m.groupdict() else None
        sub = int(sub_str) if sub_str else None
        return (work, ch, vs, sub)

    # Q source: also try dot-notation pattern
    if corpus_id == "q_source":
        m2 = _Q_EXTRACT_DOT.search(text)
        if m2:
            work = m2.group("work")
            ch = int(m2.group("ch"))
            vs = int(m2.group("vs"))
            return (work, ch, vs, None)

    return None


def format_ref(
    corpus_id: str,
    work: str,
    ch: int,
    vs: int,
    sub: int | None = None,
) -> str:
    """Format a corpus-aware reference string.

    Returns a string like:
      - apostolic_fathers: "IgnEph.1.1", "HermVis.2.1.1"
      - philo: "Leg.1.245", "Opif.89"
      - q_source: "Q 3:5"
    """
    corpus = CORPUS_REGISTRY.get(corpus_id)
    if corpus is None:
        # Fallback: generic dot notation
        base = f"{work}.{ch}.{vs}"
        if sub:
            base += f".{sub}"
        return base

    cw = corpus.works.get(work)
    if corpus_id == "q_source":
        return f"Q {ch}:{vs}"
    elif corpus_id == "philo":
        # For multi-book works, the "chapter" is actually the book number
        if cw and cw.has_book:
            result = f"{work}.{ch}.{vs}"
            if sub:
                result += f".{sub}"
            return result
        else:
            # Single-book works: section only
            result = f"{work}.{ch}"
            if vs:
                result += f".{vs}"
            return result
    else:
        # Apostolic Fathers and others: work.ch.vs[.sub]
        result = f"{work}.{ch}.{vs}"
        if sub:
            result += f".{sub}"
        return result


def format_citation(
    corpus_id: str,
    work: str,
    ch: int,
    vs: int,
    sub: int | None = None,
) -> str:
    """Format a human-readable citation for the reference.

    Includes the work's full name for readability.
    e.g. "Ignatius, To the Ephesians 1.1"
    """
    corpus = CORPUS_REGISTRY.get(corpus_id)
    work_obj = corpus.works.get(work) if corpus else None
    work_name = work_obj.name if work_obj else work
    ref = format_ref(corpus_id, work, ch, vs, sub)
    return f"{work_name} {ref}"


def parse_ref(corpus_id: str, ref_str: str) -> dict | None:
    """Parse a corpus-aware reference string into structured dict.

    Returns dict with keys: work, chapter, verse, sub (or None on failure).

    Example inputs:
      - "IgnEph.1.1" → {"work": "IgnEph", "chapter": 1, "verse": 1, "sub": None}
      - "Leg.1.245"  → {"work": "Leg", "chapter": 1, "verse": 245, "sub": None}
      - "Q 3:5"      → {"work": "Q", "chapter": 3, "verse": 5, "sub": None}
    """
    ref_str = ref_str.strip()
    if not ref_str:
        return None

    corpus = CORPUS_REGISTRY.get(corpus_id)
    if corpus is None:
        return None

    m = corpus.extract_pattern.match(ref_str)
    if m:
        work = m.group("work")
        if work not in corpus.works:
            return None
        return {
            "work": work,
            "chapter": int(m.group("ch")),
            "verse": int(m.group("vs")),
            "sub": int(m.group("sub")) if _safe_group(m, "sub") else None,
        }

    # Q source: try colon format
    if corpus_id == "q_source":
        m2 = _Q_EXTRACT.match(ref_str)
        if m2:
            return {
                "work": m2.group("work"),
                "chapter": int(m2.group("ch")),
                "verse": int(m2.group("vs")),
                "sub": None,
            }

    return None


def list_works(corpus_id: str) -> list[dict]:
    """Return a list of work metadata dicts for a corpus."""
    corpus = CORPUS_REGISTRY.get(corpus_id)
    if corpus is None:
        return []
    return [
        {
            "abbrev": w.abbrev,
            "name": w.name,
            "has_book": w.has_book,
            "levels": w.levels,
        }
        for w in corpus.works.values()
    ]
