"""Combined: all promising features together, plus individual combos.

Tests the full proposed score() function and intermediate combos.
"""

import re
import unicodedata
from rapidfuzz import fuzz
from rapidfuzz.distance import JaroWinkler
from harness import evaluate

PUNCT_RE = re.compile(r'^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$')

def strip_punct(t):
    return PUNCT_RE.sub('', t)

MULTI_RULES = [("rn","m"),("nn","m"),("ri","n"),("cl","d"),("vv","w"),("ii","u")]
SHAPE = {}
for ch in "il1I|!tfj": SHAPE[ch] = "I"
for ch in "oO0cCeQa": SHAPE[ch] = "O"
for ch in "mwMW": SHAPE[ch] = "M"
for ch in "nuvNUV": SHAPE[ch] = "N"
for ch in "bhk6": SHAPE[ch] = "B"
for ch in "pqgy9": SHAPE[ch] = "P"
for ch in "dDPRB": SHAPE[ch] = "D"
for ch in "xXzZK": SHAPE[ch] = "X"
for ch in "sS5$": SHAPE[ch] = "S"
for ch in "-_~=": SHAPE[ch] = "H"
for ch in "234": SHAPE[ch] = "2"
for ch in "78": SHAPE[ch] = "7"
SHAPE["r"] = "R"

def shape_compress(t):
    t = unicodedata.normalize("NFKC", t).lower()
    for p, r in MULTI_RULES:
        t = t.replace(p, r)
    return "".join(SHAPE.get(c, c) for c in t)

def shape_simple(t):
    """Shape without multi-char rules (for short words)."""
    t = unicodedata.normalize("NFKC", t).lower()
    return "".join(SHAPE.get(c, c) for c in t)

def norm(t):
    return strip_punct(unicodedata.normalize("NFKC", t)).lower()

def length_ratio(a, b):
    la, lb = len(a), len(b)
    if la == 0 or lb == 0:
        return 0.0
    return min(la, lb) / max(la, lb)

def bigrams(s):
    return set(s[i:i+2] for i in range(len(s) - 1))

def dice(a, b):
    if not a and not b:
        return 1.0
    if not a or not b:
        return 0.0
    return 2.0 * len(a & b) / (len(a) + len(b))


# --- Combo variants ---

def score_A_max_jw_rev_shape(query, candidate):
    """Max(JW, RevJW, Shape_Lev) * len — three-way max."""
    q, c = norm(query), norm(candidate)
    if not q or not c:
        return 0.0
    if len(q) <= 3:
        return 0.95 if shape_simple(q) == shape_simple(c) else 0.0
    jw = JaroWinkler.similarity(q, c)
    rev_jw = JaroWinkler.similarity(q[::-1], c[::-1])
    shape_lev = fuzz.ratio(shape_compress(q), shape_compress(c)) / 100.0
    lr = length_ratio(q, c)
    return max(jw, rev_jw, shape_lev) * (0.8 + 0.2 * lr)


def score_B_max_all_four(query, candidate):
    """Max(JW, RevJW, Shape_Lev, ShapeBigramDice) * len — four features."""
    q, c = norm(query), norm(candidate)
    if not q or not c:
        return 0.0
    if len(q) <= 3:
        return 0.95 if shape_simple(q) == shape_simple(c) else 0.0
    jw = JaroWinkler.similarity(q, c)
    rev_jw = JaroWinkler.similarity(q[::-1], c[::-1])
    sq, sc = shape_compress(q), shape_compress(c)
    shape_lev = fuzz.ratio(sq, sc) / 100.0
    shape_dice = dice(bigrams(sq), bigrams(sc)) if len(sq) >= 2 and len(sc) >= 2 else 0.0
    lr = length_ratio(q, c)
    return max(jw, rev_jw, shape_lev, shape_dice) * (0.8 + 0.2 * lr)


def score_C_jw_rev_only_len(query, candidate):
    """Max(JW, RevJW) * len — no shape, just fixing prefix bias."""
    q, c = norm(query), norm(candidate)
    if not q or not c:
        return 0.0
    if len(q) <= 3:
        return 0.95 if q == c else 0.0
    jw = JaroWinkler.similarity(q, c)
    rev_jw = JaroWinkler.similarity(q[::-1], c[::-1])
    lr = length_ratio(q, c)
    return max(jw, rev_jw) * (0.8 + 0.2 * lr)


def score_D_blend_with_rev(query, candidate):
    """Blend: .3*JW + .15*RevJW + .35*Shape + .2*Len."""
    q, c = norm(query), norm(candidate)
    if not q or not c:
        return 0.0
    if len(q) <= 3:
        return 0.95 if shape_simple(q) == shape_simple(c) else 0.0
    jw = JaroWinkler.similarity(q, c)
    rev_jw = JaroWinkler.similarity(q[::-1], c[::-1])
    shape_lev = fuzz.ratio(shape_compress(q), shape_compress(c)) / 100.0
    lr = length_ratio(q, c)
    return 0.3 * jw + 0.15 * rev_jw + 0.35 * shape_lev + 0.2 * lr


if __name__ == "__main__":
    evaluate(score_A_max_jw_rev_shape, name="20A - Max(JW,RevJW,Shape)*len + short guard")
    evaluate(score_B_max_all_four, name="20B - Max(JW,RevJW,Shape,Dice)*len + short guard")
    evaluate(score_C_jw_rev_only_len, name="20C - Max(JW,RevJW)*len + short guard (no shape)")
    evaluate(score_D_blend_with_rev, name="20D - Blend(.3JW+.15Rev+.35Shape+.2Len) + short guard")
