# #region LanguageDetectService [C:2] [TYPE Module] [SEMANTICS translate, language, detection, heuristic]
# @BRIEF Local language detection powered by lingua-language-detector (no LLM).
#         Provides fast, non-LLM language identification for source text rows,
#         reducing LLM token costs by pre-detecting same-language rows and
#         removing language detection from translation prompts.
# @LAYER Plugin
# @RATIONALE LLM-based language detection wastes tokens on every row. lingua is a
#            pure-Python library (pre-built wheel) that runs ~0.1ms/call with ~98%
#            accuracy, vs ~90% for LLM-based detection at ~100x+ cost.
# @REJECTED pycld3 — incompatible with Python 3.13 (requires longintrepr.h removed in 3.13).
#            langdetect — only 55 languages, less accurate on short texts.
#            fasttext — requires native compilation, no pre-built wheel for this platform.

import re

from lingua import Language, LanguageDetector, LanguageDetectorBuilder

# Cyrillic character range (Basic + Supplement)
_CYRILLIC_RE = re.compile(r"[\u0400-\u04FF\u0500-\u052F]")
# ASCII letter range (Latin-based)
_ASCII_LETTER_RE = re.compile(r"[a-zA-Z]")

# ---------------------------------------------------------------------------
# Build BCP-47 → lingua Language mapping from the lingua Language enum
# ---------------------------------------------------------------------------
_CODE_TO_LANG: dict[str, Language] = {}
for attr_name in dir(Language):
    if attr_name.startswith("_"):
        continue
    val = getattr(Language, attr_name)
    if not isinstance(val, Language):
        continue  # skip methods (all, from_str, etc.) and descriptors
    try:
        iso = val.iso_code_639_1
        code = iso.name.lower()
        _CODE_TO_LANG[code] = val
    except Exception:
        pass  # Language doesn't have ISO 639-1 code (e.g. Latin, Esperanto)

# ---------------------------------------------------------------------------
# Common source languages that appear in translation jobs (in addition to targets)
# ---------------------------------------------------------------------------
_COMMON_SOURCE_CODES: set[str] = {
    "en", "fr", "de", "es", "it", "pt", "pl", "uk",
    "tr", "ar", "zh", "ja", "ko", "vi", "th", "nl",
    "sv", "da", "fi", "cs", "ro", "el", "he", "hi",
    "id", "ms",
}

# ---------------------------------------------------------------------------
# Detector cache — reuse detector instances across calls with same language set
# ---------------------------------------------------------------------------
_detector_cache: dict[str, LanguageDetector] = {}


# #region _detector_cache_key [C:1] [TYPE Function] [SEMANTICS cache, key]
# @BRIEF Build a deterministic cache key from target languages list.
def _detector_cache_key(target_languages: list[str] | None) -> str:
    """Build a cache key from the set of target languages."""
    if not target_languages:
        return "__default"
    return ",".join(sorted(set(target_languages)))
# #endregion _detector_cache_key


# #region build_detector [C:2] [TYPE Function] [SEMANTICS language, detection, builder]
# @BRIEF Build a LanguageDetector restricted to target + common source languages.
#         Restricting the language set improves detection speed (~5x faster than
#         all 87 languages). The detector includes both target languages (for
#         pre-filtering) and common source languages (for metadata).
def build_detector(target_languages: list[str] | None = None) -> LanguageDetector:
    """Build a LanguageDetector restricted to target + common source languages.

    Args:
        target_languages: List of BCP-47 target language codes (e.g. ["ru", "de"]).
                          If None or empty, falls back to a default set.

    Returns:
        A lingua LanguageDetector instance with default (high) accuracy.
    """
    codes: set[str] = set(target_languages or []) | _COMMON_SOURCE_CODES

    lingua_langs: list[Language] = []
    for code in codes:
        lang = _CODE_TO_LANG.get(code)
        if lang:
            lingua_langs.append(lang)

    if not lingua_langs:
        # Fallback: use all spoken languages
        return LanguageDetectorBuilder.from_all_spoken_languages().build()

    return LanguageDetectorBuilder.from_languages(*lingua_langs).build()
# #endregion build_detector


