"""LXX Deuterocanonical annotation integration.

Provides Strong's numbers, morphological tagging, lemmas, and English
glosses for deuterocanonical books via the LXX Greek text.

Data source: eliranwong/LXX-Rahlfs-1935 (CC BY-NC-SA 4.0)
GitHub: https://github.com/eliranwong/LXX-Rahlfs-1935

Format: line-number-indexed CSV files. Each file has 623,693 lines,
one per word in the Rahlfs 1935 Septuagint. The versification file
maps line numbers to (book, chapter, verse) using Rahlfs numbering.

Rahlfs deutero book numbers (derived from LXX order + chapter counts):
  80: 1 Maccabees (16 ch)
  81: Judith (16 ch, 924 lines)
  82: 2 Maccabees (15 ch)
  87: Sirach / Ecclesiasticus (51 ch)
  88: Tobit (14 ch)
  89: Wisdom of Solomon (19 ch)
  70: Esther (Greek, with additions, 5 ch)
  73: ? (1 ch fragment)
  75: ? (1 ch fragment)
  77: ? (9 ch)
  79: ? (1 ch fragment)
  83: ? (7 ch)
  84: ? (18 ch)
  90: ? (18 ch)
  91: ? (14 ch)
"""

from __future__ import annotations

import logging
import re
import subprocess
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from urllib.request import urlretrieve
import zipfile

logger = logging.getLogger(__name__)

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

_DEFAULT_DATA_DIR = Path.home() / "accordance-data" / "lxx-deutero"
_REPO_URL = "https://github.com/eliranwong/LXX-Rahlfs-1935"
_ZIP_URL = f"{_REPO_URL}/archive/refs/heads/main.zip"

# Rahlfs book number → standard English name (verified deutero books)
# Mapped by LXX order + chapter count matching
# Note: Baruch and Letter of Jeremiah appear in the protocanonical
# Rahlfs range (placed between Jeremiah and Lamentations in the LXX),
# so they use † (protocanonical) prefix in the versification file
# and are not listed here.

# Protocanonical books that have deuterocanonical additions in the LXX
# and thus align with Catholic (NABRE/NRSVCE) versification rather
# than KJV/Protestant numbering. Use LXX Strong's for these.
# Search paths within the cloned repo
_VERS_PATH = "12-Marvel.Bible/00-versification_original.csv"
_STRONGS_PATH = "07_StrongNumber/final_Strongs.csv"
_GLOSS_PATH = "12-Marvel.Bible/06-gloss.csv"
_LEXEME_PATH = "12-Marvel.Bible/09-lexemes.csv"
_MORPH_PATH = "03a_morphology_with_JTauber_patches/patched_623693.csv"
_SN_GREEK_PATH = "07_StrongNumber/groundwork/SN_Greek.csv"

# Full versification: book.chapter.verse → line number (30,637 entries)
_VERS_FULL_PATH = "08_versification/001_verse_c_modified_KEEP.csv"

# Book abbreviation → standard English name (from full versification file)
_BOOK_ABBREV_MAP: Dict[str, str] = {
    # Protocanonical
    "Gen": "Genesis",
    "Exod": "Exodus",
    "Lev": "Leviticus",
    "Num": "Numbers",
    "Deut": "Deuteronomy",
    "JoshA": "Joshua",
    "JoshB": "Joshua",
    "JudgA": "Judges",
    "JudgB": "Judges",
    "Ruth": "Ruth",
    "1Sam": "1 Samuel",
    "2Sam": "2 Samuel",
    "1Kgs": "1 Kings",
    "2Kgs": "2 Kings",
    "1Chr": "1 Chronicles",
    "2Chr": "2 Chronicles",
    "2Esdr": "Ezra",
    "Neh": "Nehemiah",
    "Esth": "Esther",
    "Job": "Job",
    "Ps": "Psalms",
    "Prov": "Proverbs",
    "Eccl": "Ecclesiastes",
    "Song": "Song of Solomon",
    "Isa": "Isaiah",
    "Jer": "Jeremiah",
    "Lam": "Lamentations",
    "Ezek": "Ezekiel",
    "DanOG": "Daniel",
    "DanTh": "Daniel",
    "Hos": "Hosea",
    "Joel": "Joel",
    "Amos": "Amos",
    "Obad": "Obadiah",
    "Jonah": "Jonah",
    "Mic": "Micah",
    "Nah": "Nahum",
    "Hab": "Habakkuk",
    "Zeph": "Zephaniah",
    "Hag": "Haggai",
    "Zech": "Zechariah",
    "Mal": "Malachi",
    # Deuterocanonical
    "TobBA": "Tobit",
    "TobS": "Tobit",
    "Jdt": "Judith",
    "Wis": "Wisdom of Solomon",
    "Sir": "Sirach",
    "Bar": "Baruch",
    "EpJer": "Letter of Jeremiah",
    "1Macc": "1 Maccabees",
    "2Macc": "2 Maccabees",
    "3Macc": "3 Maccabees",
    "4Macc": "4 Maccabees",
    "1Esdr": "1 Esdras",
    "SusOG": "Susanna",
    "SusTh": "Susanna",
    "BelOG": "Bel and the Dragon",
    "BelTh": "Bel and the Dragon",
    "PsSol": "Psalms of Solomon",
    "Odes": "Odes",
}

