"""Simulated MAG boxes against the Ministra STB API (load.php), using the admin debug key.

Each worker is one box (its own MAC) that loops over a boot session (handshake, get_profile,
get_localization, get_genres, get_all_channels, get_ordered_list, EPG) or watchdog polls.
--mode boot: boot sessions back to back (a reboot storm); --mode idle: watchdog only.

usage: python3 stb-bench.py URL --boxes N --seconds S --mode boot|idle --debug-key KEY --macs macs.txt
URL is the full load.php URL, e.g. http://ministra.example.com/stalker_portal/server/load.php.
KEY is an administrator's debug key (Ministra admin > Administrators). macs.txt lists the
MACs of active devices, one per line; each simulated box uses its own.
Read-only for the portal's content, but it may update the devices' last-seen data.
"""
import argparse
import http.client
import statistics
import threading
import time
import urllib.parse
from collections import defaultdict

UA = ('Mozilla/5.0 (QtEmbedded; U; Linux; C) AppleWebKit/533.3 (KHTML, like Gecko) '
      'MAG200 stbapp ver: 2 rev: 250 Safari/533.3')

BOOT = [
    ('handshake', 'type=stb&action=handshake&token='),
    ('get_profile', 'type=stb&action=get_profile&hd=1&sn=062014N012345&stb_type=MAG250&image_version=218'),
    ('get_localization', 'type=stb&action=get_localization'),
    ('get_genres', 'type=itv&action=get_genres'),
    ('get_all_channels', 'type=itv&action=get_all_channels'),
    ('get_ordered_list', 'type=itv&action=get_ordered_list&genre=*&p=1&sortby=number'),
    ('epg', 'type=epg&action=get_simple_data_table&ch_id=1&date=&p=1'),
]
WATCHDOG = ('watchdog', 'type=watchdog&action=get_events&event_active_id=0&init=0&cur_play_type=1')


def main():
    p = argparse.ArgumentParser()
    p.add_argument('url')
    p.add_argument('--boxes', type=int, default=10)
    p.add_argument('--seconds', type=float, default=20)
    p.add_argument('--mode', choices=['boot', 'idle'], default='boot')
    p.add_argument('--debug-key', required=True)
    p.add_argument('--macs', required=True)
    p.add_argument('--label', default='')
    a = p.parse_args()

    macs = [m.strip() for m in open(a.macs) if m.strip()]
    u = urllib.parse.urlsplit(a.url)
    lat = defaultdict(list)
    errors = defaultdict(int)
    lock = threading.Lock()
    stop = time.monotonic() + a.seconds

    def box(i):
        mac = macs[i % len(macs)]
        headers = {
            'User-Agent': UA,
            'X-User-Agent': 'Model: MAG250; Link: Ethernet',
            'Cookie': f'mac={mac}; stb_lang=en; timezone=Europe/Warsaw; debug_key={a.debug_key}',
            'Connection': 'keep-alive',
        }
        conn = http.client.HTTPConnection(u.hostname, u.port, timeout=30)
        steps = BOOT if a.mode == 'boot' else [WATCHDOG]
        while time.monotonic() < stop:
            for name, q in steps:
                if time.monotonic() >= stop:
                    break
                t = time.perf_counter()
                try:
                    conn.request('GET', f'{u.path}?{q}&JsHttpRequest=1-xml', headers=headers)
                    r = conn.getresponse()
                    body = r.read()
                    ok = r.status == 200 and body.startswith(b'{"js"') and b'Authorization failed' not in body
                except Exception:
                    ok = False
                    conn.close()
                    conn = http.client.HTTPConnection(u.hostname, u.port, timeout=30)
                ms = (time.perf_counter() - t) * 1000
                with lock:
                    if ok:
                        lat[name].append(ms)
                    else:
                        errors[name] += 1

    threads = [threading.Thread(target=box, args=(i,)) for i in range(a.boxes)]
    started = time.monotonic()
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    elapsed = time.monotonic() - started

    every = [x for v in lat.values() for x in v]
    n_err = sum(errors.values())
    q = statistics.quantiles(every, n=100) if len(every) > 1 else [0] * 99
    print(f'{a.label:<20} boxes={a.boxes:<4} req/s={len(every) / elapsed:7.1f}  '
          f'p50={q[49]:6.1f}ms p95={q[94]:6.1f}ms p99={q[98]:6.1f}ms  errors={n_err}')
    if a.mode == 'boot':
        per = '  '.join(f'{k}={statistics.median(v):.0f}' for k, v in lat.items())
        print(f'{"":<20} median ms per call: {per}')


if __name__ == '__main__':
    main()
