"""Arabic text normalization for Quran validation.

Ports the logic from arabic-text-normalizer (npm) + normalizer.ts inline,
so this package has zero runtime dependencies.
"""

from __future__ import annotations

import re
import unicodedata
from dataclasses import dataclass


@dataclass
class NormalizeOptions:
    """Options for Arabic text normalization."""
    diacritics: bool = True
    markers: bool = True
    verse_numbers: bool = True
    tatweel: bool = True
    small_letters: bool = True
    punctuation: bool = True
    collapse_whitespace: bool = True
    strip_hamza: bool = False


# --- Regex patterns ported from arabic-text-normalizer ---

# Arabic tashkeel/harakat: U+064B-U+065F (includes shadda U+0651)
_DIACRITICS = re.compile(r"[\u064B-\u065F]")

# Alif with madda above (آ U+0622) -> plain alif (ا U+0627)
_ALIF_MADDA = re.compile(r"\u0622")

# Alif wasla (ٱ U+0671) -> plain alif (ا U+0627)
_ALIF_WASLA = re.compile(r"\u0671")

# Alif variants: U+0672, U+0673 -> plain alif
_ALIF_VARIANTS = re.compile(r"[\u0672\u0673]")

# Superscript alif preceded by regular alif (اٰ -> ا)
_ALIF_SUPERSCRIPT_ALIF = re.compile(r"\u0627\u0670")

# Remaining superscript alif (ٰ U+0670) -> plain alif
_SUPERSCRIPT_ALIF = re.compile(r"\u0670")

# Farsi/Urdu yeh variants -> Arabic yeh (ي U+064A)
_FARSI_YEH = re.compile(r"[\u06CC\u06D2]")

# Farsi/Urdu kaf (ک U+06A9) -> Arabic kaf (ك U+0643)
_FARSI_KAF = re.compile(r"\u06A9")

# Quranic annotation marks: U+06D6-U+06ED
_QURANIC_ANNOTATIONS = re.compile(r"[\u06D6-\u06ED]")

# Tatweel/kashida: U+0640
_TATWEEL = re.compile(r"\u0640")

# Ornate parentheses: U+FD3E, U+FD3F
_ORNATE_PARENS = re.compile(r"[\uFD3E\uFD3F]")

# Arabic-Indic digits + Extended Arabic-Indic digits
_ARABIC_DIGITS = re.compile(r"[\u0660-\u0669\u06F0-\u06F9]")

# Punctuation
_PUNCTUATION = re.compile(r"[.,;:!?…\u060C\u061B\u061F]")

# Multiple whitespace
_MULTI_WHITESPACE = re.compile(r"\s+")

# Hamza forms to strip (أ إ ئ ء) — ؤ is preserved
_HAMZA_TO_STRIP = re.compile(r"[\u0621\u0623\u0625\u0626]")

# Alef maqsura (ى U+0649) -> ya (ي U+064A)
_ALEF_MAQSURA = re.compile(r"\u0649")

# Uthmani spelling: وة or واة → اة
_UTHMANI_WAW_TA = re.compile(r"وا?ة")

# Double ya → single ya
_DOUBLE_YA = re.compile(r"يي")

# Sad/Sin orthographic variants
_SAD_SIN_VARIANTS: list[tuple[re.Pattern[str], str]] = [
    (re.compile(r"بصط"), "بسط"),
    (re.compile(r"صيطر"), "سيطر"),
]

# Definite-article double-lam collapse
_DOUBLE_LAM = re.compile(r"(ا)لل")

# Bidi / zero-width control characters
_BIDI_CONTROLS = re.compile(r"[\u200C\u200D\u200E\u200F\u061C]")

# Arabic character detection
_ARABIC_CHAR = re.compile(
    r"[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]"
)

# Arabic segment extraction
_ARABIC_SEGMENT = re.compile(
    r"[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]"
    r"[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF\s]*"
)


def _normalize_core(text: str, options: NormalizeOptions | None = None) -> str:
    """Core normalization logic ported from arabic-text-normalizer."""
    opts = options or NormalizeOptions()
    result = text

    if opts.diacritics:
        result = _DIACRITICS.sub("", result)
        result = _ALIF_MADDA.sub("\u0627", result)  # آ -> ا
        result = _ALIF_WASLA.sub("\u0627", result)   # ٱ -> ا
        result = _ALIF_VARIANTS.sub("\u0627", result) # ٲ ٳ -> ا
        # Strip superscript alif when preceded by regular alif
        result = _ALIF_SUPERSCRIPT_ALIF.sub("\u0627", result)  # اٰ -> ا
        result = _SUPERSCRIPT_ALIF.sub("\u0627", result)  # remaining ٰ -> ا
        result = _FARSI_YEH.sub("\u064A", result)  # ی ے -> ي
        result = _FARSI_KAF.sub("\u0643", result)   # ک -> ك

    if opts.markers or opts.small_letters:
        result = _QURANIC_ANNOTATIONS.sub("", result)

    if opts.verse_numbers:
        result = _ORNATE_PARENS.sub("", result)
        result = _ARABIC_DIGITS.sub("", result)

    if opts.tatweel:
        result = _TATWEEL.sub("", result)

    if opts.punctuation:
        result = _PUNCTUATION.sub("", result)

    if opts.strip_hamza:
        result = _HAMZA_TO_STRIP.sub("", result)
        result = _ALEF_MAQSURA.sub("\u064A", result)  # ى -> ي
        result = _UTHMANI_WAW_TA.sub("اة", result)
        result = _DOUBLE_YA.sub("ي", result)
        for pattern, replacement in _SAD_SIN_VARIANTS:
            result = pattern.sub(replacement, result)
        result = _DOUBLE_LAM.sub(r"\1ل", result)

    if opts.collapse_whitespace:
        result = _MULTI_WHITESPACE.sub(" ", result).strip()

    return result


