#!/usr/bin/env python3
"""Bounded local experiment with fixed patterns and synthetic strings.

Usage: python3.12 regex-backtracking-lab.py [output-directory]
Python 3.11+ is required for atomic groups and possessive quantifiers.
Only the standard library is used. No network/service tests are performed.
"""

import csv
import itertools
import json
from pathlib import Path
import platform
import re
import statistics
import subprocess
import sys
import time

PATTERNS = {
    "nested": r"(a+)+",
    "simple": r"a+",
    "atomic": r"(?>a+)+",
    "possessive": r"a++",
}
LENGTHS = (16, 18, 20, 22, 24)
REPEATS = 3


def worker(label, n, kind):
    pattern = re.compile(PATTERNS[label])
    text = "a" * n + ("!" if kind == "near_miss" else "")
    timings = []
    for _ in range(REPEATS):
        start = time.perf_counter_ns()
        match = pattern.fullmatch(text)
        timings.append((time.perf_counter_ns() - start) / 1e6)
        assert (match is not None) == (kind == "valid")
    return {"pattern": label, "n_a": n, "input_kind": kind,
            "input_characters": len(text), "status": "completed",
            "matched": match is not None, "samples_ms": timings,
            "median_ms": statistics.median(timings)}


def run_case(label, n, kind, wait_timeout):
    command = [sys.executable, str(Path(__file__).resolve()),
               "--worker", label, str(n), kind]
    try:
        result = subprocess.run(command, text=True, capture_output=True,
                                check=True, timeout=wait_timeout)
    except subprocess.TimeoutExpired:
        return {"pattern": label, "n_a": n, "input_kind": kind,
                "input_characters": n + (kind == "near_miss"),
                "status": "terminated_after_subprocess_timeout", "matched": None,
                "parent_communication_timeout_seconds": wait_timeout,
                "note": "Communication wait includes remaining child initialization and calls; process creation and cleanup may add wall time. No completed match time or boolean result."}
    return json.loads(result.stdout)


def main():
    output = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("regex-backtracking-output")
    output.mkdir(parents=True, exist_ok=True)
    measurements = []
    for label in PATTERNS:
        for kind in ("valid", "near_miss"):
            for n in LENGTHS:
                row = run_case(label, n, kind, wait_timeout=5)
                assert row["status"] == "completed", row
                measurements.append(row)

    compiled = {label: re.compile(pattern) for label, pattern in PATTERNS.items()}
    samples = ["", "a", "aa", "a!", "aa!", "a\n"]
    sample_checks = [{"input_repr": repr(text), "matched": {
        label: p.fullmatch(text) is not None for label, p in compiled.items()}}
        for text in samples]
    tested, mismatches, accepted = 0, [], []
    for length in range(6):
        for chars in itertools.product(("a", "b", "!", "\n"), repeat=length):
            text = "".join(chars)
            verdicts = [p.fullmatch(text) is not None for p in compiled.values()]
            expected = bool(text) and set(text) == {"a"}
            tested += 1
            if any(value != expected for value in verdicts):
                mismatches.append(repr(text))
            if expected:
                accepted.append(text)
    assert tested == 1365 and not mismatches
    captures = {label: list(p.fullmatch("aaa").groups()) for label, p in compiled.items()}
    counterexample = {pattern: re.fullmatch(pattern, "aa") is not None
                      for pattern in (r"a+a", r"a++a")}
    assert counterexample == {"a+a": True, "a++a": False}
    stopped = run_case("nested", 32, "near_miss", wait_timeout=2)

    fields = ["pattern", "input_kind", "n_a", "input_characters", "matched", "median_ms",
              "trial_1_ms", "trial_2_ms", "trial_3_ms"]
    with (output / "timings.csv").open("w", newline="", encoding="utf-8") as file:
        writer = csv.DictWriter(file, fieldnames=fields, lineterminator="\n")
        writer.writeheader()
        for row in measurements:
            flat = {name: row[name] for name in fields[:6]}
            flat.update({f"trial_{i}_ms": value for i, value in enumerate(row["samples_ms"], 1)})
            writer.writerow(flat)
    report = {
        "experiment": "fixed_regex_patterns_and_synthetic_strings",
        "date": "2026-10-11",
        "environment": {"python": platform.python_version(),
                        "implementation": platform.python_implementation(),
                        "macos": platform.mac_ver()[0], "machine": platform.machine(),
                        "engine": "stdlib re; compiled Pattern.fullmatch; no explicit flags"},
        "patterns": PATTERNS,
        "measurement": {"repeats": REPEATS, "summary": "median",
                        "unit": "milliseconds", "timer": "perf_counter_ns",
                        "includes_compilation_input_creation_or_child_startup": False,
                        "ordinary_case_parent_communication_timeout_seconds": 5},
        "measurements": measurements,
        "sample_checks": sample_checks,
        "finite_equivalence_check": {"alphabet": ["a", "b", "!", "\n"],
                                     "lengths": "0 through 5", "tested": tested,
                                     "mismatches": mismatches, "accepted": accepted},
        "captures_for_aaa": captures,
        "possessive_counterexample_on_aa": counterexample,
        "bounded_longer_case": stopped,
        "scope": "No arbitrary user regex, external endpoint, alternate engine, throughput or load tests.",
    }
    (output / "results.json").write_text(
        json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8",
    )
    print(json.dumps({"environment": report["environment"],
                      "nested_timings": [r for r in measurements if r["pattern"] == "nested"],
                      "captures": captures, "counterexample": counterexample,
                      "finite_tested": tested, "bounded_longer_case": stopped}, indent=2))


if __name__ == "__main__":
    if sys.version_info < (3, 11):
        raise SystemExit("Use Python 3.11 or newer; the article used Python 3.12.14.")
    if len(sys.argv) > 1 and sys.argv[1] == "--worker":
        print(json.dumps(worker(sys.argv[2], int(sys.argv[3]), sys.argv[4])))
    else:
        main()
