"""Run the two detectors over the same heartbeat traces and compare them.

    python3 demo.py

Everything here is deterministic: seeded traces, no sleeping, no threads.
Re-running prints byte-identical output.
"""

import statistics

from detector import (
    HEARTBEAT_INTERVAL_MS,
    FixedDetector,
    PhiDetector,
    evaluate,
    heartbeat_trace,
)

# A network that is calm, then congested, then calm again. The sender emits a
# heartbeat every 100ms throughout; only the network delay changes.
CALM = (10.0, 3.0)
CONGESTED = (80.0, 40.0)
PHASES = [(40, *CALM), (30, *CONGESTED), (30, *CALM)]
TRACES = 200
EXAMPLE_SEED = 4


def rule(title):
    print(f"\n{title}")
    print("-" * len(title))


def census(make_detector):
    """Total false alarms and mean detection latency over TRACES traces."""
    alarms, latencies = 0, []
    for seed in range(TRACES):
        arrivals, crash_due = heartbeat_trace(seed, PHASES)
        fa, latency = evaluate(make_detector(), arrivals, crash_due)
        alarms += fa
        latencies.append(latency)
    return alarms, statistics.fmean(latencies)


print("Heartbeat every 100ms. Network delay is calm N(10,3)ms for 40 beats,")
print("congested N(80,40)ms for 30, then calm again for 30 — after which the")
print(f"sender dies. {TRACES} seeded traces.")

# ---------------------------------------------------------------- 1
rule("1. The moment the network changes, seed %d" % EXAMPLE_SEED)
arrivals, crash_due = heartbeat_trace(EXAMPLE_SEED, PHASES)
det = PhiDetector()
det.heartbeat(arrivals[0])
print(f"{'beat':>5} {'gap ms':>8} {'window mu':>10} {'window sd':>10} {'phi':>9}  verdict")
for i in range(1, len(arrivals)):
    gap = arrivals[i] - arrivals[i - 1]
    mu = statistics.fmean(det.intervals)
    sd = max(statistics.pstdev(det.intervals), det.min_stddev_ms)
    phi = det.phi(arrivals[i])
    if 37 <= i <= 46:
        verdict = "DEAD" if phi > det.threshold else "alive"
        print(f"{i:>5} {gap:>8.1f} {mu:>10.2f} {sd:>10.2f} {phi:>9.2f}  {verdict}")
    det.heartbeat(arrivals[i])
print("\nBeat 40 is the first congested heartbeat. The window still describes")
print("the calm network, so the gap is a many-sigma event and phi convicts a")
print("node that is alive and sending.")

# ---------------------------------------------------------------- 2
rule("2. It convicts on a silence it later tolerates")
convicted, tolerated, inversions, fired_traces = [], [], 0, 0
for seed in range(TRACES):
    arrivals, _ = heartbeat_trace(seed, PHASES)
    d = PhiDetector()
    d.heartbeat(arrivals[0])
    first_hit, later_ok = None, []
    for i in range(1, len(arrivals)):
        gap = arrivals[i] - arrivals[i - 1]
        hit = d.suspects(arrivals[i])
        if hit and first_hit is None:
            first_hit = (i, gap)
        elif not hit and first_hit is not None:
            later_ok.append(gap)
        d.heartbeat(arrivals[i])
    if first_hit and later_ok:
        fired_traces += 1
        convicted.append(first_hit[1])
        tolerated.append(max(later_ok))
        if max(later_ok) > first_hit[1]:
            inversions += 1
print(f"traces where phi convicted a live node:        {fired_traces}/{TRACES}")
print(f"  ...and later tolerated a LONGER silence:     {inversions}/{fired_traces}"
      f" = {100 * inversions / fired_traces:.1f}%")
print(f"  mean silence it convicted on:                {statistics.fmean(convicted):.1f} ms")
print(f"  mean longest silence it then tolerated:      {statistics.fmean(tolerated):.1f} ms")

# ---------------------------------------------------------------- 3
rule("3. Both detectors, swept. Lower is better in both columns")
points = []
for th in (2, 3, 5, 8, 10, 12, 16, 20):
    fa, lat = census(lambda th=th: PhiDetector(threshold=th))
    points.append((f"phi threshold {th}", fa, lat))
for t in (250, 275, 300, 350, 400):
    fa, lat = census(lambda t=t: FixedDetector(timeout_ms=t))
    points.append((f"fixed {t}ms", fa, lat))
print(f"{'detector':>18} {'false alarms':>13} {'detect ms':>10}  status")
for name, fa, lat in points:
    beaten = [n for n, f, l in points if f <= fa and l <= lat and (f < fa or l < lat)]
    status = f"dominated by {beaten[0]}" if beaten else "on the frontier"
    print(f"{name:>18} {fa:>13} {lat:>10.1f}  {status}")

# ---------------------------------------------------------------- 4
rule("4. The floor under sigma is what rescues phi")
print(f"{'min_stddev_ms':>14} {'false alarms':>13} {'detect ms':>10}")
for floor in (1.0, 5.0, 10.0, 20.0, 30.0, 50.0):
    fa, lat = census(lambda f=floor: PhiDetector(min_stddev_ms=f))
    print(f"{floor:>14.0f} {fa:>13} {lat:>10.1f}")
print("\nThe adaptive detector is fixed by hard-coding a constant that tells")
print("it to distrust its own variance estimate.")

# ---------------------------------------------------------------- 5
rule("5. It is not about the change being sudden")
print("Ramp the congestion in linearly over N heartbeats instead of stepping:")
print(f"{'ramp':>8} {'phi false alarms':>18} {'fixed 300ms':>13}")
for ramp in (1, 3, 5, 10, 20, 30):
    ramped = [(40, *CALM)]
    for k in range(ramp):
        f = (k + 1) / ramp
        ramped.append((1, CALM[0] + f * (CONGESTED[0] - CALM[0]),
                       CALM[1] + f * (CONGESTED[1] - CALM[1])))
    ramped.append((max(0, 30 - ramp), *CONGESTED))
    ramped.append((30, *CALM))
    pfa = ffa = 0
    for seed in range(TRACES):
        arrivals, crash_due = heartbeat_trace(seed, ramped)
        pfa += evaluate(PhiDetector(), arrivals, crash_due)[0]
        ffa += evaluate(FixedDetector(300.0), arrivals, crash_due)[0]
    print(f"{str(ramp) + ' beats':>8} {pfa:>18} {ffa:>13}")
