"""Phonetic Pattern Validators.

Each validator function takes a word and a claimed pattern string,
returns (is_valid: bool, message: str).

Validators catch structural mismatches BEFORE checking the word database.
A word can pass structural validation but still be wrong semantically
(e.g., 'notice' matches soft_c_ci structurally but the vowel claim was wrong in W2).
"""

from typing import Tuple

__all__ = ["validate_pattern", "ALL_VALIDATORS"]

# Registry: pattern_keyword -> validator_function
_ALL_VALIDATORS: dict = {}


def _register(patterns: list):
    def decorator(func):
        for p in patterns:
            _ALL_VALIDATORS[p] = func
        return func
    return decorator


def validate_pattern(word: str, claimed_pattern: str) -> Tuple[bool, str]:
    """Validate a word against its claimed pattern.
    
    Returns (valid, message). Message explains the issue if invalid.
    """
    import re as _re
    word = word.lower().strip()
    pattern = claimed_pattern.lower().strip()
    
    # Try most-specific patterns first to avoid substring collisions
    sorted_validators = sorted(_ALL_VALIDATORS.items(), 
                               key=lambda x: -len(x[0]))  # longest keys first
    
    for key, func in sorted_validators:
        # For short keys (2-3 chars), require word boundary to avoid false matches
        # e.g., "or" shouldn't match inside "short", "ar" shouldn't match in "farmer"
        if len(key) <= 3:
            if _re.search(r'\b' + _re.escape(key) + r'\b', pattern):
                return func(word, pattern)
        else:
            if key in pattern:
                return func(word, pattern)
    
    # Unknown pattern — flag for review
    return True, f"[REVIEW] Unknown pattern type: {claimed_pattern!r}"


# ---------------------------------------------------------------------------
# Short vowel validators
# ---------------------------------------------------------------------------
@_register(["short_a", "short_e", "short_i", "short_o", "short_u"])
def validate_short_vowel(word: str, claimed: str) -> Tuple[bool, str]:
    """Check that a word claiming a short vowel actually has one.
    
    Rules:
    - Word must have at least 2 letters
    - The stressed syllable should contain the claimed short vowel
    - CVC or CVCC patterns are typical (closed syllable)
    """
    target = claimed.split("_")[1]  # 'a', 'e', 'i', 'o', 'u'
    
    if len(word) < 2:
        return False, f"'{word}' is too short for any vowel pattern"
    
    # The vowel should appear in the word
    if target not in word:
        return False, f"'{word}' claims short {target} but has no '{target}' in it"
    
    # Check for CVCe pattern that would make it LONG
    # If word ends in consonant + e and has single vowel before consonant, it's likely long
    if len(word) >= 3 and word[-1] == 'e' and word[-2] != 'e':
        # Check if it's a CVCe pattern (single vowel + consonant + e)
        non_e = word[:-1]  # remove trailing e
        if len(non_e) == 3 and non_e[1] == target and non_e[2] != target:
            return False, (
                f"'{word}' has CVCe structure ({non_e[0]}-{target}-{non_e[2]}-e) "
                f"which makes long {target}, not short {target}"
            )
    
    return True, f"OK: '{word}' contains short {target}"


# ---------------------------------------------------------------------------
# Silent E (CVCe / long vowel + e) validators
# ---------------------------------------------------------------------------
@_register(["long_a_a_e", "long_e_e_e", "long_i_i_e", "long_o_o_e", "long_u_u_e",
            "silent_e", "long_a", "long_e", "long_i", "long_o", "long_u"])
