"""False positive test on round 2 top performers."""

import csv
import re
import unicodedata
from pathlib import Path
from rapidfuzz import fuzz
from rapidfuzz.distance import JaroWinkler

DATA_DIR = Path(__file__).parent
CSV_PATH = DATA_DIR / "ocr_errors.csv"
OUTPUT_DIR = DATA_DIR / "output"
PUNCT_RE = re.compile(r'^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$')

def strip_punct(t): return PUNCT_RE.sub('', t)
def norm(t): return strip_punct(unicodedata.normalize("NFKC", t)).lower()
def length_ratio(a, b):
    la, lb = len(a), len(b)
    return min(la, lb) / max(la, lb) if la and lb else 0.0

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):
    t = unicodedata.normalize("NFKC", t).lower()
    return "".join(SHAPE.get(c, c) for c in t)

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))


# --- Algorithms ---

def score_max_jw_rev_len(q_raw, c_raw):
    """15c: Max(JW, RevJW) * len"""
    q, c = norm(q_raw), norm(c_raw)
    if not q or not c: return 0.0
    fwd = JaroWinkler.similarity(q, c)
    rev = JaroWinkler.similarity(q[::-1], c[::-1])
    lr = length_ratio(q, c)
    return max(fwd, rev) * (0.8 + 0.2 * lr)

def score_first_char_mask_len(q_raw, c_raw):
    """17b: Max(JW, JW_masked*0.98) * len"""
    q, c = norm(q_raw), norm(c_raw)
    if not q or not c: return 0.0
    jw = JaroWinkler.similarity(q, c)
    jw_m = JaroWinkler.similarity(q[1:], c[1:]) * 0.98 if len(q)>1 and len(c)>1 else 0.0
    lr = length_ratio(q, c)
    return max(jw, jw_m) * (0.8 + 0.2 * lr)

def score_20A(q_raw, c_raw):
    """20A: Max(JW, RevJW, Shape)*len + short guard"""
    q, c = norm(q_raw), norm(c_raw)
    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 = JaroWinkler.similarity(q[::-1], c[::-1])
    sl = fuzz.ratio(shape_compress(q), shape_compress(c)) / 100.0
    lr = length_ratio(q, c)
    return max(jw, rev, sl) * (0.8 + 0.2 * lr)

def score_20B(q_raw, c_raw):
    """20B: Max(JW, RevJW, Shape, Dice)*len + short guard"""
    q, c = norm(q_raw), norm(c_raw)
    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 = JaroWinkler.similarity(q[::-1], c[::-1])
    sq, sc = shape_compress(q), shape_compress(c)
    sl = fuzz.ratio(sq, sc) / 100.0
    sd = 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, sl, sd) * (0.8 + 0.2 * lr)

def score_20D(q_raw, c_raw):
    """20D: Blend(.3JW+.15Rev+.35Shape+.2Len) + short guard"""
    q, c = norm(q_raw), norm(c_raw)
    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 = JaroWinkler.similarity(q[::-1], c[::-1])
    sl = fuzz.ratio(shape_compress(q), shape_compress(c)) / 100.0
    lr = length_ratio(q, c)
    return 0.3*jw + 0.15*rev + 0.35*sl + 0.2*lr


ALGORITHMS = {
    "MaxJW+Rev*len": score_max_jw_rev_len,
    "CharMask*len": score_first_char_mask_len,
    "20A Max3*len+sg": score_20A,
    "20B Max4*len+sg": score_20B,
    "20D Blend+sg": score_20D,
}


def load_pairs():
    pairs = []
    with open(CSV_PATH) as f:
        for row in csv.DictReader(f):
            pairs.append((row["ocr_error"], row["correction"]))
    return pairs

def find_source_file(ocr_error):
    for txt_path in OUTPUT_DIR.glob("*.txt"):
        if ocr_error in txt_path.read_text(errors="replace"):
            return txt_path
    return None

def get_candidates(text, n_words):
    tokens = text.split()
    if n_words == 1: return tokens
    return [" ".join(tokens[i:i+n_words]) for i in range(len(tokens) - n_words + 1)]


def evaluate_fp(algo_name, score_fn):
    pairs = load_pairs()
    rank_1 = rank_top3 = total = 0
    failures = []

    for ocr_error, correction in pairs:
        src = find_source_file(ocr_error)
        if not src: continue
        text = src.read_text(errors="replace")
        n_words = len(correction.split())
        candidates = [c for c in get_candidates(text, n_words)
                      if strip_punct(c).lower() != strip_punct(correction).lower()]
        if not candidates: continue

        scored = sorted([(score_fn(correction, c), c) for c in candidates],
                        key=lambda x: x[0], reverse=True)

        rank = None
        for i, (s, cand) in enumerate(scored):
            cc, ec = strip_punct(cand).lower(), strip_punct(ocr_error).lower()
            if ec in cc or cc in ec or cc == ec:
                rank = i + 1
                break
        if rank is None: continue

        total += 1
        if rank == 1: rank_1 += 1
        if rank <= 3: rank_top3 += 1
        if rank != 1 and len(failures) < 10:
            failures.append((correction, ocr_error, rank, scored[0][1][:25], scored[0][0]))

    print(f"\n  {algo_name:<22} Rank1: {rank_1}/{total} ({rank_1/total:.1%})  Top3: {rank_top3}/{total} ({rank_top3/total:.1%})")
    for q, e, r, w, ws in failures[:5]:
        print(f"    {q:<16} expect={e:<16} rank={r:<4} winner={w:<25} ({ws:.2f})")


if __name__ == "__main__":
    print("=" * 70)
    print("  FALSE POSITIVE PRECISION - ROUND 2")
    print("=" * 70)
    for name, fn in ALGORITHMS.items():
        evaluate_fp(name, fn)
