#!/usr/bin/env python3
# survey-report.py -- fleet-wide channel-congestion analysis from a survey-collect JSON.
#
# THE DECISION-DRIVER: turns the raw per-AP survey (SURVEY_JSON from survey-collect.sh) into the
# fleet-wide per-channel measured busy% table + per-band-group rollup + cleanest/dirtiest ranking,
# so the channel plan is chosen from MEASURED FACTS, not policy. This is what makes the
# DFS-vs-non-DFS (and any channel) call obvious instead of assumed.
#
# WHY THIS EXISTS (Cascades 2026-06-19): the skill collected the survey but never aggregated it, and
# both survey-collect's report and channel-plan's palette were hardcoded to NON-DFS. On this site the
# non-DFS channels (149/157) measured 12-28% busy while DFS measured 2-3% -- the opposite of the
# baked-in assumption. A blind non-DFS plan made things worse; the data-driven DFS plan halved 5GHz
# retry. Always run THIS before channel-plan, and feed channel-plan a palette derived from it.
#
# Usage:
#   python survey-report.py <SURVEY_JSON> [band=na|ng]
#   SURVEY_JSON=.claude/tmp/<site>-survey.json python survey-report.py - na
#
# Output: per-channel median/mean/max busy% (n APs), band-group rollup (UNII-1/UNII-2a-DFS/
# UNII-2c-DFS/UNII-3), the cleanest + dirtiest channels, and a suggested clean 40MHz palette to
# hand to channel-plan via --channels.

import json, os, sys, statistics as st
from collections import defaultdict

path = sys.argv[1] if len(sys.argv) > 1 and sys.argv[1] != '-' else os.environ.get('SURVEY_JSON', '')
band = sys.argv[2] if len(sys.argv) > 2 else 'na'
if not path or not os.path.exists(path):
    sys.exit("usage: survey-report.py <SURVEY_JSON> [na|ng]   (or SURVEY_JSON env). file not found.")
d = json.load(open(path))
sb = {'na': '5', 'ng': '2.4'}[band]

def band_of(c):
    if band == 'ng':
        return '2.4 GHz'
    if 36 <= c <= 48:  return 'UNII-1 (non-DFS)'
    if 52 <= c <= 64:  return 'UNII-2a (DFS)'
    if 100 <= c <= 144: return 'UNII-2c (DFS)'
    if 149 <= c <= 165: return 'UNII-3 (non-DFS)'
    return '?'

ch = defaultdict(list)
for ap, bands in d.items():
    for c, busy in bands.get(sb, {}).items():
        try: ch[int(c)].append(busy)
        except: pass
if not ch:
    sys.exit(f"[survey-report] no {band} ({sb}GHz) data in {path}")

print(f"==== FLEET CHANNEL CONGESTION ({band}, {len(d)} APs scanned) -- measured busy% (lower=cleaner) ====")
print(f"{'ch':>4} {'band':<18} {'median':>7} {'mean':>6} {'max':>5}  n")
rows = []
for c in sorted(ch):
    v = ch[c]; rows.append((st.median(v), c, st.mean(v), max(v), len(v)))
    print(f"{c:>4} {band_of(c):<18} {st.median(v):>6.0f}% {st.mean(v):>5.0f}% {max(v):>4.0f}% {len(v):>3}")

grp = defaultdict(list)
for ap, bands in d.items():
    for c, busy in bands.get(sb, {}).items():
        try: grp[band_of(int(c))].append(busy)
        except: pass
print("\nBY BAND GROUP (median busy% across all AP-channel samples):")
for g in sorted(grp):
    v = grp[g]
    print(f"  {g:<18} median={st.median(v):>4.0f}%  mean={st.mean(v):>4.0f}%  (n={len(v)})")

clean = sorted(rows)            # by median busy asc
print("\nCLEANEST channels:", ", ".join(f"ch{c}({m:.0f}%)" for m, c, *_ in clean[:8]))
print("DIRTIEST channels:", ", ".join(f"ch{c}({m:.0f}%)" for m, c, *_ in clean[::-1][:5]))

if band == 'na':
    # suggest a clean 40MHz palette: lower-primary channels whose 40MHz pair (c, c+4) are both clean
    medbusy = {c: st.median(v) for c, v in ch.items()}
    THRESH = max(8, st.median([m for m in medbusy.values()]))  # "clean" = <= this
    pairs = [(52, 56), (60, 64), (100, 104), (108, 112), (116, 120), (124, 128), (132, 136), (140, 144),
             (36, 40), (44, 48), (149, 153), (157, 161)]
    palette = [lo for lo, hi in pairs
               if medbusy.get(lo, 99) <= THRESH and medbusy.get(hi, 99) <= THRESH]
    dfs_clean = [c for c in palette if 52 <= c <= 144]
    print(f"\nSUGGESTED clean 40MHz palette (both halves <= {THRESH:.0f}% busy): {palette}")
    print(f"  of those, DFS (cleaner here but radar-vacate risk): {dfs_clean}")
    print(f"  -> feed channel-plan:  channel-plan.sh <site> na --channels {','.join(map(str,palette))} --apply")
    print("  NOTE: choose DFS vs non-DFS from THIS data + the radar tradeoff, not a hardcoded policy.")
