"""Example: monitor-traffic for an interface on multiple MikroTik routers simultaneously.

Connects to any number of RouterOS devices in parallel and streams
``/interface/monitor-traffic`` for the given interface, printing live
bandwidth and packet-rate statistics from all routers interleaved in
real-time.

Usage::

    # Two routers, monitor ether1
    MSYS_NO_PATHCONV=1 uv run python -m routeros.examples.monitor_traffic.main \\
        --router 192.168.222.1:8728,admin,r00t \\
        --router 192.168.27.1:8728,admin,r00t \\
        --interface ether1

    # Different interface, debug logging
    MSYS_NO_PATHCONV=1 uv run python -m routeros.examples.monitor_traffic.main \\
        --router 192.168.1.1:8728,admin,secret \\
        --interface ether2 \\
        --debug

Press Ctrl+C to stop.
"""

from __future__ import annotations

import argparse
import logging
import queue
import signal
import sys
import threading
from dataclasses import dataclass

import routeros

# ---------------------------------------------------------------------------
# Data model for a single traffic sample
# ---------------------------------------------------------------------------


@dataclass
class TrafficSample:
    """One ``!re`` sentence decoded from monitor-traffic."""
    label: str          # human-readable router label, e.g. "192.168.27.1"
    name: str           # interface name
    rx_bps: int         # rx-bits-per-second
    tx_bps: int         # tx-bits-per-second
    rx_pps: int         # rx-packets-per-second
    tx_pps: int         # tx-packets-per-second


def _bps_str(bps: int) -> str:
    """Format bits-per-second as a human-readable string."""
    if bps >= 1_000_000_000:
        return f"{bps / 1_000_000_000:6.1f} Gbps"
    if bps >= 1_000_000:
        return f"{bps / 1_000_000:6.1f} Mbps"
    if bps >= 1_000:
        return f"{bps / 1_000:6.1f} Kbps"
    return f"{bps:6d}  bps"


# ---------------------------------------------------------------------------
# Per-router worker
# ---------------------------------------------------------------------------

# Sentinel placed in the shared queue when a worker exits.
_DONE = object()


def _worker(
    label: str,
    address: str,
    username: str,
    password: str,
    interface: str,
    shared_q: "queue.Queue[object]",
    stop_event: threading.Event,
    log: logging.Logger,
) -> None:
    """Connect to one router, start monitor-traffic, push samples to shared_q."""
    try:
        c = routeros.dial(address, username, password, timeout=10.0, logger=log)
    except Exception as exc:
        log.error("[%s] could not connect: %s", label, exc)
        shared_q.put(_DONE)
        return

    log.info("[%s] connected, starting monitor-traffic on %s", label, interface)

    with c:
        # monitor-traffic streams !re indefinitely – use listen().
        try:
            listener = c.listen(
                "/interface/monitor-traffic",
                f"=interface={interface}",
                queue_size=100,
            )
        except routeros.RouterOSError as exc:
            log.error("[%s] could not start monitor-traffic: %s", label, exc)
            shared_q.put(_DONE)
            return

        # Wait for stop signal in a background thread, then cancel.
        def _cancel_on_stop():
            stop_event.wait()
            try:
                listener.cancel()
            except Exception:
                pass

        threading.Thread(target=_cancel_on_stop, daemon=True).start()

        for sentence in listener:
            m = sentence.map
            try:
                sample = TrafficSample(
                    label=label,
                    name=m.get("name", interface),
                    rx_bps=int(m.get("rx-bits-per-second", 0)),
                    tx_bps=int(m.get("tx-bits-per-second", 0)),
                    rx_pps=int(m.get("rx-packets-per-second", 0)),
                    tx_pps=int(m.get("tx-packets-per-second", 0)),
                )
            except ValueError as exc:
                log.warning("[%s] could not parse sentence: %s – %s", label, m, exc)
                continue

            shared_q.put(sample)

        if listener.err and not stop_event.is_set():
            log.error("[%s] listener error: %s", label, listener.err)

    shared_q.put(_DONE)


# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------


def _parse_router(value: str) -> tuple[str, str, str, str]:
    """Parse ``host:port,user,pass`` → (label, address, username, password)."""
    parts = value.split(",", 2)
    if len(parts) < 3:
        raise argparse.ArgumentTypeError(
            f"expected format  host:port,username,password  – got {value!r}"
        )
    address, username, password = parts[0].strip(), parts[1].strip(), parts[2].strip()
    # Label is just the host part (without port) for compact display.
    label = address.rsplit(":", 1)[0] if ":" in address else address
    return label, address, username, password