def validate_silent_e(word: str, claimed: str) -> Tuple[bool, str]:
    """Validate Silent E (Magic E) pattern.
    
    Rules:
    - Word must end in 'e'
    - Word must have exactly one vowel before the final consonant(s)
    - The vowel must match the claimed long vowel
    - Minimum 4 letters (CVCe)
    - The pattern is: VOWEL + consonant + E
    """
    # Extract claimed vowel
    vowel_map = {
        "long_a": "a", "a_e": "a",
        "long_e": "e", "e_e": "e",
        "long_i": "i", "i_e": "i",
        "long_o": "o", "o_e": "o",
        "long_u": "u", "u_e": "u",
    }
    
    target_vowel = None
    for key, val in vowel_map.items():
        if key in claimed:
            target_vowel = val
            break
    
    # Special cases — check BEFORE structural validation
    exceptions = {
        "strange": True,   # str-a-ng-e (ng digraph)
        "change": True,    # ch-a-ng-e
        "orange": True,    # or-a-ng-e
        # Multi-syllable words taught as CVCe patterns (final -e makes vowel long)
        "rescue": True,    # re-scue (ue → /juː/)
        "invite": True,    # in-vite (i_e pattern)
        "relieve": True,   # re-lieve (ie team)
        "divide": True,    # di-vide (i_e pattern)
    }
    if word in exceptions:
        return True, f"OK: '{word}' is a valid silent_e exception"
    
    if not target_vowel:
        # Generic "silent_e" or "long X (silent e)" claim
        # Try to extract from "(silent e)" pattern
        return True, f"[REVIEW] Generic silent_e claim for '{word}': {claimed!r}"
    
    if not word.endswith('e'):
        return False, f"'{word}' claims silent_e/{target_vowel}_e but doesn't end in 'e'"
    
    if len(word) < 4:
        return False, f"'{word}' has {len(word)} letters — too short for CVCe pattern (need 4+)"
    
    # Check CVCe structure: the vowel should appear before exactly one consonant before final 'e'
    before_e = word[:-1]  # everything before final e
    
    # Count vowels in before_e
    vowels_in = sum(1 for c in before_e if c in 'aeiou')
    
    # The target vowel should appear exactly once in before_e
    target_count = before_e.count(target_vowel)
    
    if target_count == 0:
        return False, f"'{word}' claims long {target_vowel} + silent e but '{target_vowel}' not found"
    
    if target_count > 1:
        return False, f"'{word}' has {target_count} '{target_vowel}'s — confusing for silent_e pattern"
    
    # The vowel must come before the final consonant (not be the last letter before e)
    vowel_pos = before_e.index(target_vowel)
    if vowel_pos == len(before_e) - 1:
        # Vowel is directly before the e — not CVCe, this is just Vowel+E
        return False, f"'{word}' has '{target_vowel}' directly before final 'e' — not CVCe pattern"
    
    # Check that there's exactly one consonant between vowel and final e (for strict CVCe)
    # Allow for patterns like 'strange' (str-a-n-g-e) where there are 2 consonants
    consonants_after = before_e[vowel_pos + 1:]
    if not consonants_after:
        return False, f"'{word}' has no consonant between vowel and final 'e'"
    
    return True, f"OK: '{word}' matches long {target_vowel} + silent e"


# ---------------------------------------------------------------------------
# Vowel team validators
# ---------------------------------------------------------------------------
@_register(["long_a_ai", "long_a_ay", "long_e_ea", "long_e_ee",
            "long_e_ew", "long_i_ee", "long_i_igh", "long_o_oe", "long_o_oa",
            "long_o_ou", "long_o_ow", "long_u_ew", "long_u_oo",
            "short_o_ow", "short_e_ew", "vowel_team"])
