#!/usr/bin/env bash
# audit-site.sh — Plane-1 (config + interference) WiFi audit for a UniFi site on the UOS.
# Reads the controller Mongo (ace) via .claude/scripts/uos-mongo.sh; no live-stats plane needed.
#
# Usage:
#   bash .claude/skills/unifi-wifi/scripts/audit-site.sh <site-name|site_id>
#   bash .claude/skills/unifi-wifi/scripts/audit-site.sh cascades
#
# Output: 2.4/5/6 config summary, the per-channel neighbor-density (interference) map, and
# flagged issues (min_rssi off, 40MHz on 2.4, off-1/6/11 channels, high power). Pair with
# references/methodology.md to turn flags into changes; validate with live stats (Plane 2).
set -euo pipefail
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
UOS="$REPO/.claude/scripts/uos-mongo.sh"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only.
# (Pure Mongo read/analysis; no external call with an uninstrumented failure branch -> helper for consistency.)
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "unifi-wifi/audit-site" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
arg="${1:?usage: audit-site.sh <site-name|site_id>}"

if [[ "$arg" =~ ^[0-9a-f]{24}$ ]]; then
    SITE="$arg"
else
    SITE="$(bash "$UOS" --sites 2>/dev/null | grep -vi 'pq.html' | grep -i "$arg" | awk '{print $1}' | head -1)"
    [ -n "$SITE" ] || { echo "[ERROR] no site matching '$arg' (try: uos-mongo.sh --sites)"; exit 1; }
fi
echo "[INFO] auditing site_id=$SITE"

cat <<JS | bash "$UOS" 2>&1 | grep -viE 'pq.html|post-quantum|store now|server may need'
var SITE='$SITE';
var aps=db.device.find({site_id:SITE,type:'uap'},{name:1,radio_table:1}).toArray();
function tally(o,k){o[k]=(o[k]||0)+1;}
var ng_ch={}, ng_w={}, ng_pwr={}, na_w={}, na_used=0, sixe_used=0, ngOffRssi=0, flags=[];
aps.forEach(function(a){
  (a.radio_table||[]).forEach(function(r){
    if(r.radio=='ng'){
      tally(ng_ch,r.channel); tally(ng_w,r.ht); tally(ng_pwr,r.tx_power_mode);
      if(!r.min_rssi_enabled){ ngOffRssi++; flags.push("2.4 min_rssi OFF: "+a.name); }
      if(String(r.ht)!='20') flags.push("2.4 width "+r.ht+"MHz (want 20): "+a.name);
      if([1,6,11].indexOf(r.channel)<0) flags.push("2.4 off-plan ch"+r.channel+" (want 1/6/11): "+a.name);
      if(/high/i.test(String(r.tx_power_mode))) flags.push("2.4 power=high (want low/medium): "+a.name);
    } else if(r.radio=='na'){ na_used++; tally(na_w,r.ht);
    } else { sixe_used++; }
  });
});
print("==== CONFIG SUMMARY ("+aps.length+" APs) ====");
print("  2.4GHz channels: "+JSON.stringify(ng_ch)+"   (want only 1/6/11)");
print("  2.4GHz widths:   "+JSON.stringify(ng_w)+"   (want only 20)");
print("  2.4GHz power:    "+JSON.stringify(ng_pwr)+"   (want low/medium/custom in density)");
print("  2.4GHz min_rssi OFF on "+ngOffRssi+" radios");
print("  5GHz radios: "+na_used+"  widths: "+JSON.stringify(na_w)+"   (want 40 in density, not 80/160)");
print("  6GHz radios active: "+sixe_used+"   (steer 6E-capable clients here - usually the clean band)");
print("\n==== NEIGHBOR-DENSITY MAP (rogue = co-channel interference) ====");
print("  2.4GHz:");
db.rogue.aggregate([{\$match:{site_id:SITE,band:'ng'}},{\$group:{_id:'\$channel',n:{\$sum:1}}},{\$sort:{n:-1}},{\$limit:6}]).forEach(function(d){print("    ch"+d._id+": "+d.n+" neighbor BSSIDs")});
print("  5GHz (top):");
db.rogue.aggregate([{\$match:{site_id:SITE,band:'na'}},{\$group:{_id:'\$channel',n:{\$sum:1}}},{\$sort:{n:-1}},{\$limit:6}]).forEach(function(d){print("    ch"+d._id+": "+d.n)});
print("\n==== FLAGS ("+flags.length+") ====");
if(flags.length==0) print("  (none from config plane)"); else flags.forEach(function(f){print("  [!] "+f)});
print("\n[next] map flags -> changes via references/methodology.md (prune 2.4, shrink cells, steer to 6GHz, manual 1/6/11).");
print("[next] validate cu_total / satisfaction / tx_retries before+after via the live Network API (Plane 2, not yet wired).");
JS