def build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(
        description="Live monitor-traffic for an interface on multiple MikroTik routers.",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        epilog=(
            "Each --router value must be: host:port,username,password\n"
            "Example: --router 192.168.1.1:8728,admin,secret"
        ),
    )
    p.add_argument(
        "--router",
        dest="routers",
        metavar="HOST:PORT,USER,PASS",
        action="append",
        default=[],
        help="Router to monitor (repeat for multiple routers)",
    )
    p.add_argument(
        "--interface",
        default="ether1",
        help="Interface name to monitor on every router",
    )
    p.add_argument("--debug", action="store_true", help="Enable DEBUG logging")
    return p


# ---------------------------------------------------------------------------
# Display
# ---------------------------------------------------------------------------

_LABEL_WIDTH = 18   # padded router label column


def _print_header(label_width: int) -> None:
    sep = "-" * (label_width + 2 + 10 + 2 + 22 + 2 + 22 + 2 + 20 + 2 + 20)
    print(sep)
    print(
        f"{'Router':<{label_width}}  "
        f"{'Iface':<10}  "
        f"{'RX':<22}  "
        f"{'TX':<22}  "
        f"{'RX pps':>10}  "
        f"{'TX pps':>10}"
    )
    print(sep)
    sys.stdout.flush()


def _print_sample(s: TrafficSample, label_width: int) -> None:
    print(
        f"{s.label:<{label_width}}  "
        f"{s.name:<10}  "
        f"{_bps_str(s.rx_bps):<22}  "
        f"{_bps_str(s.tx_bps):<22}  "
        f"{s.rx_pps:>10,}  "
        f"{s.tx_pps:>10,}"
    )
    sys.stdout.flush()


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------


def main() -> None:
    args = build_parser().parse_args()

    level = logging.DEBUG if args.debug else logging.WARNING
    logging.basicConfig(
        level=level,
        format="%(asctime)s %(levelname)-8s %(message)s",
        datefmt="%H:%M:%S",
        stream=sys.stderr,
    )
    log = logging.getLogger("routeros")

    if not args.routers:
        # Friendly default if user forgets --router.
        print("No routers specified.  Use --router host:port,user,pass", file=sys.stderr)
        print("Example:", file=sys.stderr)
        print(
            "  python -m routeros.examples.monitor_traffic.main "
            "--router 192.168.1.1:8728,admin,secret "
            "--interface ether1",
            file=sys.stderr,
        )
        sys.exit(1)

    # Parse router specs.
    routers: list[tuple[str, str, str, str]] = []
    for spec in args.routers:
        try:
            routers.append(_parse_router(spec))
        except argparse.ArgumentTypeError as exc:
            print(f"Error: {exc}", file=sys.stderr)
            sys.exit(1)

    label_width = max(_LABEL_WIDTH, *(len(r[0]) for r in routers))

    # Shared queue: workers push TrafficSample or _DONE sentinel.
    shared_q: queue.Queue[object] = queue.Queue()
    stop_event = threading.Event()

    # Ctrl+C handler.
    def _on_sigint(sig, frame):
        print("\nStopping…", file=sys.stderr)
        stop_event.set()

    signal.signal(signal.SIGINT, _on_sigint)

    # Launch one worker thread per router.
    workers: list[threading.Thread] = []
    for label, address, username, password in routers:
        t = threading.Thread(
            target=_worker,
            args=(label, address, username, password, args.interface,
                  shared_q, stop_event, log),
            daemon=True,
            name=f"worker-{label}",
        )
        t.start()
        workers.append(t)

    print(
        f"Monitoring  {args.interface}  on {len(routers)} router(s).  "
        "Press Ctrl+C to stop.\n",
        file=sys.stderr,
    )

    _print_header(label_width)

    # Drain the queue until all workers have sent _DONE.
    done_count = 0
    total = len(routers)

    while done_count < total:
        try:
            item = shared_q.get(timeout=1.0)
        except queue.Empty:
            if stop_event.is_set():
                break
            continue

        if item is _DONE:
            done_count += 1
        else:
            _print_sample(item, label_width)  # type: ignore[arg-type]

    # Wait for all threads to finish.
    for t in workers:
        t.join(timeout=3.0)


if __name__ == "__main__":
    main()
