"""Open Scriptures data integration — Hebrew morphology + Strong's dictionaries.

Data sources (downloaded on first use or via script):
- morphhb: Hebrew Bible morphology (Westminster)
- strongs: Strong's Hebrew + Greek dictionaries
- HebrewLexicon: BDB outline with Strong's links

GitHub: https://github.com/openscriptures

All data is Creative Commons Attribution 4.0 (morphology/lexicon) or
Public Domain (Strong's text, WLC text).  See individual repos for details.
"""

from __future__ import annotations

import shutil
import subprocess
from pathlib import Path
from typing import Dict, List, Optional
from xml.etree import ElementTree as ET

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

_DEFAULT_DATA_DIR = Path.home() / "accordance-data" / "openscriptures"

_REPOS = {
    "morphhb": {
        "name": "OSHB Morphology (WLC + Westminster parsing)",
        "git_url": "https://github.com/openscriptures/morphhb",
        "target_dir": "morphhb",
    },
    "strongs": {
        "name": "Strong's Hebrew + Greek Dictionaries",
        "git_url": "https://github.com/openscriptures/strongs",
        "target_dir": "strongs",
    },
    "hebrew_lexicon": {
        "name": "BDB Hebrew Lexicon",
        "git_url": "https://github.com/openscriptures/HebrewLexicon",
        "target_dir": "HebrewLexicon",
    },
}

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _git_available() -> bool:
    return shutil.which("git") is not None


def _clone_repo(git_url: str, target: Path) -> bool:
    """Clone a repo with depth 1. Returns True on success."""
    if target.exists():
        shutil.rmtree(target)
    result = subprocess.run(
        ["git", "clone", "--depth", "1", git_url, str(target)],
        capture_output=True,
        text=True,
    )
    return result.returncode == 0


def _dir_size(path: Path) -> str:
    """Human-readable directory size."""
    total = sum(f.stat().st_size for f in path.rglob("*") if f.is_file())
    if total >= 1_000_000_000:
        return f"{total / 1_000_000_000:.1f} GB"
    if total >= 1_000_000:
        return f"{total / 1_000_000:.1f} MB"
    if total >= 1_000:
        return f"{total / 1_000:.1f} KB"
    return f"{total} B"


def _normalize_strongs(s: str) -> str:
    """Normalize a Strong's number like 'H7225', 'h7225', '7225' to 'H7225'."""
    s = s.strip()
    if not s:
        return ""
    # If it starts with H/G, preserve that prefix
    if s[0].upper() in ("H", "G"):
        prefix = s[0].upper()
        num = s[1:].lstrip("0") or "0"
    else:
        # Auto-detect: Hebrew numbers are typically < 9000
        num = s.lstrip("0") or "0"
        try:
            prefix = "H" if int(num) < 9000 else "G"
        except ValueError:
            return s.upper()
    return f"{prefix}{num}"


def _strip_strongs_lemma(lemma_raw: str) -> str:
    """Strip augmented prefix characters from an OSHB lemma like 'c/m/6529'.

    The OSHB uses augmented Strong's numbers: prefixes b/ c/ d/ h/ k/ l/ m/
    and suffixes a, b, c (homograph disambiguation).  We strip both to return
    the base Strong's number, e.g. 'c/m/6529' → '6529'.
    """
    # Remove prefix segments (single lowercase letters followed by /)
    # e.g. "c/m/6529" → "6529", "d/8064" → "8064"
    result = lemma_raw.strip()
    # Keep stripping leading single-char segments like "c/", "b/", "d/", etc.
    while "/" in result:
        parts = result.split("/", 1)
        if len(parts[0]) == 1 and parts[0].islower():
            result = parts[1]
        else:
            break
    # Strip trailing homograph disambiguator (space + letter)
    # e.g. "1254 a" → "1254"
    if " " in result:
        result = result.split(" ", 1)[0]
    return result


# ---------------------------------------------------------------------------
# OSIS XML namespace handling
# ---------------------------------------------------------------------------

_OSIS_NS = "http://www.bibletechnologies.net/2003/OSIS/namespace"

# Namespace used by LexicalIndex.xml and BrownDriverBriggs.xml
_LEX_NS = "http://openscriptures.github.com/morphhb/namespace"