def normalize_arabic(text: str, options: NormalizeOptions | None = None) -> str:
    """Normalize Arabic text for comparison.

    Uses NFKC decomposition, bidi control stripping, and configurable
    normalization rules.

    Args:
        text: The Arabic text to normalize.
        options: Normalization options.

    Returns:
        Normalized text.
    """
    # NFKC decomposes presentation forms (e.g., ﷲ → الله)
    result = unicodedata.normalize("NFKC", text)
    # Strip bidi and zero-width controls
    result = _BIDI_CONTROLS.sub("", result)
    return _normalize_core(result, options)


def remove_diacritics(text: str) -> str:
    """Remove only diacritics (tashkeel) from Arabic text.

    Preserves base letters but removes vowel marks, shadda, sukun,
    and other diacritical marks.
    """
    return _normalize_core(text, NormalizeOptions(
        diacritics=True,
        markers=False,
        verse_numbers=False,
        tatweel=False,
        small_letters=False,
        punctuation=False,
        collapse_whitespace=False,
    ))


def contains_arabic(text: str) -> bool:
    """Check if text contains Arabic characters."""
    return bool(_ARABIC_CHAR.search(text))


@dataclass
class _Segment:
    text: str
    start_index: int
    end_index: int


def extract_arabic_segments(text: str) -> list[_Segment]:
    """Extract Arabic text segments from mixed text.

    Args:
        text: Text that may contain Arabic and non-Arabic content.

    Returns:
        List of segments with text and position info.
    """
    segments: list[_Segment] = []
    for match in _ARABIC_SEGMENT.finditer(text):
        segment = match.group(0).strip()
        if segment:
            segments.append(_Segment(
                text=segment,
                start_index=match.start(),
                end_index=match.end(),
            ))
    return segments


def calculate_similarity(str1: str, str2: str) -> float:
    """Calculate similarity between two strings using Levenshtein distance.

    Returns:
        Similarity score between 0 and 1 (1 = identical).
    """
    if str1 == str2:
        return 1.0
    if not str1 or not str2:
        return 0.0

    distance = _levenshtein_distance(str1, str2)
    max_length = max(len(str1), len(str2))
    return 1.0 - distance / max_length


def _levenshtein_distance(str1: str, str2: str) -> int:
    m, n = len(str1), len(str2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]

    for i in range(m + 1):
        dp[i][0] = i
    for j in range(n + 1):
        dp[0][j] = j

    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if str1[i - 1] == str2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1]
            else:
                dp[i][j] = min(
                    dp[i - 1][j] + 1,
                    dp[i][j - 1] + 1,
                    dp[i - 1][j - 1] + 1,
                )
    return dp[m][n]


@dataclass
class Difference:
    input: str
    correct: str
    position: int


def find_differences(input_text: str, correct: str) -> list[Difference]:
    """Find differences between two strings.

    Args:
        input_text: The input string.
        correct: The correct string.

    Returns:
        List of differences with positions.
    """
    differences: list[Difference] = []
    min_length = min(len(input_text), len(correct))
    diff_start = -1
    input_chunk = ""
    correct_chunk = ""

    for i in range(min_length):
        if input_text[i] != correct[i]:
            if diff_start == -1:
                diff_start = i
            input_chunk += input_text[i]
            correct_chunk += correct[i]
        elif diff_start != -1:
            differences.append(Difference(
                input=input_chunk,
                correct=correct_chunk,
                position=diff_start,
            ))
            diff_start = -1
            input_chunk = ""
            correct_chunk = ""

    if diff_start != -1 or len(input_text) != len(correct):
        if diff_start == -1:
            diff_start = min_length
        input_chunk += input_text[diff_start + len(input_chunk):]
        correct_chunk += correct[diff_start + len(correct_chunk):]

        if input_chunk or correct_chunk:
            differences.append(Difference(
                input=input_chunk or "(missing)",
                correct=correct_chunk or "(extra)",
                position=diff_start,
            ))

    return differences