# #region get_detector [C:2] [TYPE Function] [SEMANTICS language, detection, cache]
# @BRIEF Get or create a cached detector for the given target languages.
#         Cache keyed by sorted set of language codes — same key returns same instance.
def get_detector(target_languages: list[str] | None = None) -> LanguageDetector:
    """Get or create a cached detector for the given target languages."""
    key = _detector_cache_key(target_languages)
    detector = _detector_cache.get(key)
    if detector is None:
        detector = build_detector(target_languages)
        _detector_cache[key] = detector
    return detector
# #endregion get_detector


# #region detect_language [C:2] [TYPE Function] [SEMANTICS language, detection]
# @BRIEF Detect the language of a single text string. Returns BCP-47 code or "und".
# @RAISES Does not raise — all exceptions caught internally and return "und".
def detect_language(text: str, detector: LanguageDetector) -> str:
    """Detect the language of a text string using the given detector.

    Args:
        text: The source text to detect language for.
        detector: A lingua LanguageDetector instance (from build_detector/get_detector).

    Returns:
        BCP-47 language code (e.g. "en", "ru") or "und" if uncertain/empty.
    """
    if not text or not text.strip():
        return "und"

    try:
        result = detector.detect_language_of(text)
        if result is None:
            return "und"
        iso = result.iso_code_639_1
        return iso.name.lower()
    except Exception:
        return "und"
# #endregion detect_language


# #region _character_block_fallback [C:2] [TYPE Function] [SEMANTICS language, heuristic, cyrillic]
# @BRIEF When lingua returns "und", check if character-block dominance (e.g. Cyrillic)
#         suggests a match to one of the target languages. Catches texts with many numbers
#         and punctuation where n-gram-based detection is unreliable.
# @RATIONALE Russian texts with numbers/punctuation (e.g. "По договору поставки. в объёме
#            67 890 123,45 руб..") frequently get "und" from lingua because digits dilute
#            the n-gram signal. A simple letter-block ratio catches these reliably.
# @REJECTED Relying solely on lingua for all cases — too many false "und" on number-heavy texts.
#            Using LLM for re-detection — defeats the purpose of local detection.
def _character_block_fallback(
    texts: list[str],
    results: list[str],
    target_languages: list[str] | None,
) -> list[str]:
    """Apply character-block heuristics for 'und' results.

    For each 'und' result, if the target list includes a Cyrillic language (ru/uk/be)
    and the text is Cyrillic-dominant (≥35% of letter chars), assign that language.
    """
    if not target_languages:
        return results

    tls_lower = [t.lower() for t in target_languages]
    cyrillic_targets = [t for t in tls_lower if t in ("ru", "uk", "be")]
    if not cyrillic_targets:
        return results  # no Cyrillic target → nothing to do

    for i, (text, result) in enumerate(zip(texts, results)):
        if result != "und" or not text:
            continue

        ascii_letters = len(_ASCII_LETTER_RE.findall(text))
        cyrillic_letters = len(_CYRILLIC_RE.findall(text))
        total_letters = ascii_letters + cyrillic_letters

        if total_letters > 0 and cyrillic_letters / total_letters >= 0.35:
            results[i] = cyrillic_targets[0]

    return results
# #endregion _character_block_fallback


# #region batch_detect [C:3] [TYPE Function] [SEMANTICS language, detection, batch]
# @BRIEF Detect language for multiple texts in batch. Builds detector if not provided.
# @RELATION DEPENDS_ON -> [detect_language]
# @RELATION DEPENDS_ON -> [get_detector]
# @RELATION DEPENDS_ON -> [_character_block_fallback]
def batch_detect(
    texts: list[str],
    target_languages: list[str] | None = None,
    detector: LanguageDetector | None = None,
) -> list[str]:
    """Detect language for multiple texts in batch.

    Args:
        texts: List of source text strings.
        target_languages: Target languages for detector building (used if detector is None).
        detector: Pre-built detector (reused across batches). If None, built from target_languages.

    Returns:
        List of BCP-47 language codes (one per input text), or "und" for uncertain.
    """
    if detector is None:
        detector = get_detector(target_languages)

    results = [detect_language(t, detector) for t in texts]
    results = _character_block_fallback(texts, results, target_languages)
    return results
# #endregion batch_detect
# #endregion LanguageDetectService