# ---------------------------------------------------------------------------
# OpenScripturesClient
# ---------------------------------------------------------------------------


class OpenScripturesClient:
    """Client for Open Scriptures morphology, Strong's, and BDB data.

    Downloads repos on first use via ``git clone --depth 1``.  All parsing
    uses stdlib only — no external dependencies.

    Usage::

        client = OpenScripturesClient()
        morph = client.get_hebrew_morph("בְּרֵאשִׁית")
        defn  = client.get_strongs_definition("H7225")
        bdb   = client.get_bdb_entry("H7225")
        results = client.search_strongs("beginning", language="he")
    """

    def __init__(self, data_dir: str | Path | None = None):
        if data_dir is None:
            self.data_dir = _DEFAULT_DATA_DIR
        else:
            self.data_dir = Path(data_dir)
        # Caches
        self._wlc_book_cache: Dict[str, ET.Element] = {}
        self._strongs_he_cache: Optional[ET.Element] = None
        self._strongs_gr_cache: Optional[ET.Element] = None
        self._bdb_cache: Optional[ET.Element] = None
        self._lexical_index_cache: Optional[ET.Element] = None
        self._strongs_search_index: Optional[Dict[str, List[str]]] = None

    # ------------------------------------------------------------------
    # Data download
    # ------------------------------------------------------------------

    def ensure_data(self) -> bool:
        """Download all three Open Scriptures repos if not present.

        Returns True if data is available (already present or successfully
        downloaded), False otherwise.
        """
        if self.available():
            return True
        if not _git_available():
            print("OpenScripturesClient: git not found on PATH.  Cannot download data.")
            print("Install git or clone repos manually to:", self.data_dir)
            return False

        self.data_dir.mkdir(parents=True, exist_ok=True)
        success = True
        for key, repo in _REPOS.items():
            target = self.data_dir / repo["target_dir"]
            if target.is_dir():
                continue  # already exists
            print(f"Downloading {repo['name']} ...")
            if _clone_repo(repo["git_url"], target):
                print(f"  Done ({_dir_size(target)}).")
            else:
                print(f"  Failed to clone {repo['git_url']}.")
                success = False
        return success

    def available(self) -> bool:
        """Check whether all Open Scriptures data repos are present."""
        return all((self.data_dir / _REPOS[k]["target_dir"]).is_dir() for k in _REPOS)

    # ------------------------------------------------------------------
    # MorphHB — Hebrew morphology lookup
    # ------------------------------------------------------------------

    @property
    def wlc_dir(self) -> Path:
        return self.data_dir / "morphhb" / "wlc"

    def _load_wlc_book(self, book_abbr: str) -> Optional[ET.Element]:
        """Load and cache a single WLC book XML file."""
        if book_abbr in self._wlc_book_cache:
            return self._wlc_book_cache[book_abbr]
        path = self.wlc_dir / f"{book_abbr}.xml"
        if not path.exists():
            self._wlc_book_cache[book_abbr] = None
            return None
        try:
            tree = ET.parse(str(path))
            root = tree.getroot()
            self._wlc_book_cache[book_abbr] = root
            return root
        except ET.ParseError:
            self._wlc_book_cache[book_abbr] = None
            return None

    def _scan_wlc_for_word(self, word: str, max_results: int = 20) -> List[Dict]:
        """Scan all WLC book files for a word.  First exact Unicode match
        wins; falls back to diacritic-agnostic matching.

        Returns list of dicts with keys: word, lemma, morph, book, reference.
        """
        results: List[Dict] = []
        word_stripped = word.strip()

        # Try exact match first, then diacritic-insensitive
        for match_mode in ("exact", "lenient"):
            for fpath in sorted(self.wlc_dir.glob("*.xml")):
                book_abbr = fpath.stem  # e.g. "Gen", "Exod"
                root = self._load_wlc_book(book_abbr)
                if root is None:
                    continue

                # OSIS: <div type="book"> → <chapter> → <verse> → <w>
                for div_elem in root.iter(f"{{{_OSIS_NS}}}div"):
                    if div_elem.get("type") != "book":
                        continue
                    for ch_elem in div_elem.iter(f"{{{_OSIS_NS}}}chapter"):
                        for v_elem in ch_elem.iter(f"{{{_OSIS_NS}}}verse"):
                            v_id = v_elem.get("osisID", "")
                            for w_elem in v_elem.iter(f"{{{_OSIS_NS}}}w"):
                                w_text = "".join(w_elem.itertext()).strip()
                                if match_mode == "exact":
                                    if w_text != word_stripped:
                                        continue
                                else:
                                    # Diacritic-agnostic: strip Hebrew niqqud
                                    if _strip_niqqud(w_text) != _strip_niqqud(
                                        word_stripped
                                    ):
                                        continue

                                lemma_raw = w_elem.get("lemma", "")
                                morph_raw = w_elem.get("morph", "")
                                results.append(
                                    {
                                        "word": w_text,
                                        "lemma": lemma_raw,
                                        "lemma_strongs": _strip_strongs_lemma(
                                            lemma_raw
                                        ),
                                        "morph_code": morph_raw,
                                        "morph_description": _describe_oshb_morph(
                                            morph_raw
                                        ),
                                        "book": book_abbr,
                                        "reference": v_id.replace(".", " "),
                                    }
                                )
                                if len(results) >= max_results:
                                    return results
            if results:
                return results
        return results

    def get_hebrew_morph(self, word: str) -> List[Dict]:
        """Look up a Hebrew word's morphology across the entire Hebrew Bible.

        Args:
            word: A Hebrew word string (with or without niqqud/diacritics).

        Returns:
            List of dicts with keys: word, lemma, lemma_strongs, morph_code,
            morph_description, book, reference.

            Returns a list with a helpful message dict (key 'message') if data
            is not available.
        """
        if not self.available() and not self.ensure_data():
            return [
                {
                    "message": "Open Scriptures data not available. "
                    "Run client.ensure_data() or download repos "
                    f"to {self.data_dir}."
                }
            ]
        wlc = self.wlc_dir
        if not wlc.is_dir() or not any(wlc.glob("*.xml")):
            return [
                {"message": "morphhb WLC data not found.  Run client.ensure_data()."}
            ]
        results = self._scan_wlc_for_word(word)
        if not results:
            return [{"message": f"No morphology found for word: {word}", "word": word}]
        return results

    def get_verse_morph(self, book: str, chapter: int, verse: int) -> List[Dict]:
        """Get morphology for every word in a specific verse.

        Args:
            book: Book abbreviation (e.g. "Gen", "Exod", "Ps").
            chapter: Chapter number.
            verse: Verse number.

        Returns:
            List of dicts with morphology data for each word.
        """
        if not self.wlc_dir.is_dir():
            self.ensure_data()
        root = self._load_wlc_book(book)
        if root is None:
            return [{"message": f"Book '{book}' not found in WLC data."}]

        target_id = f"{book}.{chapter}.{verse}"
        results: List[Dict] = []
        # Iterate verse → w (descending) instead of w → parent (stdlib can't)
        for v_elem in root.iter(f"{{{_OSIS_NS}}}verse"):
            if v_elem.get("osisID") != target_id:
                continue
            for w_elem in v_elem.iter(f"{{{_OSIS_NS}}}w"):
                w_text = "".join(w_elem.itertext()).strip()
                lemma_raw = w_elem.get("lemma", "")
                morph_raw = w_elem.get("morph", "")
                results.append(
                    {
                        "word": w_text,
                        "lemma": lemma_raw,
                        "lemma_strongs": _strip_strongs_lemma(lemma_raw),
                        "morph_code": morph_raw,
                        "morph_description": _describe_oshb_morph(morph_raw),
                        "reference": target_id.replace(".", " "),
                    }
                )
            break  # found the verse, done
        if not results:
            return [{"message": f"No data for {target_id}"}]
        return results

    # ------------------------------------------------------------------
    # Strong's dictionary
    # ------------------------------------------------------------------

    @property
    def _strongs_he_path(self) -> Path:
        return self.data_dir / "strongs" / "hebrew" / "StrongHebrewG.xml"

    @property
    def _strongs_gr_path(self) -> Path:
        # Greek Strong's is in a versioned subdirectory
        gr_dir = self.data_dir / "strongs" / "greek"
        # Try the versioned path first
        for sub in sorted(gr_dir.glob("StrongsGreekDictionaryXML_*"), reverse=True):
            if sub.is_dir():
                path = sub / "strongsgreek.xml"
                if path.exists():
                    return path
        # Fallback
        return gr_dir / "StrongGreekG.xml"

    def _load_strongs_xml(self, language: str) -> Optional[ET.Element]:
        """Load and cache the Strong's XML for a language."""
        lang = language.strip().lower()
        if lang in ("he", "hebrew"):
            if self._strongs_he_cache is not None:
                return self._strongs_he_cache
            path = self._strongs_he_path
        elif lang in ("gr", "greek"):
            if self._strongs_gr_cache is not None:
                return self._strongs_gr_cache
            path = self._strongs_gr_path
        else:
            return None

        if not path.exists():
            return None
        try:
            tree = ET.parse(str(path))
            root = tree.getroot()
            if lang in ("he", "hebrew"):
                self._strongs_he_cache = root
            else:
                self._strongs_gr_cache = root
            return root
        except ET.ParseError:
            return None

    @staticmethod
    def _find_strongs_entry_he(root: ET.Element, num_only: str) -> Optional[ET.Element]:
        """Find a Strong's entry in the OSIS-format Hebrew XML.

        Entries are <div type="entry" n="7225"> elements.
        """
        for div_elem in root.iter(f"{{{_OSIS_NS}}}div"):
            if div_elem.get("type") == "entry" and div_elem.get("n") == num_only:
                return div_elem
        return None

    @staticmethod
    def _find_strongs_entry_gr(root: ET.Element, num_only: str) -> Optional[ET.Element]:
        """Find a Strong's entry in the DTD-format Greek XML.

        Entries are <entry strongs="00001"> elements (5-digit zero-padded, no namespace).
        """
        padded = num_only.zfill(5)
        for entry_elem in root.iter("entry"):
            if entry_elem.get("strongs") == padded:
                return entry_elem
        return None

    def get_strongs_definition(self, strongs_number: str, language: str = "he") -> Dict:
        """Get the Strong's dictionary definition for a given Strong's number.

        Args:
            strongs_number: e.g. "H7225", "G3056", or just "7225".
            language: "he" (Hebrew, default) or "gr"/"greek".

        Returns:
            Dict with keys: strongs, language, lemma, definition, xlit.
            Or dict with 'message' key on failure / not found.
        """
        sn = _normalize_strongs(strongs_number)
        lang = language.strip().lower()
        if lang in ("he", "hebrew"):
            lang_code = "he"
            lang_name = "Hebrew"
        elif lang in ("gr", "greek"):
            lang_code = "gr"
            lang_name = "Greek"
        else:
            return {
                "message": f"Unknown language '{language}'. Use 'he' or 'greek'.",
                "strongs": sn,
            }

        if not self.available() and not self.ensure_data():
            return {"message": "Open Scriptures data not available.", "strongs": sn}

        root = self._load_strongs_xml(lang_code)
        if root is None:
            return {
                "message": f"Strong's {lang_name} XML not found.  "
                "Run client.ensure_data().",
                "strongs": sn,
            }

        # The OSIS-format Strong's uses <div type="entry" n="7225"> elements
        # Each entry contains a <w> with lemma/POS/xlit and definition text
        # in child <seg> and <note> elements.
        num_only = sn[1:]  # strip H/G prefix

        # Find entry using the appropriate format parser
        if lang_code == "he":
            entry_elem = self._find_strongs_entry_he(root, num_only)
        else:
            entry_elem = self._find_strongs_entry_gr(root, num_only)

        if entry_elem is None:
            return {
                "message": f"Strong's {sn} not found in {lang_name} dictionary.",
                "strongs": sn,
            }

        # Extract lemma and definition
        if lang_code == "he":
            # OSIS format: <w> child has lemma/xlit, <seg>/<note> have definition
            lemma = ""
            xlit = ""
            definition = ""
            for child in entry_elem:
                tag_local = (
                    child.tag.split("}", 1)[-1] if "}" in child.tag else child.tag
                )
                if tag_local == "w":
                    lemma = child.get("lemma", "")
                    xlit = child.get("xlit", "")
                elif tag_local in ("seg", "note"):
                    definition += " " + "".join(child.itertext()).strip()
            definition = definition.strip()
        else:
            # Greek DTD format: <greek>/<strongs_def>/<kjv_def> children
            lemma = ""
            xlit = ""
            definition = ""
            for child in entry_elem:
                if child.tag == "greek":
                    lemma = child.get("unicode", "")
                    xlit = child.get("translit", "")
                elif child.tag == "strongs_def":
                    definition += " " + "".join(child.itertext()).strip()
                elif child.tag == "kjv_def":
                    definition += " [KJV: " + "".join(child.itertext()).strip() + "]"
            definition = definition.strip()

        return {
            "strongs": sn,
            "language": lang_name,
            "lemma": lemma,
            "xlit": xlit,
            "definition": definition,
        }

    def search_strongs(self, query: str, language: str = "he") -> List[Dict]:
        """Search Strong's dictionary entries by English definition text.

        Args:
            query: English search term(s).
            language: "he" (Hebrew default) or "greek".

        Returns:
            List of matching entries, each with strongs, lemma, definition.
            Max 50 results.
        """
        lang = language.strip().lower()
        if lang in ("he", "hebrew"):
            lang_code = "he"
            lang_name = "Hebrew"
        elif lang in ("gr", "greek"):
            lang_code = "gr"
            lang_name = "Greek"
        else:
            return [{"message": f"Unknown language '{language}'. Use 'he' or 'greek'."}]

        if not self.available() and not self.ensure_data():
            return [{"message": "Open Scriptures data not available."}]

        root = self._load_strongs_xml(lang_code)
        if root is None:
            return [{"message": f"Strong's {lang_name} XML not found."}]

        query_lower = query.lower()
        results: List[Dict] = []
        max_results = 50

        is_hebrew = lang_code == "he"

        for div_elem in root.iter(f"{{{_OSIS_NS}}}div" if is_hebrew else "entry"):
            if is_hebrew:
                if div_elem.get("type") != "entry":
                    continue
                n = div_elem.get("n", "")
            else:
                n = div_elem.get("strongs", "").lstrip("0") or "0"

            # Gather all text in the entry
            full_text = " ".join(
                "".join(child.itertext()).strip() for child in div_elem
            )
            if query_lower not in full_text.lower():
                continue

            # Extract lemma and definition
            if is_hebrew:
                lemma = ""
                for child in div_elem:
                    tag_local = (
                        child.tag.split("}", 1)[-1] if "}" in child.tag else child.tag
                    )
                    if tag_local == "w":
                        lemma = child.get("lemma", "")
                        break
                def_parts = []
                for child in div_elem:
                    tag_local = (
                        child.tag.split("}", 1)[-1] if "}" in child.tag else child.tag
                    )
                    if tag_local in ("seg", "note"):
                        def_parts.append("".join(child.itertext()).strip())
                definition = " ".join(def_parts).strip()
            else:
                lemma = ""
                for child in div_elem:
                    if child.tag == "greek":
                        lemma = child.get("unicode", "")
                        break
                def_parts = []
                for child in div_elem:
                    if child.tag == "strongs_def":
                        def_parts.append("".join(child.itertext()).strip())
                    elif child.tag == "kjv_def":
                        def_parts.append(
                            "[KJV: " + "".join(child.itertext()).strip() + "]"
                        )
                definition = " ".join(def_parts).strip()

            prefix = lang_name[0].upper()
            strongs_num = f"{prefix}{n}"

            results.append(
                {
                    "strongs": strongs_num,
                    "lemma": lemma,
                    "definition": definition[:500],
                }
            )
            if len(results) >= max_results:
                break

        return results[:max_results]

    # ------------------------------------------------------------------
    # BDB Lexicon (via HebrewLexicon repo)
    # ------------------------------------------------------------------

    @property
    def _bdb_dir(self) -> Path:
        return self.data_dir / "HebrewLexicon"

    @property
    def _bdb_xml_path(self) -> Path:
        return self._bdb_dir / "BrownDriverBriggs.xml"

    @property
    def _lexical_index_path(self) -> Path:
        return self._bdb_dir / "LexicalIndex.xml"

    def _load_bdb(self) -> Optional[ET.Element]:
        """Load BDB XML, cached."""
        if self._bdb_cache is not None:
            return self._bdb_cache
        path = self._bdb_xml_path
        if not path.exists():
            return None
        try:
            tree = ET.parse(str(path))
            self._bdb_cache = tree.getroot()
            return self._bdb_cache
        except ET.ParseError:
            return None

    def _load_lexical_index(self) -> Optional[ET.Element]:
        """Load LexicalIndex.xml, cached."""
        if self._lexical_index_cache is not None:
            return self._lexical_index_cache
        path = self._lexical_index_path
        if not path.exists():
            return None
        try:
            tree = ET.parse(str(path))
            self._lexical_index_cache = tree.getroot()
            return self._lexical_index_cache
        except ET.ParseError:
            return None

    def get_bdb_entry(self, strongs_number: str) -> Dict:
        """Get the BDB lexicon entry linked to a Strong's Hebrew number.

        Uses LexicalIndex.xml to map Strong's → BDB entry ID, then reads
        the BDB XML for the full entry.

        Args:
            strongs_number: e.g. "H7225" or just "7225".

        Returns:
            Dict with keys: strongs, bdb_id, entry (text).  Or dict with
            'message' key if not available.
        """
        sn = _normalize_strongs(strongs_number)
        if not sn.startswith("H"):
            return {"message": f"BDB is a Hebrew lexicon. Got: {sn}", "strongs": sn}

        if not self.available() and not self.ensure_data():
            return {"message": "Open Scriptures data not available.", "strongs": sn}

        # Step 1: Look up Strong's number in LexicalIndex
        li = self._load_lexical_index()
        if li is None:
            return {
                "message": "LexicalIndex.xml not found.  Run client.ensure_data().",
                "strongs": sn,
            }

        num_only = sn[1:]

        # LexicalIndex has <xref strong="7225" bdb="a.xx.xx"/> in entries
        bdb_id: Optional[str] = None
        for entry_elem in li.iter(f"{{{_LEX_NS}}}entry"):
            for xref in entry_elem.iter(f"{{{_LEX_NS}}}xref"):
                if xref.get("strong") == num_only:
                    bdb_id = xref.get("bdb", "")
                    break
            if bdb_id:
                break

        # Also try the AugIndex (maps augmented Strong's to lexical entries)
        if bdb_id is None:
            aug_path = self._bdb_dir / "AugIndex.xml"
            if aug_path.exists():
                try:
                    aug_tree = ET.parse(str(aug_path))
                    aug_root = aug_tree.getroot()
                    for elem in aug_root.iter():
                        strong_attr = elem.get("strong", "")
                        if strong_attr == num_only or strong_attr == sn:
                            bdb_id = elem.get("bdb", "")
                            if bdb_id:
                                break
                except ET.ParseError:
                    pass

        if not bdb_id:
            return {"message": f"No BDB entry found for Strong's {sn}.", "strongs": sn}

        # Step 2: Look up the BDB ID in BrownDriverBriggs.xml
        bdb_root = self._load_bdb()
        if bdb_root is None:
            return {"message": "BrownDriverBriggs.xml not found.", "strongs": sn}

        bdb_entry_elem = None
        for entry_elem in bdb_root.iter(f"{{{_LEX_NS}}}entry"):
            if entry_elem.get("id") == bdb_id:
                bdb_entry_elem = entry_elem
                break

        if bdb_entry_elem is None:
            return {
                "message": f"BDB entry '{bdb_id}' not found in XML.",
                "strongs": sn,
                "bdb_id": bdb_id,
            }

        entry_text = _element_to_text(bdb_entry_elem)

        return {
            "strongs": sn,
            "bdb_id": bdb_id,
            "entry": entry_text[:2000],  # truncate very long entries
        }


