"""Link perturbation via ``tc netem`` on a node's FABNetv4 egress.

The sites are real and geographically apart, so base latency is already
realistic; this adds controlled, repeatable degradation on top (RQ1). Invoked
over SSH by the notebook coordinator:

    python3 -m fl.perturb apply --delay-ms 25 --loss-pct 1 --rate-mbit 100
    python3 -m fl.perturb clear

If ``--iface`` is omitted the FABNetv4 interface is auto-detected as the one
holding a 10.x address (FABRIC's FABNetv4 range).
"""

from __future__ import annotations

import argparse
import subprocess
import sys


def detect_fabnet_iface() -> str:
    """Best-effort: the interface carrying the FABNetv4 (10.x) address."""
    out = subprocess.run(
        ["ip", "-o", "-4", "addr", "show"],
        capture_output=True, text=True, check=False,
    ).stdout
    for line in out.splitlines():
        parts = line.split()
        if len(parts) >= 4 and parts[3].startswith("10."):
            return parts[1]
    raise RuntimeError("could not auto-detect FABNetv4 interface (no 10.x address)")


def apply(iface: str, delay_ms: int, loss_pct: float, rate_mbit: int) -> None:
    subprocess.run(["sudo", "tc", "qdisc", "del", "dev", iface, "root"],
                   capture_output=True, check=False)  # clear any existing qdisc
    netem = ["sudo", "tc", "qdisc", "add", "dev", iface, "root", "netem"]
    if delay_ms and delay_ms > 0:
        netem += ["delay", f"{delay_ms}ms"]
    if loss_pct and loss_pct > 0:
        netem += ["loss", f"{loss_pct}%"]
    if rate_mbit and rate_mbit > 0:
        netem += ["rate", f"{rate_mbit}mbit"]
    if len(netem) == 6:  # nothing requested beyond bare netem
        print(f"### PROGRESS: no perturbation requested for {iface}")
        return
    res = subprocess.run(netem, capture_output=True, text=True, check=False)
    if res.returncode != 0:
        raise RuntimeError(f"tc netem failed on {iface}: {res.stderr.strip()}")
    print(f"### PROGRESS: perturbation on {iface}: "
          f"delay={delay_ms}ms loss={loss_pct}% rate={rate_mbit}mbit")


def clear(iface: str) -> None:
    subprocess.run(["sudo", "tc", "qdisc", "del", "dev", iface, "root"],
                   capture_output=True, check=False)
    print(f"### PROGRESS: cleared perturbation on {iface}")


def main() -> None:
    ap = argparse.ArgumentParser(description="tc netem link perturbation")
    ap.add_argument("action", choices=["apply", "clear"])
    ap.add_argument("--iface", default="", help="interface (default: auto-detect FABNetv4)")
    ap.add_argument("--delay-ms", type=int, default=0)
    ap.add_argument("--loss-pct", type=float, default=0.0)
    ap.add_argument("--rate-mbit", type=int, default=0)
    args = ap.parse_args()

    iface = args.iface or detect_fabnet_iface()
    if args.action == "apply":
        apply(iface, args.delay_ms, args.loss_pct, args.rate_mbit)
    else:
        clear(iface)


if __name__ == "__main__":
    try:
        main()
    except Exception as exc:  # surface a clean message over SSH
        print(f"ERROR: {exc}", file=sys.stderr)
        sys.exit(1)
