"""Figure 8 of the Raft paper, staged deliberately, run twice.

The schedule below is hand-written. It is an adversary: five phases of crashes
and dropped messages chosen to break the rule everyone invents first. Both
commit rules run this *identical* schedule -- `figure8()` takes the rule as an
argument and changes nothing else -- so every difference in the output comes
from the one clause in `Node.advance_commit`.

  python3 demo.py
"""

from raft import Cluster, NAIVE, RAFT

BAR = "=" * 68


def dump(c, label):
    """Every node's role, term, commit index and log, in one block."""
    print(f"  {label}")
    for i in c.ids:
        node = c.nodes[i]
        state = node.role[0].upper() if node.alive else "x"
        log = " ".join(repr(e) for e in node.log) or "-"
        print(f"    S{i} [{state}] term={node.term} "
              f"commit={node.commit_index}   log: {log}")


def figure8(rule, verbose=True):
    """The schedule. Identical for both rules; `rule` is only passed through."""
    c = Cluster(5, rule)
    say = print if verbose else (lambda *a, **k: None)

    # (0) A quiet term. S1 wins term 1 and gets entry `a` onto all five nodes.
    #     Nothing controversial happens here; it just gives every log a common
    #     prefix, exactly as Figure 8 does.
    term = c.elect(1)
    c.nodes[1].client("a")
    c.deliver()
    c.deliver()                       # second round carries commit_index out
    say(f"(0) S1 wins term {term}; a@1 replicated to all five")
    dump(c, "after (0)")

    # (a) S1 wins term 2 and appends `b`, but only S2 receives the
    #     AppendEntries. S3, S4 and S5 never hear about it.
    term = c.elect(1)
    c.nodes[1].client("b")
    c.deliver(only={1, 2})
    say(f"\n(a) S1 wins term {term}; b@2 reaches S2 only -- 2 of 5")
    dump(c, "after (a)")

    # (b) S1 crashes. S5 wins term 3 with votes from S3 and S4 -- their logs
    #     are just [a@1], so S5's [a@1] is up to date enough. S5 takes a client
    #     command of its own at index 2, and it goes nowhere.
    c.crash(1)
    term = c.elect(5)
    c.nodes[5].client("c")
    c.deliver(only={5})
    say(f"\n(b) S1 down; S5 wins term {term}; c@3 sits at index 2 on S5 alone")
    dump(c, "after (b)")

    # (c) THE MOMENT. S5 crashes, S1 restarts and wins term 4, and pushes the
    #     old b@2 out to S3. Now b@2 is on S1, S2, S3 -- a real majority of 5.
    c.crash(5)
    c.restart(1)
    term = c.elect(1, only={1, 2, 3})
    c.deliver(only={1, 2, 3})
    leader = c.nodes[1]
    stored = 1 + sum(1 for p in leader.peers if leader.match_index[p] >= 2)
    say(f"\n(c) S5 down; S1 wins term {term}; b@2 now on {c.holders(2, 'b')}")
    dump(c, "after (c)")
    say(f"    S1 counts b@2 on {stored} of 5 -- a majority.")
    say(f"    b@2 was created in term {leader.log[1].term}; S1's term is "
        f"{leader.term}.")
    say(f"    commit_index = {leader.commit_index}    [rule = {rule}]")
    claimed = leader.commit_index >= 2

    # (d) S1 crashes again. S5 restarts and wins term 5: its log ends c@3 and
    #     the followers' end b@2, and term 3 beats term 2, so S5 is the more
    #     up-to-date candidate. It then overwrites index 2 everywhere.
    c.crash(1)
    c.restart(5)
    term = c.elect(5, only={2, 3, 4, 5})
    c.deliver(only={2, 3, 4, 5})
    c.deliver(only={2, 3, 4, 5})
    say(f"\n(d) S1 down; S5 wins term {term}; index 2 becomes c@3")
    dump(c, "after (d)")

    # (e) S1 rejoins and is truncated like everyone else.
    c.restart(1)
    c.nodes[5].replicate()
    c.deliver()
    c.deliver()
    say("\n(e) S1 rejoins and is overwritten too")
    dump(c, "after (e)")

    return c, claimed


def run_figure8():
    verdicts = {}
    for rule in (NAIVE, RAFT):
        print(BAR)
        print(f"  COMMIT RULE = {rule}")
        print(BAR)
        c, claimed = figure8(rule)
        survivors = c.holders(2, "b")
        verdicts[rule] = (claimed, survivors)
        print()
        print(f"  index 2 reported COMMITTED at step (c): {claimed}")
        print(f"  nodes still holding b@2 at the end:     "
              f"{survivors if survivors else 'none'}")
        print()

    print(BAR)
    print("  VERDICT")
    print(BAR)
    naive_claimed, naive_left = verdicts[NAIVE]
    raft_claimed, raft_left = verdicts[RAFT]
    print(f"  Same schedule, both times. Only the commit rule differed.")
    print(f"    naive: claimed b@2 committed = {naive_claimed}, "
          f"survivors = {naive_left if naive_left else 'none'}")
    print(f"    raft : claimed b@2 committed = {raft_claimed}, "
          f"survivors = {raft_left if raft_left else 'none'}")
    print(f"  The naive rule told a client 'committed' about an entry that")
    print(f"  no node in the cluster still holds.")
    print()


def boundary():
    """Where the effect vanishes: give S1 one entry of its OWN term."""
    print(BAR)
    print("  BOUNDARY -- the same schedule, plus one term-4 entry")
    print(BAR)
    c = Cluster(5, RAFT)
    c.elect(1)
    c.nodes[1].client("a")
    c.deliver()
    c.deliver()
    c.elect(1)
    c.nodes[1].client("b")
    c.deliver(only={1, 2})
    c.crash(1)
    c.elect(5)
    c.nodes[5].client("c")
    c.deliver(only={5})
    c.crash(5)
    c.restart(1)
    term = c.elect(1, only={1, 2, 3})

    # THE ONE DIFFERENCE from figure8(): a command accepted in term 4, pushed
    # to the same majority that already has b@2.
    c.nodes[1].client("d")
    c.deliver(only={1, 2, 3})
    c.deliver(only={1, 2, 3})
    leader = c.nodes[1]
    print(f"(c') S1 wins term {term} and appends d@4 to {c.holders(3, 'd')}")
    dump(c, "after (c')")
    print(f"    index 3 holds d@{leader.log[2].term}, and S1's term is "
          f"{leader.term} -- they match, so it commits.")
    print(f"    commit_index = {leader.commit_index}: index 2 rides along "
          f"behind index 3.")

    c.crash(1)
    c.restart(5)
    c.elect(5, only={2, 3, 4, 5})
    print(f"\n(d') S5 tries again. Leader? {c.nodes[5].role == 'leader'}. "
          f"It reached term {c.nodes[5].term} trying.")
    for i in (2, 3, 4):
        print(f"    S{i}: term={c.nodes[i].term} voted_for={c.nodes[i].voted_for}")
    dump(c, "after (d')")
    print(f"\n  nodes still holding b@2: {c.holders(2, 'b')}")
    print("  One entry of the leader's own term, and the old entry is safe.")
    print()


if __name__ == "__main__":
    run_figure8()
    boundary()