# ---------------------------------------------------------------------------
# Internal helpers — OSHB morphology description
# ---------------------------------------------------------------------------


def _describe_oshb_morph(code: str) -> str:
    """Convert an OSHB/Westminster morphology code to a human-readable string.

    Uses the same Hebrew morphology parser from morphology.py.
    """
    if not code:
        return "(none)"
    # Delegate to the existing Hebrew morphology parser
    from .morphology import describe_hebrew_morph

    return describe_hebrew_morph(code)


_HEBREW_NIQQUD = frozenset(
    "\u05b0\u05b1\u05b2\u05b3\u05b4\u05b5\u05b6\u05b7\u05b8"
    "\u05b9\u05ba\u05bb\u05bc\u05bd\u05be\u05bf\u05c0\u05c1"
    "\u05c2\u05c3\u05c4\u05c5\u05c6\u05c7"
)


def _strip_niqqud(text: str) -> str:
    """Remove Hebrew niqqud (vowel points) and cantillation marks."""
    return "".join(ch for ch in text if ch not in _HEBREW_NIQQUD)


def _element_to_text(elem: ET.Element) -> str:
    """Extract all text from an XML element and its children, space-separated."""
    parts = []
    if elem.text:
        parts.append(elem.text.strip())
    for child in elem:
        parts.append(_element_to_text(child))
        if child.tail:
            parts.append(child.tail.strip())
    return " ".join(p for p in parts if p)


