"""Four measurements on one hash ring, all deterministic (no RNG anywhere).

  1. Load balance at 8 and 32 nodes, with 1 / 10 / 150 points per node,
     where that load comes from, and why more keys will not fix it.
  2. How many keys move when one more node joins, and who they come from.
  3. The same join tried 100 different ways — the spread, not the mean.
  4. Whether that join actually relieves the node that was overloaded.

Run: `python3 demo.py`
"""

from collections import Counter

from hash_ring import Ring, balance, moved

KEYS = [f"key{i}" for i in range(10000)]
CANDIDATES = [f"newnode{i}" for i in range(100)]
VNODE_SETTINGS = (1, 10, 150)


def nodes(n):
    return [f"node{i}" for i in range(n)]


def rule(title):
    print(f"\n=== {title} ===")


# -- 1 -------------------------------------------------------------------


def load_balance():
    rule(f"1. load balance: {len(KEYS)} keys")
    print(" nodes  vnodes      min      max   max/fair   max/min")
    for n in (8, 32):
        for vnodes in VNODE_SETTINGS:
            ring = Ring(nodes(n), vnodes=vnodes)
            lo, hi, over_fair, spread = balance(ring.distribution(KEYS))
            print(
                f"{n:6d}  {vnodes:6d}  {lo:7d}  {hi:7d}"
                f"   {over_fair:8.2f}  {spread:8.2f}"
            )

    rule("1b. load is arc length, not luck: 8 nodes, 1 point each")
    ring = Ring(nodes(8), vnodes=1)
    counts = ring.distribution(KEYS)
    arcs = ring.arcs()
    print("   node   ring arc     keys   key share")
    for node in sorted(counts, key=lambda x: -counts[x]):
        print(
            f"  {node:>5}   {arcs[node] * 100:7.2f}%  {counts[node]:7d}"
            f"     {counts[node] / len(KEYS) * 100:6.2f}%"
        )

    rule("1c. more keys will not fix it: 8 nodes, 1 point each")
    print("     keys   max/fair")
    for count in (1000, 10000, 100000, 1000000):
        sample = [f"key{i}" for i in range(count)]
        _, _, over_fair, _ = balance(ring.distribution(sample))
        print(f"  {count:7d}   {over_fair:8.4f}")
    biggest = max(arcs.values())
    print(f"  the ceiling is 8 x the biggest arc = {8 * biggest:.4f}")


# -- 2 -------------------------------------------------------------------


def one_join():
    for n in (8, 32):
        fair = 1 / (n + 1) * 100
        rule(
            f"2. one node joins: {n} -> {n + 1} nodes"
            f" (fair share {fair:.2f}%)"
        )
        print(" vnodes    moved%   donor nodes   biggest donor")
        for vnodes in VNODE_SETTINGS:
            ring = Ring(nodes(n), vnodes=vnodes)
            before = ring.route_all(KEYS)
            ring.add("newnode0")
            after = ring.route_all(KEYS)
            m = moved(before, after)
            donors = Counter(old for old, _ in m.values())
            top, lost = donors.most_common(1)[0]
            print(
                f"{vnodes:6d}   {len(m) / len(KEYS) * 100:7.2f}%"
                f"   {len(donors):11d}   {top} gave up {lost}"
            )


# -- 3 -------------------------------------------------------------------


def join_spread():
    n = 8
    fair = 1 / (n + 1) * 100
    rule(f"3. the same join, {len(CANDIDATES)} ways (fair share {fair:.2f}%)")
    print(" vnodes      min      max     mean   max/min   fewest..most keys")
    for vnodes in VNODE_SETTINGS:
        ring = Ring(nodes(n), vnodes=vnodes)
        before = ring.route_all(KEYS)
        counts = []
        for candidate in CANDIDATES:
            trial = Ring(nodes(n), vnodes=vnodes)
            trial.add(candidate)
            counts.append(len(moved(before, trial.route_all(KEYS))))
        pcts = [c / len(KEYS) * 100 for c in counts]
        ratio = max(counts) / min(counts) if min(counts) else float("inf")
        print(
            f"{vnodes:6d}  {min(pcts):6.2f}%  {max(pcts):6.2f}%"
            f"  {sum(pcts) / len(pcts):6.2f}%  {ratio:8.2f}"
            f"   {min(counts)}..{max(counts)}"
        )


# -- 4 -------------------------------------------------------------------


def relief():
    n = 8
    for vnodes in VNODE_SETTINGS:
        ring = Ring(nodes(n), vnodes=vnodes)
        counts = ring.distribution(KEYS)
        busiest = max(counts, key=lambda node: counts[node])
        rule(
            f"4. does adding a node relieve the busiest one?"
            f"  vnodes={vnodes}"
        )
        print(f"  busiest before: {busiest} holds {counts[busiest]} keys")
        unchanged = 0
        best = counts[busiest]
        for candidate in CANDIDATES:
            trial = Ring(nodes(n), vnodes=vnodes)
            trial.add(candidate)
            after = trial.distribution(KEYS)[busiest]
            if after == counts[busiest]:
                unchanged += 1
            best = min(best, after)
        print(
            f"  unchanged in {unchanged} of {len(CANDIDATES)} placements"
            f"  ({unchanged / len(CANDIDATES) * 100:.0f}%)"
        )
        print(f"  best any placement did: {counts[busiest]} -> {best} keys")


if __name__ == "__main__":
    load_balance()
    one_join()
    join_spread()
    relief()
    print()
