"""Print every number the lesson quotes, from safety_filter.py. Run:  python3 show_numbers.py"""
import re

import safety_filter as sf

R = sf.build()
LABELS, SCORES, TEST, VOCAB, W = R["labels"], R["scores"], R["test"], R["vocab"], R["w"]
SAME = 68                                           # the first 68 test messages use wordings the training messages had; the other 52 do not


def pct_text(x):
    return f"{100 * x:.2f}%"


def row(p, c):
    miss, alarm, precision = sf.rates(c)
    return f"  0.{p:02d}  caught {c[0]:>5,}  missed {c[1]:>3}  false alarms {c[2]:>5,}  passed {c[3]:>5,}   miss {pct_text(miss):>7}  false-alarm {pct_text(alarm):>7}  precise {pct_text(precision):>7} ({sf.share(c[0], c[0] + c[2])}%)"


print("DATA")
harm_train = sum(y for _, y in R["train"])
print(f"  train {len(R['train'])} ({harm_train} harmful, {len(R['train']) - harm_train} harmless); test {len(TEST)} ({sum(LABELS)} harmful, {len(LABELS) - sum(LABELS)} harmless)")
print(f"  test messages in wordings never seen in training: {len(TEST) - SAME} ({sum(LABELS[SAME:])} harmful, {len(TEST) - SAME - sum(LABELS[SAME:])} harmless)")
print(f"  features the filter has weights for: {len(VOCAB)}; loss {R['losses'][0]:.4f} at step 1, {R['losses'][-1]:.4f} at step {len(R['losses'])}; bias {R['b']:.3f}")
words = sorted(((float(W[i]), name[2:]) for name, i in VOCAB.items() if name.startswith("w:")), reverse=True)
print("  highest word weights:", ", ".join(f"{name} {v:.2f}" for v, name in words[:9]))
print("  lowest word weights: ", ", ".join(f"{name} {v:.2f}" for v, name in words[-3:]))
for word in ("address", "phone", "number", "street", "the"):
    print(f"    weight of '{word}': {float(W[VOCAB['w:' + word]]):.3f}")
for word in ("phone", "address", "the"):
    in_train = lambda label: sum(1 for text, y in R["train"] if y == label and word in re.findall(r"[a-z0-9]+", text))
    print(f"    training messages with '{word}': {in_train(1)} of {harm_train} harmful, {in_train(0)} of {len(R['train']) - harm_train} harmless")

print("SCORES (test messages)")
print(f"  lowest harmful score {min(s for y, s in zip(LABELS, SCORES) if y):.2f}; highest harmless score {max(s for y, s in zip(LABELS, SCORES) if not y):.2f}")
for lo, hi in ((0, .05), (.05, .25), (.25, .5), (.5, .9), (.9, 1.01)):
    h = sum(1 for y, s in zip(LABELS, SCORES) if y and lo <= s < hi)
    n = sum(1 for y, s in zip(LABELS, SCORES) if not y and lo <= s < hi)
    print(f"  score {lo:.2f} to {min(hi, 1):.2f}: {h:>2} harmful, {n:>2} harmless")

print("THRESHOLD SWEEP (test messages)")
for p in (1, 10, 25, 30, 35, 40, 45, 50, 60, 70, 80, 90, 99):
    print(row(p, R["sweep"][p]))
errors = {p: c[1] + c[2] for p, c in R["sweep"].items()}
best = min(errors.values())
fewest = [p for p, e in errors.items() if e == best]
print(f"  fewest mistakes: {best}, at {len(fewest)} thresholds from 0.{min(fewest):02d} to 0.{max(fewest):02d}")
print(f"  miss rate at 0.99: {100 * R['sweep'][99][1] / sum(LABELS):.0f}%; false-alarm rate at 0.01: {100 * R['sweep'][1][2] / (len(LABELS) - sum(LABELS)):.1f}%")
cross = sf.crossing(R["sweep"])
print(f"  at the crossing, miss {R['sweep'][cross][1]} of {sum(LABELS)} = {100 * sf.rates(R['sweep'][cross])[0]:.1f}%, false alarms {R['sweep'][cross][2]} of {len(LABELS) - sum(LABELS)} = {100 * sf.rates(R['sweep'][cross])[1]:.1f}%")
print(f"  miss count is 0 up to 0.{max(p for p, c in R['sweep'].items() if c[1] == 0):02d}; false-alarm count is 0 from 0.{min(p for p, c in R['sweep'].items() if c[2] == 0):02d}")
print(f"  thresholds where both mistakes are 0: {[p for p, c in R['sweep'].items() if c[1] == 0 and c[2] == 0]}")
print(f"  miss rate first reaches the false-alarm rate at 0.{sf.crossing(R['sweep']):02d}")
print(f"  at 0.01 no harmful message is missed but {R['sweep'][1][2]} harmless are blocked; at 0.99 {R['sweep'][99][1]} harmful are missed and {R['sweep'][99][2]} harmless blocked")