# ============================================================================
# MCP Tool Snippets
# ============================================================================
# Paste these into accordance_mcp_server.py alongside the other tool
# registrations.  They call ``_get_os_client()`` to obtain a lazy singleton.
# ============================================================================

MCP_TOOL_SNIPPETS = r'''
# ---------------------------------------------------------------------------
# MCP Tool: lookup_openscriptures_morph
# ---------------------------------------------------------------------------

_os_client = None

def _get_os_client():
    global _os_client
    if _os_client is None:
        from accordance_parser.openscriptures import OpenScripturesClient
        _os_client = OpenScripturesClient()
    return _os_client


@mcp.tool()
def lookup_openscriptures_morph(word: str, max_results: int = 10) -> str:
    """Look up Hebrew word morphology from the Open Scriptures Hebrew Bible.

    Searches the Westminster morphology database (morphhb) for every occurrence
    of a Hebrew word, returning lemma, parsing, and Strong's numbers.

    Args:
        word: Hebrew word to look up (with or without vowel points/diacritics).
        max_results: Maximum results to return (default 10, max 50).

    Returns:
        Markdown report with word, lemma, Strong's number, morphology parsing,
        and Bible reference for each occurrence.
    """
    if max_results > 50:
        max_results = 50
    client = _get_os_client()
    results = client.get_hebrew_morph(word)
    if not results:
        return f"No morphology found for word: {word}"
    if "message" in results[0]:
        return str(results[0]["message"])

    shown = results[:max_results]
    lines = [
        f"# Hebrew Morphology: {word}",
        f"**Results:** {len(shown)} of {len(results)} occurrences",
        "",
        "| # | Word | Lemma | Strong's | Morph | Reference |",
        "|---|------|-------|----------|-------|-----------|",
    ]
    for i, r in enumerate(shown, 1):
        lemma_short = r.get("lemma", "")[:20]
        strongs = r.get("lemma_strongs", "")
        morph_desc = r.get("morph_description", "")
        ref = r.get("reference", "")
        lines.append(
            f"| {i} | {r.get('word', '')[:12]} | {lemma_short}"
            f" | {strongs} | {morph_desc} | {ref} |"
        )
    return "\n".join(lines)


# ---------------------------------------------------------------------------
# MCP Tool: search_openscriptures_strongs
# ---------------------------------------------------------------------------

@mcp.tool()
def search_openscriptures_strongs(query: str, language: str = "hebrew",
                                   max_results: int = 20) -> str:
    """Search Open Scriptures Strong's dictionaries by English definition text.

    Args:
        query: English search term(s) to find in Strong's definitions.
        language: "hebrew" (default) or "greek".
        max_results: Maximum results to return (default 20, max 50).

    Returns:
        Markdown report with matching Strong's entries.
    """
    if max_results > 50:
        max_results = 50
    client = _get_os_client()
    results = client.search_strongs(query, language=language)
    if not results:
        return f"No Strong's entries found matching: {query}"
    if "message" in results[0]:
        return str(results[0]["message"])

    shown = results[:max_results]
    lang_label = "Hebrew" if language.lower() in ("he", "hebrew") else "Greek"
    lines = [
        f"# Strong's {lang_label} — Search: \"{query}\"",
        f"**Results:** {len(shown)} of {len(results)} matches",
        "",
    ]
    for i, r in enumerate(shown, 1):
        lines.append(f"### {i}. {r['strongs']} — {r.get('lemma', '')}")
        lines.append(r.get("definition", "")[:600])
        lines.append("")
    return "\n".join(lines)
'''
