#!/usr/bin/env python3
"""Build results.json from raw/*.json (no hand-copied numbers).

Run from anywhere: python3 scripts/build_results.py
It reads raw/ next to scripts/ and writes results.json beside them.
What the numbers mean, and every caveat: https://nxtrm.mishuk.me/compare/benchmark/
"""
import json, os, statistics

HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
RAW = os.path.join(ROOT, "raw")


def load(name):
    p = os.path.join(RAW, name + ".json")
    return json.load(open(p)) if os.path.exists(p) else None


def mib_to_mb(x):
    return round(x * 1048576 / 1e6, 1)


def run_summary(r):
    if r is None:
        return None
    ps_rss = [s["app_ps_rss_kb"] / 1024 for s in r["samples"]]
    kinds = {}
    for p in r["end_snapshot"]["app_processes"]:
        kinds[p["kind"]] = kinds.get(p["kind"], 0) + 1
    return {
        "label": r["label"],
        "started": r["started"],
        "first_sample": r["samples"][0]["time"],
        "memory_footprint_median_mib": r["median_app_footprint_mib"],
        "memory_footprint_median_mb": mib_to_mb(r["median_app_footprint_mib"]),
        "memory_footprint_samples_mib": r["app_footprint_mib_per_sample"],
        "footprint_tool_crosscheck_mib": r["footprint_tool_crosscheck_mib"],
        "resident_rss_median_mib": r["median_app_resident_mib"],
        "ps_rss_median_mib": round(statistics.median(ps_rss), 1),
        "process_count": r["app_process_count_per_sample"][0],
        "process_count_per_sample": r["app_process_count_per_sample"],
        "processes_by_kind": kinds,
        "idle_cpu_percent_of_one_core": r["avg_cpu_percent"],
        "wakeups_per_s": r["wakeups_per_s"],
        "cpu_window_s": r["cpu_window_s"],
        "memory_pressure_at_samples": [s["memory_pressure"] for s in r["samples"]],
        "load_average_1min_at_samples": [s["loadavg"][0] for s in r["samples"]],
        "excluded_shell_processes": len(r["samples"][0]["excluded_shell_subtrees"]),
        "per_process": r["per_process_window"],
        "note": r["note"],
    }


sizes = load("sizes")["apps"]
env = load("environment")

runs = {
    "nextterm_asis": run_summary(load("nextterm-asis")),
    "nextterm_3tabs": run_summary(load("nextterm-demo-3tabs")),
    "nextterm_3tabs_late": run_summary(load("nextterm-demo-3tabs-late")),
    "nextterm_after_close": run_summary(load("nextterm-after-close")),
    "nextterm_asis_end": run_summary(load("nextterm-asis-end")),
    "cursor_3tabs": run_summary(load("cursor-demo-3tabs")),
    "cursor_3tabs_late": run_summary(load("cursor-demo-3tabs-late")),
    "phpstorm_3tabs": run_summary(load("phpstorm-demo-3tabs")),
    "phpstorm_3tabs_late": run_summary(load("phpstorm-demo-3tabs-late")),
    "phpstorm_folder_0tabs_first_launch": run_summary(load("phpstorm-folder-0tabs")),
    "zed_3tabs": run_summary(load("zed-demo-3tabs")),
    "zed_3tabs_late": run_summary(load("zed-demo-3tabs-late")),
    "sublime_folder": run_summary(load("sublime-demo-folder")),
    "sublime_folder_late": run_summary(load("sublime-demo-folder-late")),
}


def dl(app):
    a = sizes[app]
    d = a.get("installed_version_download")
    if d:
        return d["bytes"], d["file"]
    cur = a["current_download"][0]
    return cur["bytes"], cur["file"] + f" (current {a['current_version']})"


headline = []
order = [("Next Term", "nextterm_3tabs", "nextterm_3tabs_late"),
         ("Cursor", "cursor_3tabs", "cursor_3tabs_late"),
         ("PhpStorm", "phpstorm_3tabs", "phpstorm_3tabs_late"),
         ("Zed", "zed_3tabs", "zed_3tabs_late"),
         ("Sublime Text", "sublime_folder", "sublime_folder_late")]
