"""Synthetic Gaussian leakage experiment; no cryptographic implementation is timed."""

import argparse
import csv
import math
from pathlib import Path
import random


COUNTS = (1, 10, 100, 1000, 10000, 100000)
DELTA = 0.02
SIGMA = 1.0
TRIALS = 20000
SEED = 20260929


def error_probability(count, delta, rho):
    deviation = SIGMA * math.sqrt(rho + (1.0 - rho) / count)
    return 0.5 * math.erfc(delta / (2.0 * math.sqrt(2.0) * deviation))


def wilson_interval(errors, trials):
    probability = errors / trials
    quantile = 1.959963984540054
    denominator = 1.0 + quantile**2 / trials
    center = (probability + quantile**2 / (2.0 * trials)) / denominator
    radius = quantile * math.sqrt(
        probability * (1.0 - probability) / trials + quantile**2 / (4.0 * trials**2)
    ) / denominator
    return center - radius, center + radius


def experiment():
    generator = random.Random(SEED)
    rows = []
    profiles = (("iid", DELTA, 0.0), ("correlated", DELTA, 0.01), ("no_leak", 0.0, 0.0))
    for profile, delta, rho in profiles:
        for count in COUNTS:
            deviation = SIGMA * math.sqrt(rho + (1.0 - rho) / count)
            errors = 0
            for _ in range(TRIALS):
                secret_bit = generator.getrandbits(1)
                sample_mean = generator.gauss(delta * secret_bit, deviation)
                guess = int(sample_mean >= delta / 2.0)
                errors += guess != secret_bit
            lower, upper = wilson_interval(errors, TRIALS)
            rows.append({
                "profile": profile,
                "samples_per_trial": count,
                "delta": delta,
                "sigma": SIGMA,
                "rho": rho,
                "trials": TRIALS,
                "seed": SEED,
                "errors": errors,
                "empirical_error": errors / TRIALS,
                "analytic_error": error_probability(count, delta, rho),
                "wilson_lower": lower,
                "wilson_upper": upper,
            })
    return rows


def plot_results(rows, directory):
    import matplotlib

    matplotlib.use("Agg")
    import matplotlib.pyplot as plt

    figure, axis = plt.subplots(figsize=(10, 5.8), constrained_layout=True)
    colors = {"iid": "#007c68", "correlated": "#9d4528", "no_leak": "#626774"}
    labels = {"iid": "Independent noise", "correlated": "Correlated noise (rho = 0.01)", "no_leak": "No-leak control"}
    for profile in colors:
        selected = [row for row in rows if row["profile"] == profile]
        counts = [row["samples_per_trial"] for row in selected]
        estimates = [row["empirical_error"] for row in selected]
        lower = [row["empirical_error"] - row["wilson_lower"] for row in selected]
        upper = [row["wilson_upper"] - row["empirical_error"] for row in selected]
        axis.plot(counts, [row["analytic_error"] for row in selected], color=colors[profile], label=labels[profile])
        axis.errorbar(counts, estimates, yerr=[lower, upper], fmt="o", markersize=4, capsize=3, color=colors[profile])
    axis.set_xscale("log")
    axis.set_ylim(0.0, 0.54)
    axis.set_xlabel("Observations of the same hidden bit per trial (log scale)")
    axis.set_ylabel("Probability of an incorrect bit guess")
    axis.set_title("Small timing differences accumulate under repeated observation", loc="left", pad=16)
    axis.grid(alpha=0.2)
    axis.legend(loc="lower left")
    figure.supxlabel("Synthetic Gaussian model | delta/sigma = 0.02 | 20,000 trials per point\nLines: analytic error. Points: Monte Carlo. Bars: pointwise 95% Wilson intervals.", fontsize=10)
    figure.savefig(directory / "leakage-accumulation.png", dpi=180)
    plt.close(figure)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--output-dir", type=Path, default=Path(__file__).resolve().parent)
    parser.add_argument("--plot", action="store_true", help="Also generate the figure; requires matplotlib.")
    arguments = parser.parse_args()
    arguments.output_dir.mkdir(parents=True, exist_ok=True)
    rows = experiment()
    with (arguments.output_dir / "leakage-results.csv").open("w", newline="", encoding="utf-8") as target:
        writer = csv.DictWriter(target, fieldnames=list(rows[0]))
        writer.writeheader()
        writer.writerows(rows)
    if arguments.plot:
        plot_results(rows, arguments.output_dir)
    for row in rows:
        print(f"{row['profile']:10} N={row['samples_per_trial']:6} empirical={row['empirical_error']:.5f} analytic={row['analytic_error']:.5f}")


if __name__ == "__main__":
    main()
