"""
Fuzzy matching checker: loose/relaxed detection of 42.

Handles:
- Digits with spaces: "4  2" → 42
- Mixed full/half-width: "4２", "４2"
- Chinese with spaces: "四 十 二"
- Mixed language: "4十2", "4ty2", "四2"
- Digits with decorative separators stripped
"""

import re
import unicodedata


# Maps for digit-like characters → their numeric value
def _char_to_digit(ch):
    """Try to get a digit value (0-9) from various scripts."""
    # ASCII digits
    if "0" <= ch <= "9":
        return int(ch)
    # Full-width digits ０-９
    if "\uff10" <= ch <= "\uff19":
        return ord(ch) - 0xff10
    # Unicode digit
    d = unicodedata.digit(ch, -1)
    if d >= 0:
        return d
    return None


# Chinese/CJK number words
_CJK_DIGITS = {
    "零": 0, "〇": 0,
    "一": 1, "壹": 1, "壱": 1,
    "二": 2, "贰": 2, "貳": 2, "两": 2, "兩": 2,
    "三": 3, "叁": 3, "參": 3,
    "四": 4, "肆": 4,
    "五": 5, "伍": 5,
    "六": 6, "陆": 6, "陸": 6,
    "七": 7, "柒": 7,
    "八": 8, "捌": 8,
    "九": 9, "玖": 9,
}

_CJK_MULTIPLIERS = {
    "十": 10, "拾": 10, "什": 10,
    "百": 100, "佰": 100,
    "千": 1000, "仟": 1000,
}

# English fragments for fuzzy partial matching
_EN_FRAGMENTS = {
    "forty": 40, "fourty": 40,  # common misspelling
    "ty": None,   # suffix marker (used after a digit like 4ty2)
    "two": 2,
    "one": 1, "three": 3, "four": 4, "five": 5,
    "six": 6, "seven": 7, "eight": 8, "nine": 9,
    "ten": 10, "eleven": 11, "twelve": 12,
    "twenty": 20, "thirty": 30, "fifty": 50,
}


def _strip_decorative(s):
    """Remove spaces, hyphens, underscores, combining marks, and decorative chars."""
    result = []
    for ch in s:
        cat = unicodedata.category(ch)
        # Skip combining marks
        if cat.startswith("M"):
            continue
        # Skip whitespace, hyphens, underscores
        if ch in " \t\n\r_-\u00a0":
            continue
        # Skip zero-width / format chars
        if cat == "Cf":
            continue
        result.append(ch)
    return "".join(result)


def _try_pure_digits_with_spaces(s):
    """Try stripping spaces/decorations from digit-only strings: '4  2' → 42."""
    digits = []
    has_gap = False
    for ch in s:
        d = _char_to_digit(ch)
        if d is not None:
            digits.append(str(d))
        elif ch in " \t\u00a0_-·•":
            has_gap = True
            continue
        elif unicodedata.category(ch).startswith("M"):
            has_gap = True
            continue
        elif unicodedata.category(ch) == "Cf":
            has_gap = True
            continue
        else:
            return None  # non-digit, non-separator

    if not has_gap or not digits:
        return None

    try:
        val = int("".join(digits))
        if val == 42:
            return f"fuzzy digits: '{s.strip()}' → 42 (spaces/decorations stripped)"
    except ValueError:
        pass
    return None


def _try_cjk_with_spaces(s):
    """Try CJK numbers with spaces: '四 十 二' → 42."""
    cleaned = _strip_decorative(s)
    if not cleaned:
        return None

    # Check if it's all CJK number characters
    all_cjk = True
    for ch in cleaned:
        if ch not in _CJK_DIGITS and ch not in _CJK_MULTIPLIERS:
            all_cjk = False
            break

    if not all_cjk or cleaned == s.strip():
        return None  # no cleaning happened, or not all CJK

    # Parse CJK number
    val = _parse_cjk_number(cleaned)
    if val == 42:
        return f"fuzzy CJK: '{s.strip()}' → 四十二 = 42"
    return None


def _parse_cjk_number(s):
    """Parse a CJK number string to integer."""
    if not s:
        return None

    total = 0
    current = 0
    last_was_digit = False

    for ch in s:
        if ch in _CJK_DIGITS:
            current = _CJK_DIGITS[ch]
            last_was_digit = True
        elif ch in _CJK_MULTIPLIERS:
            mult = _CJK_MULTIPLIERS[ch]
            if last_was_digit:
                total += current * mult
            else:
                total += mult  # implicit 一十 = 十
            current = 0
            last_was_digit = False
        else:
            return None

    total += current  # add any trailing digit
    return total


