#!/usr/bin/env python3
"""Scale sweep: identify how chunk size/thread count behave as work grows."""
import csv, json, os, platform, statistics, subprocess, time
from datetime import datetime, timezone
from pathlib import Path

HERE = Path(__file__).resolve().parent
SOURCE = HERE.parent / "cpu-sieve-calibration" / "sieve.c"
SIZES = [1_000_000, 5_000_000, 25_000_000, 50_000_000]
KNOWN_PI = {1_000_000: 78_498, 5_000_000: 348_513,
            25_000_000: 1_565_927, 50_000_000: 3_001_134}
SEGMENTS = [16_384, 65_536, 262_144, 1_048_576]
THREADS = [1, 2]
REPEATS = 3

def run(binary, n, segment, threads):
    start = time.perf_counter()
    p = subprocess.run([str(binary), str(n), str(segment), str(threads)],
                       text=True, capture_output=True, timeout=45, check=True)
    return json.loads(p.stdout), time.perf_counter() - start

def main():
    started = datetime.now(timezone.utc).isoformat()
    binary = HERE / "sieve"
    c = subprocess.run(["gcc", "-O3", "-march=native", "-pthread",
                        str(SOURCE), "-o", str(binary)], text=True, capture_output=True)
    if c.returncode:
        raise RuntimeError("Compile failed: " + c.stderr)
    rows = []
    for n in SIZES:
        observed = []
        for nt in THREADS:
            for seg in SEGMENTS:
                times, counts = [], []
                for rep in range(REPEATS):
                    out, sec = run(binary, n, seg, nt)
                    times.append(sec)
                    counts.append(out["prime_count"])
                if len(set(counts)) != 1:
                    raise RuntimeError("Count varied across repetitions")
                if counts[0] != KNOWN_PI[n]:
                    raise RuntimeError("Reference count mismatch at N=%d: %d != %d" %
                                       (n, counts[0], KNOWN_PI[n]))
                med = statistics.median(times)
                row = {"limit": n, "threads": nt, "segment_size": seg,
                       "prime_count": counts[0], "repeats": REPEATS,
                       "median_seconds": med, "mean_seconds": statistics.mean(times),
                       "stdev_seconds": statistics.pstdev(times),
                       "candidates_per_second": (n - 1) / med,
                       "raw_seconds": times}
                rows.append(row)
                observed.append(row)
                print("N=%d threads=%d segment=%d median=%.4fs" %
                      (n, nt, seg, med), flush=True)
        best = max(observed, key=lambda x: x["candidates_per_second"])
        one = max((x for x in observed if x["threads"] == 1),
                  key=lambda x: x["candidates_per_second"])
        two = max((x for x in observed if x["threads"] == 2),
                  key=lambda x: x["candidates_per_second"])
        print("SCALE_RESULT N=%d best=(threads %d, segment %d); speedup=%.3fx" %
              (n, best["threads"], best["segment_size"], one["median_seconds"] / two["median_seconds"]),
              flush=True)

    finished = datetime.now(timezone.utc).isoformat()
    summaries = []
    for n in SIZES:
        group = [x for x in rows if x["limit"] == n]
        best = max(group, key=lambda x: x["candidates_per_second"])
        one = max((x for x in group if x["threads"] == 1),
                  key=lambda x: x["candidates_per_second"])
        two = max((x for x in group if x["threads"] == 2),
                  key=lambda x: x["candidates_per_second"])
        summaries.append({"limit": n, "best": best, "best_single": one, "best_dual": two,
                          "dual_thread_speedup": one["median_seconds"] / two["median_seconds"]})
    obj = {"experiment": "cpu-scaling-study", "started_utc": started,
           "finished_utc": finished, "host": platform.node(),
           "platform": platform.platform(), "logical_cpus": os.cpu_count(),
           "segments": SEGMENTS, "threads": THREADS, "repeats_per_configuration": REPEATS,
           "reference_prime_counts": KNOWN_PI, "samples": rows, "summaries": summaries,
           "hypothesis": "The best chunk size and benefit of two threads may depend on workload scale.",
           "limitations": ["Results characterize this host and this segmented-sieve workload only.",
                           "Three repeats per configuration are an initial estimate.",
                           "No inference about LLM performance should be drawn from sieve performance."]}
    (HERE / "results.json").write_text(json.dumps(obj, indent=2) + "\n")
    with (HERE / "results.csv").open("w", newline="") as f:
        fields = ["limit", "threads", "segment_size", "prime_count", "median_seconds",
                  "mean_seconds", "stdev_seconds", "candidates_per_second", "raw_seconds"]
        w = csv.DictWriter(f, fieldnames=fields); w.writeheader()
        for r in rows:
            q = dict(r); q["raw_seconds"] = ";".join("%.6f" % v for v in r["raw_seconds"])
            w.writerow({k: q[k] for k in fields})
    lines = ["# CPU scaling study — measured results", "",
             "- Window (UTC): %s to %s" % (started, finished),
             "- Host: %s; logical CPUs: %s" % (platform.platform(), os.cpu_count()),
             "- Workload sizes: %s; 8 configurations per size; %d repetitions per configuration." %
             (", ".join(format(n, ",") for n in SIZES), REPEATS),
             "- Independent reference prime counts matched at every workload size.", "",
             "## Best measured settings by scale", "",
             "| Limit | Prime count | Best threads | Best segment | Median seconds | 2-thread speedup vs best 1-thread |",
             "|---:|---:|---:|---:|---:|---:|"]
    for s in summaries:
        b = s["best"]
        lines.append("| %s | %s | %d | %s B | %.4f | %.3f× |" %
                     (format(s["limit"], ","), format(b["prime_count"], ","),
                      b["threads"], format(b["segment_size"], ","),
                      b["median_seconds"], s["dual_thread_speedup"]))
    lines += ["", "## Interpretation", "",
              "- Compare the full results.csv before choosing a default; the best configuration can differ by scale.",
              "- Use these measurements to configure local batch-oriented searches, while keeping thread count and segment size adjustable.",
              "- Repeat the sweep in a separate session before relying on small differences.",
              "", "## Limits", "",
              "- This produces host-specific systems knowledge, not a new mathematical theorem.",
              "- Prime counts are checked against known reference values and must match across all configurations.",
              "- No packages were installed and no service or recurring job was created."]
    (HERE / "report.md").write_text("\n".join(lines) + "\n")
    print("FINAL_RESULT " + json.dumps({"report": str(HERE / "report.md"),
          "results_json": str(HERE / "results.json"), "summaries": summaries}), flush=True)

if __name__ == "__main__":
    main()
