#!/usr/bin/env python
"""Compact JavaFX state summary for live MCSJ AgentV4.

Avoids repeatedly loading multi-megabyte FXTREE output into an operator session.
Read-only: requests FXTREE and prints only visible panels, tabs, tables,
focused nodes, buttons, editable controls, and unique visible row text.
"""
from __future__ import annotations

import argparse
import json
import socket
from pathlib import Path
from typing import Any

CONTROL_TOKENS = (
    "Button", "TextField", "MaskField", "DatePicker", "ComboBoxEditingCell",
    "TextEditingCell", "CheckBox", "RadioButton",
)


def request_tree(host: str, port: int, timeout: float) -> dict[str, Any]:
    with socket.create_connection((host, port), timeout=timeout) as sock:
        sock.settimeout(timeout)
        sock.sendall(b"FXTREE\n")
        sock.shutdown(socket.SHUT_WR)
        chunks: list[bytes] = []
        while True:
            data = sock.recv(65536)
            if not data:
                break
            chunks.append(data)
    return json.loads(b"".join(chunks).decode("utf-8", errors="replace"))


def walk(node: Any):
    if isinstance(node, dict):
        yield node
        for child in node.get("children", []) or []:
            yield from walk(child)
    elif isinstance(node, list):
        for child in node:
            yield from walk(child)


def clean_text(value: Any) -> str:
    return " ".join(str(value or "").strip().split())


def node_brief(node: dict[str, Any]) -> dict[str, Any]:
    out: dict[str, Any] = {"class": str(node.get("class", "")).split(".")[-1]}
    for source, target in (("id", "id"), ("text", "text"), ("focused", "focused"),
                           ("disabled", "disabled"), ("rowCount", "rows"),
                           ("columns", "columns"), ("selectedTabText", "selected_tab")):
        value = node.get(source)
        if source == "text":
            value = clean_text(value)
        if value not in (None, "", False, []):
            out[target] = value
    if node.get("tabs"):
        out["tabs"] = [clean_text(t.get("text")) for t in node["tabs"] if clean_text(t.get("text"))]
    return out


def summarize_panel(panel: dict[str, Any]) -> dict[str, Any]:
    root = ((panel.get("scene") or {}).get("root") or {})
    controls: list[dict[str, Any]] = []
    tables: list[dict[str, Any]] = []
    tabs: list[dict[str, Any]] = []
    focused: list[dict[str, Any]] = []
    seen_controls: set[tuple] = set()

    for node in walk(root):
        if node.get("visible") is False:
            continue
        cls = str(node.get("class", ""))
        brief = node_brief(node)
        if node.get("focused"):
            focused.append(brief)
        if "Table" in cls and "Cell" not in cls and ("rowCount" in node or "columns" in node):
            row_text: list[str] = []
            seen_text: set[str] = set()
            for descendant in walk(node.get("children", [])):
                if descendant.get("visible") is False:
                    continue
                text = clean_text(descendant.get("text"))
                if text and text not in seen_text and text not in (node.get("columns") or []):
                    seen_text.add(text)
                    row_text.append(text)
            item = brief
            if row_text:
                item["visible_text"] = row_text[:40]
            tables.append(item)
        if "TabPane" in cls and (node.get("tabs") or node.get("selectedTabText")):
            tabs.append(brief)
        if any(token in cls for token in CONTROL_TOKENS):
            key = (brief.get("class"), brief.get("id"), brief.get("text"), brief.get("focused"), brief.get("disabled"))
            if key not in seen_controls:
                seen_controls.add(key)
                controls.append(brief)

    return {
        "panel": panel.get("panelClass", "").split(".")[-1],
        "bounds": panel.get("panelBounds"),
        "focused": focused,
        "tabs": tabs,
        "tables": tables,
        "controls": controls,
    }


def summarize(tree: dict[str, Any], top_only: bool = False) -> dict[str, Any]:
    panels = tree.get("panels", []) or []
    selected = panels[:1] if top_only else panels
    return {
        "jfx_panel_count": tree.get("jfxPanelCount", len(panels)),
        "panels_returned": len(selected),
        "panels": [summarize_panel(panel) for panel in selected],
    }


def main() -> None:
    parser = argparse.ArgumentParser(description="Print compact MCSJ JavaFX state")
    parser.add_argument("--host", default="127.0.0.1")
    parser.add_argument("--port", type=int, default=9801)
    parser.add_argument("--timeout", type=float, default=20.0)
    parser.add_argument("--file", help="Summarize a saved FXTREE JSON file instead of querying live MCSJ")
    parser.add_argument("--top", action="store_true", help="Return only the first/top JavaFX panel")
    parser.add_argument("--pretty", action="store_true")
    args = parser.parse_args()

    tree = json.loads(Path(args.file).read_text(encoding="utf-8")) if args.file else request_tree(args.host, args.port, args.timeout)
    print(json.dumps(summarize(tree, top_only=args.top), indent=2 if args.pretty else None, separators=None if args.pretty else (",", ":")))


if __name__ == "__main__":
    main()
