#!/usr/bin/env python3
# rebuild-and-push.py — rebuild the "machines not in GuruRMM" list using the CORRECT
# ScreenConnect online signal (ActiveConnections ProcessType==2 = guest agent connected;
# GuestInfoUpdateTime for recency), NOT Syncro's stale last_synced_at. Then push the
# generic STAGING installer to every machine that is ONLINE right now (it will actually
# land instead of queuing). Machines enroll into Staging -> reassign-staging.py sorts them.
#
# Env: RMM, TOK (GuruRMM), SK (Syncro), SC_SECRET (ScreenConnect). --dry = no pushes.
import json,urllib.request,ssl,os,sys,base64,datetime,urllib.parse
sys.stdout.reconfigure(encoding='utf-8',errors='replace')
RMM=os.environ["RMM"]; TOK=os.environ["TOK"]; SK=os.environ["SK"]; SEC=os.environ["SC_SECRET"]
ctx=ssl.create_default_context(); DRY="--dry" in sys.argv
SB="https://computerguru.screenconnect.com/App_Extensions/2d558935-686a-4bd0-9991-07539f5fe749/Service.ashx"
STAGING_ONELINER="irm 'https://rmm.azcomputerguru.com/install/DARK-STORM-3150/windows'|iex"
ENC=base64.b64encode(STAGING_ONELINER.encode("utf-16-le")).decode()
PUSHCMD=f"powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand {ENC}"
def scp(m,b):
    req=urllib.request.Request(f"{SB}/{m}",data=json.dumps(b).encode(),method="POST",headers={"CTRLAuthHeader":SEC,"Origin":"https://computerguru.screenconnect.com","Content-Type":"application/json"})
    try:return json.loads(urllib.request.urlopen(req,context=ctx,timeout=25).read().decode("utf-8","replace"))
    except Exception as e:return {"__err":str(e)}
def rget(p):
    return json.loads("".join(c for c in urllib.request.urlopen(urllib.request.Request(f"{RMM}{p}",headers={"Authorization":f"Bearer {TOK}"}),context=ctx,timeout=30).read().decode("utf-8","replace") if ord(c)>=32 or c in "\t\n\r"))
def assets(cid):
    raw=urllib.request.urlopen(urllib.request.Request(f"https://computerguru.syncromsp.com/api/v1/customer_assets?customer_id={cid}&per_page=100&api_key={SK}"),context=ctx,timeout=25).read()
    raw="".join(chr(b) for b in raw if b>=32 or b in (9,10,13))
    return [a["name"] for a in json.loads(raw).get("assets",[]) if a.get("asset_type")=="Syncro Device" and a.get("name")]
def online(s): return any(c.get("ProcessType")==2 for c in (s.get("ActiveConnections") or []))
def recency_days(s):
    for f in ("GuestInfoUpdateTime","LastGuestConnectedEventTime"):
        t=s.get(f)
        if t and not str(t).startswith("0001"):
            try: return (datetime.datetime.utcnow()-datetime.datetime.strptime(str(t)[:19],"%Y-%m-%dT%H:%M:%S")).days
            except: pass
    return None
rmm=set(a["hostname"].lower() for a in rget("/api/agents") if a.get("hostname"))
tgts=json.load(open("projects/gps-rmm-audit/targets.json"))["clients"]
groups={"ONLINE now":[],"active <=14d":[],"stale 15-45d":[],"dead/no-session":[]}
pushed=0; failed=0
for t in tgts:
    for h in assets(t["cid"]):
        if h.lower() in rmm: continue
        ss=scp("GetSessionsByName",{"sessionName":h})
        if not (isinstance(ss,list) and ss): groups["dead/no-session"].append((t["client"],h,"no SC")); continue
        if len(ss)>1: continue   # ambiguous shared name -> skip
        s=ss[0]; d=recency_days(s)
        if online(s):
            groups["ONLINE now"].append((t["client"],h,f"{d}d" if d is not None else "?"))
            if not DRY:
                r=scp("SendCommandToSession",[s["SessionID"],PUSHCMD])
                if isinstance(r,dict) and r.get("__err"): failed+=1
                else: pushed+=1
        elif d is not None and d<=14: groups["active <=14d"].append((t["client"],h,f"{d}d"))
        elif d is not None and d<=45: groups["stale 15-45d"].append((t["client"],h,f"{d}d"))
        else: groups["dead/no-session"].append((t["client"],h,f"{d}d" if d is not None else "old"))
out=["# Machines not in GuruRMM — by ScreenConnect reachability (CORRECTED)","",
     "Online = ActiveConnections ProcessType==2 (guest agent connected). Recency = GuestInfoUpdateTime.",
     f"Rebuilt {datetime.date(2026,7,4)}. Staging installer pushed to all ONLINE machines (land in Staging -> reassign-staging.py).",""]
for g,items in groups.items():
    out.append(f"## {g} ({len(items)})")
    for c,h,d in sorted(items): out.append(f"- {h}  [{c}]  ({d})")
    out.append("")
open("projects/gps-rmm-audit/needs-screenconnect.md","w",encoding="utf-8").write("\n".join(out))
print(f"ONLINE now: {len(groups['ONLINE now'])}  | active<=14d: {len(groups['active <=14d'])} | stale15-45: {len(groups['stale 15-45d'])} | dead: {len(groups['dead/no-session'])}")
print(f"Staging installer pushed: {pushed} (failed {failed}){' [DRY]' if DRY else ''}")
