#!/usr/bin/env python3
# reassign-staging.py — move GuruRMM agents out of the "Staging - Auto Enroll" catch-all
# into their real client, by matching hostname -> Syncro customer -> GuruRMM client.
#
# Flow: a generic installer (site DARK-STORM-3150) is pushed via a Syncro policy to every
# managed machine; they enroll into Staging. This tool then reassigns each to the right
# client automatically. Idempotent — run it on a schedule or on demand.
#
# Env: RMM, TOK (GuruRMM), SK (Syncro api key). --dry = report only.
import json,urllib.request,ssl,os,sys,time
RMM=os.environ["RMM"]; TOK=os.environ["TOK"]; SK=os.environ["SK"]
ctx=ssl.create_default_context(); DRY="--dry" in sys.argv
STAGING_CLIENT="Staging - Auto Enroll"
FIX_HOSTS=set(h.strip().lower() for h in os.environ.get("FIX_HOSTS","").split(",") if h.strip())
def rget(path):
    return json.loads("".join(c for c in urllib.request.urlopen(urllib.request.Request(f"{RMM}{path}",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 move(aid,site_id):
    # move endpoint is rate-limited: throttle + retry on 500
    last=None
    for attempt in range(4):
        try:
            req=urllib.request.Request(f"{RMM}/api/agents/{aid}/move",data=json.dumps({"site_id":site_id}).encode(),method="POST",headers={"Authorization":f"Bearer {TOK}","Content-Type":"application/json"})
            urllib.request.urlopen(req,context=ctx,timeout=20); return
        except Exception as e:
            last=e; time.sleep(2.0*(attempt+1))
    raise last
def syncro_customer(hostname):
    raw=urllib.request.urlopen(urllib.request.Request(f"https://computerguru.syncromsp.com/api/v1/customer_assets?query={urllib.parse.quote(hostname)}&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))
    for a in json.loads(raw).get("assets",[]):
        if (a.get("name") or "").lower()==hostname.lower():
            c=a.get("customer") or {}
            return c.get("business_name") or c.get("fullname")
    return None
import urllib.parse,re
def norm(s):
    s=(s or "").lower().strip()
    s=re.sub(r'[.,]',' ',s)
    s=re.sub(r'\b(llc|inc|incorporated|corp|corporation|company|co|ltd|the)\b',' ',s)
    return re.sub(r'\s+',' ',s).strip()
agents=rget("/api/agents")
staging=[a for a in agents if a.get("client_name")==STAGING_CLIENT or (a.get("hostname") or "").lower() in FIX_HOSTS]
print(f"{len(staging)} agents to reconcile (Staging + FIX_HOSTS)")
# GuruRMM client -> a target site id (prefer 'Main'/'Main Office', else first); keyed by normalized name
clients=rget("/api/clients")
name2site={}
for c in clients:
    if c["name"]==STAGING_CLIENT: continue
    sites=rget(f"/api/clients/{c['id']}/sites")
    if not sites: continue
    main=next((s for s in sites if s["name"].lower() in ("main","main office","office")), sites[0])
    name2site[norm(c["name"])]=main["id"]
moved=0; unmatched=[]
for a in staging:
    cust=syncro_customer(a["hostname"])
    site=name2site.get(norm(cust))
    if cust and site:
        if not DRY:
            try: move(a["id"],site); time.sleep(1.5)
            except Exception as e: unmatched.append(f"{a['hostname']} (move FAILED: {e})"); continue
        moved+=1; print(f"  {a['hostname']} -> '{cust}'")
    else:
        unmatched.append(f"{a['hostname']} (syncro='{cust}')")
print(f"\n{'WOULD move' if DRY else 'Moved'} {moved}; unmatched {len(unmatched)} (stay in Staging):")
for u in unmatched: print("  "+u)
