#!/usr/bin/env python
"""Self-healing launcher and compact state for MCSJ's in-process Swing bridge."""
from __future__ import annotations
import argparse, json, re, socket, subprocess, sys
from pathlib import Path

ROOT = Path("C:/MCSJAgent/SwingBridge")
CLIENT = ROOT / "mcsj-swing-bridge-client.py"
JATTACH = ROOT / "jattach32.exe"
AGENTS = [ROOT / "Agent.jar", ROOT / "AgentV2.jar"]
HOST, PORT = "127.0.0.1", 9797


def run(cmd, timeout=30):
    return subprocess.run([str(x) for x in cmd], text=True, capture_output=True, timeout=timeout)


def mcsj_pid():
    ps = run(["powershell.exe", "-NoProfile", "-Command", "(Get-Process MCSJ -ErrorAction Stop | Sort-Object StartTime -Descending | Select-Object -First 1).Id"])
    if ps.returncode:
        raise RuntimeError("MCSJ.exe is not running")
    return int(ps.stdout.strip())


def ping(timeout=1.5):
    try:
        with socket.create_connection((HOST, PORT), timeout=timeout) as s:
            s.sendall(b"PING\n"); s.shutdown(socket.SHUT_WR)
            return s.recv(100).decode(errors="replace").strip() == "PONG"
    except OSError:
        return False


def port_owner():
    ps = run(["powershell.exe", "-NoProfile", "-Command", f"(Get-NetTCPConnection -State Listen -LocalPort {PORT} -ErrorAction SilentlyContinue | Select-Object -First 1).OwningProcess"])
    text = ps.stdout.strip()
    return int(text) if text.isdigit() else None


def attach(pid, agent):
    result = run([JATTACH, pid, "load", "instrument", "false", f"{agent}="], timeout=30)
    return result.returncode == 0, (result.stdout + result.stderr).strip()


def ensure():
    pid = mcsj_pid()
    owner = port_owner()
    if ping():
        if owner not in (None, pid):
            raise RuntimeError(f"Swing bridge port {PORT} is owned by PID {owner}, not MCSJ PID {pid}")
        return {"status":"OK","pid":pid,"port":PORT,"ownerPid":owner,"attachedNow":False,"agent":"existing"}
    if owner is not None and owner != pid:
        raise RuntimeError(f"port {PORT} is already owned by non-MCSJ PID {owner}")
    attempts=[]
    for agent in AGENTS:
        ok, detail = attach(pid, agent)
        attempts.append({"agent":agent.name,"attachOk":ok,"detail":detail[-300:]})
        if ping(3):
            owner = port_owner()
            if owner not in (None, pid):
                raise RuntimeError(f"bridge answered but port owner is PID {owner}, expected {pid}")
            return {"status":"OK","pid":pid,"port":PORT,"ownerPid":owner,"attachedNow":True,"agent":agent.name,"attempts":attempts}
    raise RuntimeError("unable to attach Swing bridge: " + json.dumps(attempts))


def compact_state():
    ready=ensure()
    tree=run([sys.executable, CLIENT, "--timeout", "10", "tree"], timeout=15)
    if tree.returncode: raise RuntimeError(tree.stderr.strip() or tree.stdout.strip())
    obj=json.loads(tree.stdout)
    windows=obj.get("windows",[])
    menus=[]; internal=[]; popup_items=[]; popup_count=0
    def walk(node, in_popup=False):
        nonlocal popup_count
        cls=node.get("class",""); text=node.get("text",""); name=node.get("name","")
        now_popup = in_popup or cls.endswith("JPopupMenu")
        if cls.endswith("JPopupMenu"): popup_count += 1
        if cls.endswith("EMenu") and text: menus.append(text)
        if now_popup and text and (cls.endswith("EMenu") or cls.endswith("EMenuItem")):
            popup_items.append({"class":cls,"name":name,"text":text,"enabled":node.get("enabled"),"visible":node.get("visible")})
        if cls.endswith("InternalFrame") and name not in ("InternalFrame.northPane", ""):
            internal.append({"class":cls,"name":name,"text":text,"visible":node.get("visible"),"enabled":node.get("enabled")})
        for child in node.get("children",[]): walk(child, now_popup)
    for window in windows: walk(window)
    return {**ready,"title":windows[0].get("title") if windows else None,"windowCount":len(windows),"menus":menus,"internalFrames":internal,"popupCount":popup_count,"popupItems":popup_items}


def main():
    p=argparse.ArgumentParser(); p.add_argument("command",choices=["ensure","state","ping"]); args=p.parse_args()
    try:
        if args.command=="ping": out={"status":"OK" if ping() else "DOWN","pid":mcsj_pid(),"port":PORT,"ownerPid":port_owner()}
        elif args.command=="ensure": out=ensure()
        else: out=compact_state()
        print(json.dumps(out,separators=(",",":")))
        if out.get("status") not in ("OK",): return 2
    except Exception as exc:
        print(json.dumps({"status":"ERROR","message":str(exc)},separators=(",",":")))
        return 2
    return 0

if __name__=="__main__": raise SystemExit(main())