print(f"RARE HARM: {sf.TRAFFIC:,} messages, {sf.TRAFFIC_HARMFUL} harmful (1 in {sf.TRAFFIC // sf.TRAFFIC_HARMFUL}), same miss and false-alarm rates")
c50 = R["sweep"][50]
m, a, pr = sf.rates(c50)
print(f"  test messages at 0.50: miss rate {c50[1]}/{sum(LABELS)} = {100 * m:.1f}%, false-alarm rate {c50[2]}/{len(LABELS) - sum(LABELS)} = {100 * a:.2f}%")
print(f"  by hand: {sf.TRAFFIC_HARMFUL} x {100 * m:.1f}% = {sf.TRAFFIC_HARMFUL * m:.1f} missed; {sf.TRAFFIC - sf.TRAFFIC_HARMFUL:,} x {100 * a:.2f}% = {(sf.TRAFFIC - sf.TRAFFIC_HARMFUL) * a:.2f} falsely blocked")
caught_t, _, false_t, _ = sf.at_traffic(c50)
print(f"  caught by hand: {sf.TRAFFIC_HARMFUL} x {100 * (1 - m):.1f}% = {sf.TRAFFIC_HARMFUL * (1 - m):.1f}; blocked in all {caught_t} + {false_t} = {caught_t + false_t}; harmful share {caught_t} / {caught_t + false_t} = {100 * caught_t / (caught_t + false_t):.1f}%")
for p in (30, 40, 50, 60, 70, 90):
    t = sf.at_traffic(R["sweep"][p])
    print(row(p, t))
    if p == 50:
        print(f"    harmless share of the {t[0] + t[2]} blocked: {sf.share(t[2], t[0] + t[2])}%; harmful share {sf.share(t[0], t[0] + t[2])}%")
for p in range(1, 100):
    t = sf.at_traffic(R["sweep"][p])
    if t[0] >= t[2]:                                # first threshold where at least half of the blocked messages are harmful
        print(f"  at least half of the blocked messages are harmful from 0.{p:02d}, where the test messages have no false alarm left:")
        print(row(p, t))
        break
v = sf.view(LABELS, SCORES, 50, True)
print(f"  one more or one fewer false alarm among the {len(LABELS) - sum(LABELS)} harmless test messages moves the {sf.TRAFFIC:,}-message count by about {(sf.TRAFFIC - sf.TRAFFIC_HARMFUL) / (len(LABELS) - sum(LABELS)):.0f}")
print(f"  widget sentence, test mix:    {sf.view(LABELS, SCORES, 50)['says']}  {sf.view(LABELS, SCORES, 50)['precision']}")
print(f"  widget sentence, traffic mix: {v['says']}  {v['precision']}")

wrong = [i for i in range(len(TEST)) if (SCORES[i] >= .5) != bool(LABELS[i])]
print(f"MISTAKES AT 0.50: {len(wrong)}, of which {sum(1 for i in wrong if i >= SAME)} in wordings never seen in training and {sum(1 for i in wrong if i < SAME)} in the other {SAME} messages")
for i, (text, y) in enumerate(TEST):
    if (SCORES[i] >= .5) != bool(y):
        print(f"  {'harmful, missed' if y else 'harmless, blocked':<18} {SCORES[i]:.3f}  {'unseen' if i >= SAME else 'seen  '}  {text}")

print("WHY (the filter's sum for the three examples, before the sigmoid squeezes it into 0 to 1)")
for core in (sf.MISSED_EXAMPLE, sf.FALSE_ALARM_EXAMPLE, sf.PASSED_EXAMPLE):
    i = sf.find(TEST, core)
    known, unseen, z = sf.explain(TEST[i][0], R)
    print(f"  {sf.readout(TEST[i][0], LABELS[i], SCORES[i], 50)}")
    print(f"    sum {z:+.3f} = bias {R['b']:+.3f} + " + " + ".join(f"{name} {v:+.3f}" for name, v in known if name.startswith("w:")) + f" + {sum(v for n, v in known if n.startswith('c:')):+.3f} from {sum(1 for n, _ in known if n.startswith('c:'))} letter pairs")
    print(f"    words with no weight: {[n[2:] for n in unseen if n.startswith('w:')]}; letter pairs with no weight: {sum(1 for n in unseen if n.startswith('c:'))}")