# ---------------------------------------------------------------------------
# LXXDeuteroAnnotator
# ---------------------------------------------------------------------------


class LXXDeuteroAnnotator:
    """Provides Strong's + morphology + glosses for deuterocanonical books.

    Uses the eliranwong/LXX-Rahlfs-1935 CSV dataset.
    Data is cloned on first use if not already present.
    """

    def __init__(self, data_dir: Optional[str] = None):
        self.data_dir = Path(data_dir) if data_dir else _DEFAULT_DATA_DIR
        self._repo_dir = self.data_dir / "LXX-Rahlfs-1935"
        # Caches
        self._strongs: Optional[List[str]] = None  # line → G-number
        self._glosses: Optional[List[str]] = None  # line → English gloss
        self._lexemes: Optional[List[str]] = None  # line → Greek lexeme
        self._morph: Optional[List[str]] = None  # line → morphology code
        self._verse_index: Optional[Dict[Tuple[str, int, int], List[int]]] = None
        self._sn_greek_lookup: Optional[Dict[str, str]] = None  # lexeme → Strong's

    # -- Data availability --

    def available(self) -> bool:
        """Check if data is downloaded and parseable."""
        return (self._repo_dir / _VERS_PATH).exists()

    def ensure_data(self) -> bool:
        """Clone/download the LXX-Rahlfs-1935 repo if not present."""
        if self.available():
            return True
        self.data_dir.mkdir(parents=True, exist_ok=True)
        try:
            subprocess.run(
                ["git", "clone", "--depth", "1", _REPO_URL],
                cwd=str(self.data_dir),
                check=True,
                capture_output=True,
                timeout=120,
            )
            return True
        except Exception:
            logger.warning("git clone failed, trying ZIP download")
        try:
            zip_path = self.data_dir / "lxx.zip"
            urlretrieve(_ZIP_URL, zip_path)
            with zipfile.ZipFile(zip_path) as zf:
                zf.extractall(self.data_dir)
            zip_path.unlink()
            # Rename extracted dir
            extracted = self.data_dir / "LXX-Rahlfs-1935-main"
            if extracted.exists() and not self._repo_dir.exists():
                extracted.rename(self._repo_dir)
            return self.available()
        except Exception as e:
            logger.warning(f"ZIP download failed: {e}")
            return False

    # -- Internal: load line-indexed CSV files --

    def _load_line_file(self, rel_path: str) -> List[str]:
        """Load a line-numbered CSV, returning list where index=line_number."""
        path = self._repo_dir / rel_path
        if not path.exists():
            return []
        result = [""]  # 1-indexed: result[1] = line 1
        with open(path, encoding="utf-8") as f:
            for line in f:
                parts = line.rstrip("\n").split("\t")
                result.append(parts[1] if len(parts) > 1 else parts[0])
        return result

    def _ensure_line_data(self):
        """Lazy-load all line-indexed CSV files."""
        if self._strongs is not None:
            return
        self._strongs = self._load_line_file(_STRONGS_PATH)
        self._glosses = (
            self._load_line_file(_GLOSS_PATH)
            if (self._repo_dir / _GLOSS_PATH).exists()
            else []
        )
        self._lexemes = (
            self._load_line_file(_LEXEME_PATH)
            if (self._repo_dir / _LEXEME_PATH).exists()
            else []
        )
        self._morph = (
            self._load_line_file(_MORPH_PATH)
            if (self._repo_dir / _MORPH_PATH).exists()
            else []
        )
        self._ensure_sn_greek_lookup()

    # -- Internal: build verse → line numbers index --

    def _ensure_verse_index(self):
        """Build (book_abbrev, ch, vs) → [line_numbers] from full versification.

        Uses 08_versification/001_verse_c_modified_KEEP.csv which maps
        book.chapter.verse → starting line number.  To find all words
        for a verse, we collect lines from the start line up to the
        start of the next verse (or end of data).
        """
        if self._verse_index is not None:
            return
        self._verse_index = {}
        path = self._repo_dir / _VERS_FULL_PATH
        if not path.exists():
            return

        # First pass: collect all verse→start_line mappings
        entries = []  # (book_abbrev, ch, vs, line_num)
        with open(path, encoding="utf-8") as f:
            for line in f:
                parts = line.strip().split("\t")
                if len(parts) < 2:
                    continue
                ref = parts[0]
                try:
                    start_line = int(parts[1])
                except ValueError:
                    continue
                # Parse "Gen.1.1" or "TobBA.1.1"
                m = re.match(r"([A-Za-z0-9]+)\.(\d+)\.(\d+)", ref)
                if m:
                    bk_abbrev, ch, vs = m.group(1), int(m.group(2)), int(m.group(3))
                    entries.append((bk_abbrev, ch, vs, start_line))

        # Sort by line number for gap detection
        entries.sort(key=lambda x: x[3])

        # Second pass: for each verse, collect lines from start to next verse start
        for i, (bk, ch, vs, start) in enumerate(entries):
            # End is start of next verse (or last entry goes to end of data)
            if i + 1 < len(entries):
                end = entries[i + 1][3]
            else:
                end = 623694  # one past the last line (1-indexed)
            key = (bk, ch, vs)
            self._verse_index[key] = list(range(start, end))

    # -- Public API --

    def _ensure_sn_greek_lookup(self):
        """Build lexeme → Strong's number lookup from SN_Greek.csv."""
        if self._sn_greek_lookup is not None:
            return
        path = self._repo_dir / _SN_GREEK_PATH
        if not path.exists():
            self._sn_greek_lookup = {}
            return
        lookup: Dict[str, str] = {}
        with open(path, encoding="utf-8") as f:
            for line in f:
                parts = line.rstrip("\n").split("\t")
                if len(parts) >= 3:
                    lexeme = parts[1].strip()
                    try:
                        import json

                        data = json.loads(parts[2])
                        sn = str(data.get("strong", "")).strip()
                        if sn and lexeme:
                            lookup[lexeme] = sn
                    except (json.JSONDecodeError, ValueError):
                        pass
        self._sn_greek_lookup = lookup

    def resolve_book_abbrev(self, name: str) -> Optional[str]:
        """Map a standard English book name to LXX abbreviation."""
        name_lower = name.lower()
        for abbrev, en in _BOOK_ABBREV_MAP.items():
            if name_lower == en.lower():
                return abbrev
        # Aliases
        aliases: Dict[str, Optional[str]] = {
            "tobit": "TobBA",
            "tob": "TobBA",
            "tobias": "TobBA",
            "judith": "Jdt",
            "jdt": "Jdt",
            "wisdom": "Wis",
            "wis": "Wis",
            "wisdom of solomon": "Wis",
            "sirach": "Sir",
            "sir": "Sir",
            "ecclesiasticus": "Sir",
            "baruch": "Bar",
            "bar": "Bar",
            "letter of jeremiah": "EpJer",
            "epjer": "EpJer",
            "1 maccabees": "1Macc",
            "1macc": "1Macc",
            "1 macc": "1Macc",
            "2 maccabees": "2Macc",
            "2macc": "2Macc",
            "3 maccabees": "3Macc",
            "3macc": "3Macc",
            "4 maccabees": "4Macc",
            "4macc": "4Macc",
            "1 esdras": "1Esdr",
            "1esd": "1Esdr",
            "susanna": "SusTh",
            "sus": "SusTh",
            "bel and the dragon": "BelTh",
            "bel": "BelTh",
            "psalms of solomon": "PsSol",
            "pssol": "PsSol",
            "odes": "Odes",
            "ode": "Odes",
            "daniel": "DanTh",
            "dan": "DanTh",
            "esther": "Esth",
            "esth": "Esth",
        }
        return aliases.get(name_lower)

    def get_strongs_for_verse(self, book: str, chapter: int, verse: int) -> List[Dict]:
        """Get Strong's numbers for each word in a deuterocanonical verse.

        Returns list of {word_index, strongs} dicts.
        """
        abbrev = self.resolve_book_abbrev(book)
        if abbrev is None:
            return [
                {
                    "message": f"Unknown book: '{book}'. "
                    f"Try: {', '.join(sorted(_BOOK_ABBREV_MAP.values()))}"
                }
            ]
        if not self.available():
            return [{"message": "LXX data not available. Run annotator.ensure_data()"}]
        self._ensure_verse_index()
        self._ensure_line_data()
        assert self._verse_index is not None
        assert self._strongs is not None
        key = (abbrev, chapter, verse)
        lines = self._verse_index.get(key, [])
        if not lines:
            return []
        results = []
        for idx, ln in enumerate(lines):
            sn = self._strongs[ln] if ln < len(self._strongs) else ""
            # Fallback: look up lexeme in SN_Greek dictionary
            if not sn and self._lexemes and ln < len(self._lexemes):
                lexeme = self._lexemes[ln]
                if lexeme and self._sn_greek_lookup:
                    sn = self._sn_greek_lookup.get(lexeme, "")
                    if sn:
                        sn = f"G{sn}"
            results.append({"word_index": idx + 1, "strongs": sn})
        return results

    def get_morphology(self, book: str, chapter: int, verse: int) -> List[Dict]:
        """Get morphological parsing for each word.

        Returns list of {word_index, morphology, lemma} dicts.
        """
        rn = self.resolve_book_abbrev(book)
        if rn is None:
            return [{"message": f"Unknown deuterocanonical book: '{book}'"}]
        if not self.available():
            return [{"message": "LXX data not available."}]
        self._ensure_verse_index()
        self._ensure_line_data()
        assert self._verse_index is not None
        key = (rn, chapter, verse)
        lines = self._verse_index.get(key, [])
        if not lines:
            return []
        results = []
        for idx, ln in enumerate(lines):
            morph = self._morph[ln] if self._morph and ln < len(self._morph) else ""
            lemma = (
                self._lexemes[ln] if self._lexemes and ln < len(self._lexemes) else ""
            )
            results.append({"word_index": idx + 1, "morphology": morph, "lemma": lemma})
        return results

    def get_glosses(self, book: str, chapter: int, verse: int) -> List[Dict]:
        """Get English glosses for each word.

        Returns list of {word_index, gloss} dicts.
        """
        rn = self.resolve_book_abbrev(book)
        if rn is None:
            return [{"message": f"Unknown deuterocanonical book: '{book}'"}]
        if not self.available():
            return [{"message": "LXX data not available."}]
        self._ensure_verse_index()
        self._ensure_line_data()
        assert self._verse_index is not None
        key = (rn, chapter, verse)
        lines = self._verse_index.get(key, [])
        if not lines:
            return []
        results = []
        for idx, ln in enumerate(lines):
            gloss = (
                self._glosses[ln] if self._glosses and ln < len(self._glosses) else ""
            )
            results.append({"word_index": idx + 1, "gloss": gloss})
        return results

    def get_full_annotation(self, book: str, chapter: int, verse: int) -> List[Dict]:
        """Get combined annotation: Strong's + morphology + lemma + gloss.

        Returns list of {word_index, strongs, morphology, lemma, gloss} dicts.
        """
        strongs = self.get_strongs_for_verse(book, chapter, verse)
        morph = self.get_morphology(book, chapter, verse)
        glosses = self.get_glosses(book, chapter, verse)

        # If any returned error message, propagate it
        for r in [strongs, morph, glosses]:
            if r and "message" in r[0]:
                return r

        max_len = max(len(strongs), len(morph), len(glosses))
        results = []
        for i in range(max_len):
            entry: Dict = {"word_index": i + 1}
            if i < len(strongs):
                entry["strongs"] = strongs[i].get("strongs", "")
            if i < len(morph):
                entry.update({k: v for k, v in morph[i].items() if k != "word_index"})
            if i < len(glosses):
                entry["gloss"] = glosses[i].get("gloss", "")
            results.append(entry)
        return results

    def list_available_books(self) -> List[str]:
        """List all known books with LXX annotation data."""
        return sorted(set(_BOOK_ABBREV_MAP.values()))
