#!/usr/bin/env python
"""Fast, self-healing AgentV10 entrypoint for local MCSJ.

Examples:
  python C:/MCSJAgent/SwingBridge/mcsj-fast-fx.py ensure
  python C:/MCSJAgent/SwingBridge/mcsj-fast-fx.py state
  python C:/MCSJAgent/SwingBridge/mcsj-fast-fx.py find --panel-class ...
  python C:/MCSJAgent/SwingBridge/mcsj-fast-fx.py edit --panel-class ...
  python C:/MCSJAgent/SwingBridge/mcsj-fast-fx.py cell --panel-class ...
"""
from __future__ import annotations

import json
import re
import socket
import subprocess
import sys
from pathlib import Path

PORT = 9810
HOST = "127.0.0.1"
ROOT = Path(__file__).resolve().parent
CLIENT = ROOT / "mcsj-swing-bridge-client.py"
AGENT = ROOT / "AgentV10.jar"
JATTACH = ROOT / "jattach32.exe"
JAB_CLIENT = Path("C:/MCSJAgent/mcsj-jab-client.py")
OWNER_CACHE = Path("C:/MCSJAgent/cache/agentv10-owner.json")


def ping() -> bool:
    try:
        with socket.create_connection((HOST, PORT), timeout=2) as sock:
            sock.sendall(b"PING\n")
            sock.shutdown(socket.SHUT_WR)
            return sock.recv(64).decode("utf-8", errors="replace").strip() == "PONG"
    except OSError:
        return False


def live_mcsj_state() -> dict:
    result = subprocess.run(
        [sys.executable, str(JAB_CLIENT), "state"],
        capture_output=True, text=True, timeout=20,
    )
    if result.returncode != 0:
        raise RuntimeError("JAB state failed: " + result.stderr.strip())
    try:
        payload = json.loads(result.stdout)
    except json.JSONDecodeError as exc:
        raise RuntimeError("JAB state was not JSON: " + result.stdout[:500]) from exc
    if not payload.get("ok") or not payload.get("pid"):
        raise RuntimeError("MCSJ/JAB is not ready: " + json.dumps(payload))
    return payload


def port_owner_pid() -> int | None:
    result = subprocess.run(["netstat", "-ano", "-p", "TCP"], capture_output=True, text=True, timeout=10)
    pattern = re.compile(r"^\s*TCP\s+127\.0\.0\.1:%d\s+\S+\s+LISTENING\s+(\d+)\s*$" % PORT, re.I)
    for line in result.stdout.splitlines():
        match = pattern.match(line)
        if match:
            return int(match.group(1))
    return None


def read_owner_cache() -> dict | None:
    try:
        return json.loads(OWNER_CACHE.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        return None


def write_owner_cache(payload: dict) -> None:
    OWNER_CACHE.parent.mkdir(parents=True, exist_ok=True)
    OWNER_CACHE.write_text(json.dumps(payload, separators=(",", ":")), encoding="utf-8")


def ensure_agent(allow_cached: bool = True) -> dict:
    owner = port_owner_pid()
    cached = read_owner_cache() if allow_cached else None
    if cached and owner == cached.get("pid") and owner and ping():
        return {
            "status": "OK",
            "databaseTitle": cached.get("databaseTitle", ""),
            "pid": owner,
            "port": PORT,
            "ownerPid": owner,
            "attachedNow": False,
            "verifiedBy": "pid-cache+port-owner+ping",
        }

    state = live_mcsj_state()
    pid = int(state["pid"])
    owner = port_owner_pid()
    if owner is not None and owner != pid:
        raise RuntimeError("port %d is owned by PID %d, not live MCSJ PID %d" % (PORT, owner, pid))
    attached = False
    if not (owner == pid and ping()):
        result = subprocess.run(
            [str(JATTACH), str(pid), "load", "instrument", "false", "%s=%d" % (AGENT, PORT)],
            capture_output=True, text=True, timeout=30, cwd=str(ROOT),
        )
        if result.returncode != 0 or "Response code = 0" not in result.stdout:
            raise RuntimeError("AgentV10 attach failed: " + (result.stdout + result.stderr).strip())
        attached = True
        owner = port_owner_pid()
        if owner != pid or not ping():
            raise RuntimeError("AgentV10 attached but port ownership/health verification failed")
    payload = {
        "status": "OK",
        "databaseTitle": state.get("title", ""),
        "pid": pid,
        "port": PORT,
        "ownerPid": owner,
        "attachedNow": attached,
        "verifiedBy": "JAB+port-owner+ping",
    }
    write_owner_cache(payload)
    return payload


def main() -> int:
    args = sys.argv[1:]
    command = args[0].lower() if args else "state"
    try:
        mutation_commands = {"edit", "cell", "select-radio", "fire-button", "fxedittext", "fxtablecell", "fxselectradio", "fxfirebutton"}
        allowed_commands = {"ensure", "state", "find", "edit", "cell", "select-radio", "fire-button", "fxstate", "fxfind", "fxedittext", "fxtablecell", "fxselectradio", "fxfirebutton"}
        if command not in allowed_commands:
            raise RuntimeError("command is not exposed by hardened launcher: " + command)
        ensured = ensure_agent(allow_cached=(command != "ensure" and command not in mutation_commands))
        if command == "ensure":
            print(json.dumps(ensured, separators=(",", ":")))
            return 0
        aliases = {"state": "fxstate", "find": "fxfind", "edit": "fxedittext", "cell": "fxtablecell", "select-radio": "fxselectradio", "fire-button": "fxfirebutton"}
        forwarded = [aliases.get(command, command)] + args[1:]
        result = subprocess.run(
            [sys.executable, str(CLIENT), "--port", str(PORT)] + forwarded,
            timeout=60,
        )
        return result.returncode
    except Exception as exc:
        print(json.dumps({"status": "ERROR", "message": str(exc)}, separators=(",", ":")), file=sys.stderr)
        return 1


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