def validate_vowel_team(word: str, claimed: str) -> Tuple[bool, str]:
    """Validate vowel team patterns.
    
    Rules:
    - The claimed digraph/team must appear in the word
    - Examples: ai, ay, ea, ee, ie, igh, oe, oa, oo, ou, ow, ew
    """
    teams = {
        "long_a_ai": "ai",
        "long_a_ay": "ay",
        "long_e_ea": "ea",
        "long_e_ee": "ee",
        "long_e_ew": "ew",
        "long_i_ee": "ie",
        "long_i_igh": "igh",
        "long_o_oe": "oe",
        "long_o_oa": "oa",
        "long_o_ou": "ou",
        "long_o_ow": "ow",
        "long_u_ew": "ew",
        "long_u_oo": "oo",
        "short_e_ew": "ew",
        "short_o_ow": "ow",
        "vowel_team_oa": "oa",
        "vowel_team_ow": "ow",
        "vowel_team_ou": "ou",
        "vowel_team_ew": "ew",
    }
    
    team = None
    for key, val in teams.items():
        if key in claimed:
            team = val
            break
    
    if not team:
        return True, f"[REVIEW] Generic vowel_team claim for '{word}': {claimed!r}"
    
    if team not in word:
        return False, f"'{word}' claims {team} pattern but '{team}' not found in word"
    
    return True, f"OK: '{word}' contains {team} team"


# ---------------------------------------------------------------------------
# Digraph validators
# ---------------------------------------------------------------------------
@_register(["digraph_sh", "digraph_ch", "digraph_th", "digraph_wh",
            "digraph_ph", "digraph_ck", "sh sound", "ch sound",
            "th sound", "wh sound", "ph sound", "ck sound"])
def validate_digraph(word: str, claimed: str) -> Tuple[bool, str]:
    """Validate digraph patterns.
    
    Rules:
    - The claimed digraph must appear in the word
    """
    digraph_map = {
        "digraph_sh": "sh", "sh sound": "sh",
        "digraph_ch": "ch", "ch sound": "ch",
        "digraph_th": "th", "th sound": "th",
        "digraph_wh": "wh", "wh sound": "wh",
        "digraph_ph": "ph", "ph sound": "ph",
        "digraph_ck": "ck", "ck sound": "ck",
    }
    
    digraph = None
    for key, val in digraph_map.items():
        if key in claimed:
            digraph = val
            break
    
    if not digraph:
        return True, f"[REVIEW] Generic digraph claim for '{word}': {claimed!r}"
    
    if digraph not in word:
        return False, f"'{word}' claims {digraph} digraph but '{digraph}' not found in word"
    
    return True, f"OK: '{word}' contains {digraph} digraph"


# ---------------------------------------------------------------------------
# Soft C / Soft G validators
# ---------------------------------------------------------------------------
@_register(["soft_c", "soft_g"])
def validate_soft_cg(word: str, claimed: str) -> Tuple[bool, str]:
    """Validate soft C and soft G patterns.
    
    Rules:
    - Soft C: c before e, i, y makes /s/ sound
    - Soft G: g before e, i, y makes /dʒ/ sound
    - The letter must actually appear before a triggering vowel
    """
    is_c = "soft_c" in claimed
    letter = "c" if is_c else "g"
    triggers = ["e", "i", "y"]
    
    if letter not in word:
        return False, f"'{word}' claims soft {letter.upper()} but has no '{letter}'"
    
    # Find each occurrence of the letter and check what follows
    found_valid = False
    for idx, char in enumerate(word):
        if char == letter and idx + 1 < len(word):
            next_char = word[idx + 1]
            if next_char in triggers:
                found_valid = True
                break
            elif next_char == "a" and letter == "g":
                # g before a can be soft (garage, garment) — flag for review
                return True, f"[REVIEW] '{word}': g before 'a' — may be soft or hard, needs check"
    
    if not found_valid:
        return False, (
            f"'{word}' claims soft {letter.upper()} but '{letter}' is not before e/i/y "
            f"(need: {letter}e, {letter}i, or {letter}y)"
        )
    
    return True, f"OK: '{word}' has soft {letter.upper()} before triggering vowel"