def _try_mixed_language(s):
    """Try mixed-language number parsing: '4十2', '4ty2', '四2', etc."""
    cleaned = _strip_decorative(s).lower()
    if not cleaned or len(cleaned) < 2:
        return None

    # Tokenize: split into digit tokens, CJK tokens, and English tokens
    tokens = _tokenize_mixed(cleaned)
    if tokens is None or len(tokens) < 2:
        return None

    # Try to interpret as a number
    val = _eval_mixed_tokens(tokens)
    if val == 42:
        return f"fuzzy mixed: '{s.strip()}' → 42 (mixed numeral systems)"
    return None


def _tokenize_mixed(s):
    """Tokenize a mixed-language number string."""
    tokens = []
    i = 0
    while i < len(s):
        ch = s[i]

        # Digit (any script)
        d = _char_to_digit(ch)
        if d is not None:
            # Collect consecutive digits
            num_str = ""
            while i < len(s):
                d = _char_to_digit(s[i])
                if d is not None:
                    num_str += str(d)
                    i += 1
                else:
                    break
            tokens.append(("num", int(num_str)))
            continue

        # CJK digit or multiplier
        if ch in _CJK_DIGITS:
            tokens.append(("cjk_digit", _CJK_DIGITS[ch]))
            i += 1
            continue
        if ch in _CJK_MULTIPLIERS:
            tokens.append(("cjk_mult", _CJK_MULTIPLIERS[ch]))
            i += 1
            continue

        # English fragment
        matched = False
        for word, val in sorted(_EN_FRAGMENTS.items(), key=lambda x: -len(x[0])):
            if s[i:].startswith(word):
                if val is not None:
                    tokens.append(("en", val))
                else:
                    tokens.append(("en_suffix", word))
                i += len(word)
                matched = True
                break

        if not matched:
            return None  # unknown character

    return tokens


def _eval_mixed_tokens(tokens):
    """Evaluate a list of mixed tokens as a number.

    Handles patterns like:
    - num(4) + cjk_mult(10) + num(2) → 42
    - num(4) + en_suffix(ty) + num(2) → 42  (from "4ty2" = forty-two)
    - cjk_digit(4) + num(2) → 42
    - en(40) + num(2) → 42
    """
    if not tokens:
        return None

    # Pattern: digit + multiplier + digit → d * m + d
    if len(tokens) == 3:
        t0, t1, t2 = tokens
        # 4 十 2 → 4*10+2
        if t0[0] in ("num", "cjk_digit") and t1[0] == "cjk_mult" and t2[0] in ("num", "cjk_digit"):
            return t0[1] * t1[1] + t2[1]
        # 4ty2 → forty-two → 42
        if t0[0] == "num" and t1[0] == "en_suffix" and t1[1] == "ty" and t2[0] in ("num", "cjk_digit"):
            return t0[1] * 10 + t2[1]

    # Pattern: two tokens
    if len(tokens) == 2:
        t0, t1 = tokens
        # 4十 → 40 (digit + multiplier, no trailing digit)
        if t0[0] in ("num", "cjk_digit") and t1[0] == "cjk_mult":
            return t0[1] * t1[1]
        # forty + 2 or 四十 + 2
        if t0[0] == "en" and t1[0] in ("num", "cjk_digit"):
            return t0[1] + t1[1]
        # cjk_digit + num: 四2 → 42 (positional)
        if t0[0] == "cjk_digit" and t1[0] == "num" and t0[1] < 10 and t1[1] < 10:
            return t0[1] * 10 + t1[1]
        # num + cjk_digit: 4二 → 42 (positional)
        if t0[0] == "num" and t1[0] == "cjk_digit" and t0[1] < 10 and t1[1] < 10:
            return t0[1] * 10 + t1[1]

    # Simple addition: sum all values
    if len(tokens) >= 2 and all(t[0] in ("num", "en", "cjk_digit") for t in tokens):
        return sum(t[1] for t in tokens)

    return None


def check(value):
    """Check if value is a loosely-formatted representation of 42."""
    if not isinstance(value, str):
        return None

    s = value.strip()
    if not s:
        return None

    for checker in [_try_pure_digits_with_spaces, _try_cjk_with_spaces, _try_mixed_language]:
        result = checker(s)
        if result:
            return result

    return None