for app, k60, klate in order:
    b, f = dl(app)
    r60, rl = runs[k60] or {}, runs[klate] or {}  # a run not in raw/ (yet) leaves its fields empty
    headline.append({
        "app": app, "version": sizes[app]["version"],
        "download_bytes": b, "download_mb": round(b / 1e6, 1), "download_file": f,
        "on_disk_mb": sizes[app]["on_disk_mb"],
        "processes": r60.get("process_count"),
        "memory_mib_at_60s": r60.get("memory_footprint_median_mib"),
        "memory_mib_late": rl.get("memory_footprint_median_mib"),
        "idle_cpu_percent_60_120s": r60.get("idle_cpu_percent_of_one_core"),
        "idle_cpu_percent_late": rl.get("idle_cpu_percent_of_one_core"),
        "wakeups_per_s_60_120s": r60.get("wakeups_per_s"),
        "wakeups_per_s_late": rl.get("wakeups_per_s"),
    })

nt = {h["app"]: h for h in headline}
# The MCP bridges ("NextTerm --cli mcp") that agents connected to Next Term had started: one per agent,
# children of the agent, not counted in Next Term's total. Counted here only for the second set of ratios.
nt_run = load("nextterm-demo-3tabs")
bridges = nt_run["samples"][0]["separate_cli_helpers"] if nt_run else []
bridges_mib = sum(p["footprint_b"] for p in bridges) / 1048576


def ratio(app, key, extra=0.0):
    a, n = nt[app][key], nt["Next Term"][key]
    return round(a / (n + extra), 1) if a and n else None


ratios_vs_next_term = {app: {
    "download": ratio(app, "download_bytes"),
    "on_disk": ratio(app, "on_disk_mb"),
    "memory_at_60s": ratio(app, "memory_mib_at_60s"),
    "memory_at_60s_counting_mcp_bridges": ratio(app, "memory_mib_at_60s", bridges_mib) if bridges else None,
} for app in ("Cursor", "PhpStorm", "Zed", "Sublime Text")}

out = {
    "title": "Next Term vs IDEs and editors: size, processes, idle memory and CPU (macOS, October 2026)",
    "method_and_caveats": "https://nxtrm.mishuk.me/compare/benchmark/",
    "measured_on": "2026-10-10/11",
    "units": "Memory in MiB (2^20 bytes; what Activity Monitor labels MB). Download/on-disk sizes in MB (10^6 bytes, as Finder shows). CPU in percent of one core.",
    "scenario": "The same small TypeScript project (demo-acme-api: four source files and a test, package.json, tsconfig.json, a git repo) open in the app, 3 integrated-terminal tabs each running an idle zsh, nothing else open, app left idle. Shells and anything inside them are excluded from every total; only the app's own processes count (helpers, renderers, extension hosts, JVM, language servers, and XPC services/daemons macOS attributes to the app).",
    "environment": env,
    "headline": headline,
    "ratios_vs_next_term": ratios_vs_next_term,
    "next_term_detail": {
        "instance": "An already-running Next Term 0.10.1 (started ~47 min before the run, previously used; not restarted). Before the run it had only its Welcome window open (no project windows).",
        "baseline_welcome_window_only": runs["nextterm_asis"],
        "demo_3_tabs_at_60s": runs["nextterm_3tabs"],
        "demo_3_tabs_late": runs["nextterm_3tabs_late"],
        "after_closing_the_3_tabs": runs["nextterm_after_close"],
        "end_of_session_about_70min_later": runs["nextterm_asis_end"],
        "mcp_cli_helpers_listed_separately": {
            "what": "'NextTerm --cli mcp' processes are started by each coding agent's MCP client (children of the agent, not of Next Term); one per connected agent session. Not counted in Next Term's total.",
            "count_seen": len(bridges),
            "footprint_mib_each": [round(p["footprint_b"] / 1048576, 1) for p in bridges],
        },
    },
    "runs": runs,
    "sizes": {app: sizes[app] for app, _, _ in order},
}
json.dump(out, open(os.path.join(ROOT, "results.json"), "w"), indent=1)
for h in headline:
    print(h)
print(json.dumps(ratios_vs_next_term, indent=1))