# ---------------------------------------------------------------------------
# R-controlled vowel validators
# ---------------------------------------------------------------------------
@_register(["r_control", "ar", "er", "ir", "or", "ur"])
def validate_r_controlled(word: str, claimed: str) -> Tuple[bool, str]:
    """Validate R-controlled vowel patterns.
    
    Rules:
    - The claimed vowel+r combination must appear in the word
    - Examples: ar, er, ir, or, ur
    - Must be explicitly claimed (not matched as substring of other words)
    """
    import re as _re
    r_teams = ["ar", "er", "ir", "or", "ur"]
    
    # Find target from claimed pattern
    # Priority: standalone token (e.g., "ar" in "ar = /ar/") > phonetic in slashes
    target = None
    for team in r_teams:
        # Match standalone token first (e.g., "ar" at start of "ar = /ar/")
        if _re.search(r'\b' + team + r'\s*=', claimed):
            target = team
            break
    
    # If no "team =" found, try standalone token
    if not target:
        for team in r_teams:
            if _re.search(r'\b' + team + r'\b', claimed):
                target = team
                break
    
    # Last resort: check slashes (but this catches the phonetic, not the pattern)
    if not target:
        for team in r_teams:
            if f"/{team}/" in claimed:
                target = team
                break
    
    if not target:
        return True, f"[REVIEW] Generic r_control claim for '{word}': {claimed!r}"
    
    # Check if the r-team appears in the WORD (no word boundary — it's embedded)
    if target not in word:
        return False, f"'{word}' claims {target} pattern but '{target}' not found in word"
    
    return True, f"OK: '{word}' contains {target} r-controlled vowel"


# ---------------------------------------------------------------------------
# Double consonant validators
# ---------------------------------------------------------------------------
@_register(["double_consonant", "double"])
def validate_double_consonant(word: str, claimed: str) -> Tuple[bool, str]:
    """Validate double consonant patterns.
    
    Rules:
    - The word must contain at least one double consonant
    - The double consonant typically follows a short vowel
    """
    has_double = False
    for i in range(len(word) - 1):
        if word[i] == word[i + 1] and word[i].isalpha() and word[i] not in 'aeiou':
            has_double = True
            break
    
    if not has_double:
        return False, f"'{word}' claims double consonant but has no double consonants"
    
    return True, f"OK: '{word}' has double consonant"


# ---------------------------------------------------------------------------
# Irregular word validators
# ---------------------------------------------------------------------------
@_register(["irregular"])
def validate_irregular(word: str, claimed: str) -> Tuple[bool, str]:
    """Irregular words can't be structurally validated — they're flagged for review.
    
    These words don't follow standard phonetic rules and must be verified
    against the word database.
    """
    return True, f"[REVIEW] '{word}' is claimed as irregular — verify against database"


# ---------------------------------------------------------------------------
# Multiple meaning word validators
# ---------------------------------------------------------------------------
@_register(["multiple_meaning"])
def validate_multiple_meaning(word: str, claimed: str) -> Tuple[bool, str]:
    """Multiple meaning words can't be structurally validated — they're flagged for review.
    
    These words have two or more distinct meanings (e.g., bat = animal / sports equipment).
    Validation requires checking the word database for documented meanings.
    """
    return True, f"[REVIEW] '{word}' is claimed as multiple_meaning — verify definitions in database"


# ---------------------------------------------------------------------------
# Suffix validators (-s, -es, -ed, -ing, -er, -est, -ly, -ful, -less, etc.)
# ---------------------------------------------------------------------------
@_register(["prefix_un", "prefix_re", "prefix_dis", "prefix_im", "prefix_in", "prefix_mis",
            "morphological"])
def validate_morphological(word: str, claimed: str) -> Tuple[bool, str]:
    """Validate prefix/suffix patterns.

    Rules:
    - prefix_un: word ends in -un, base exists (un + happy)
    - prefix_re: word starts with re- (re + do)
    - prefix_dis: word starts with dis- (dis + agree)
    - prefix_im: word starts with im- (im + possible)
    - prefix_in: word starts with in- (in + visible)
    - prefix_mis: word starts with mis- (mis + lead)
    """
    word = word.lower()
    prefix_map = {
        "prefix_un": "un",
        "prefix_re": "re",
        "prefix_dis": "dis",
        "prefix_im": "im",
        "prefix_in": "in",
        "prefix_mis": "mis",
    }
    if claimed not in prefix_map:
        return True, f"morphological pattern '{claimed}' (manual check)"

    expected = prefix_map[claimed]
    if not word.startswith(expected):
        return False, f"{word} doesn't start with '{expected}'"

    return True, f"prefix: {claimed} ({expected} + {word[len(expected):]})"


@_register(["suffix_s", "suffix_es", "suffix_ed", "suffix_ing", "suffix_er", "suffix_est",
            "suffix_ly", "suffix_ful", "suffix_less", "suffix_ment", "suffix_ion",
            "suffix_able", "suffix_ible", "suffix_ive", "suffix_ous", "suffix_ious", "suffix_ent",
            "suffix_ness", "suffix_ure", "suffix_y", "suffix_ile"])
def _validate_suffix_er_est_ly_ful_less_ment_ion_able_ive_ous_ious_ent_ness_ure_y_ile(word, pattern):
    """Validate suffix patterns.

    Rules:
    - Word must contain the claimed suffix
    - Suffix: must appear at the end
    """
    if "suffix" in pattern:
        # Extract suffix name (e.g., "er" from "suffix_er")
        suffix_name = pattern.split("_")[-1] if "_" in pattern else ""
        if not suffix_name or not word.endswith(suffix_name):
            return False, f"'{word}' claims suffix '{suffix_name}' but doesn't end with it"
        return True, f"OK: '{word}' ends with suffix '{suffix_name}'"

    return True, f"[REVIEW] Generic morphological claim for '{word}': {pattern!r}"


# ---------------------------------------------------------------------------
# Homophone validators
# ---------------------------------------------------------------------------
@_register(["homophone"])
def validate_homophone(word: str, claimed: str) -> Tuple[bool, str]:
    """Homophones can't be structurally validated — they're flagged for review.
    
    These words sound the same but have different spellings/meanings.
    Validation requires checking the word database for matching pairs.
    """
    return True, f"[REVIEW] '{word}' is claimed as homophone — verify pair in database"


# ---------------------------------------------------------------------------
# IE rule validators (i before e / except after c)
# ---------------------------------------------------------------------------
@_register(["ie_rule"])
def validate_ie_rule(word: str, claimed: str) -> Tuple[bool, str]:
    """Validate 'i before e' rule patterns.
    
    Rules:
    - 'ie' should appear in the word (standard rule)
    - 'ei' should appear after 'c' (exception rule)
    """
    # Check for "ei" exceptions first (e.g., receive, ceiling, weird)
    if "ei" in word:
        ei_pos = word.find("ei")
        if ei_pos > 0 and word[ei_pos - 1] == "c":
            return True, f"OK: '{word}' has 'ei' after 'c' — except after c rule"
        return True, f"[REVIEW] '{word}' has 'ei' — ie_rule exception"
    
    # Standard "ie" rule
    if "ie" in claimed or "i before e" in claimed:
        if "ie" not in word:
            return False, f"'{word}' claims 'ie' rule but has no 'ie' in it"
        return True, f"OK: '{word}' has 'ie' — i before e rule"
    elif "ei" in claimed or "except after c" in claimed:
        if "ei" not in word:
            return False, f"'{word}' claims 'ei' exception but has no 'ei' in it"
        ei_pos = word.find("ei")
        if ei_pos > 0 and word[ei_pos - 1] == "c":
            return True, f"OK: '{word}' has 'ei' after 'c' — except after c rule"
        return True, f"[REVIEW] '{word}' has 'ei' but not obviously after 'c'"
    return True, f"[REVIEW] Generic ie_rule claim for '{word}': {claimed!r}"