#!/usr/bin/env python3
"""Golden-vector generator for the Adelante rotation differential test.

Produces scenarios.json: each scenario is one widget publication (zone, publish
instant, cadence, daily minute, pool size, tiers, flags) plus query instants.
The generator only proposes *inputs*; no implementation's answer is computed here.
Deterministic: seeded PRNG, fixed zone list, fixed year range.
"""
import json, random, sys
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo

SEED = 20261102
rng = random.Random(SEED)

ZONES = [
    "UTC", "Asia/Kuala_Lumpur", "Asia/Singapore", "Asia/Kolkata", "Asia/Kathmandu",
    "America/New_York", "America/Los_Angeles", "America/St_Johns", "America/Santiago",
    "America/Havana", "America/Asuncion", "America/Sao_Paulo", "America/Nuuk",
    "Europe/London", "Europe/Berlin", "Europe/Dublin", "Europe/Moscow", "Atlantic/Azores",
    "Africa/Cairo", "Asia/Beirut", "Asia/Gaza", "Asia/Tehran", "Australia/Sydney",
    "Australia/Lord_Howe", "Pacific/Auckland", "Pacific/Chatham", "Pacific/Apia",
    "Pacific/Kiritimati", "Antarctica/Troll",
]
MODES = {"manual": 0, "hourly": 3600, "twiceDaily": 43200, "daily": 86400, "weekly": 604800}
MINUTES = [0, 1, 30, 59, 60, 90, 120, 150, 180, 540, 1380, 1410, 1439]
POOLS = [1, 2, 3, 5, 7, 24, 95, 96]
DAY = 86400_000

def ms(dt):
    return int(dt.timestamp() * 1000)

def transitions(zone, y0=2026, y1=2031):
    """UTC instants (ms) where the zone's UTC offset changes, to the second."""
    tz = ZoneInfo(zone)
    out = []
    t = datetime(y0, 1, 1, tzinfo=timezone.utc)
    end = datetime(y1, 1, 1, tzinfo=timezone.utc)
    prev = t.astimezone(tz).utcoffset()
    step = timedelta(hours=1)
    while t < end:
        n = t + step
        off = n.astimezone(tz).utcoffset()
        if off != prev:
            lo, hi = t, n
            while (hi - lo) > timedelta(seconds=1):
                mid = lo + (hi - lo) / 2
                if mid.astimezone(tz).utcoffset() == prev:
                    lo = mid
                else:
                    hi = mid
            out.append(ms(hi.replace(microsecond=0)))
            prev = off
        t = n
    return out

def local_minute_instants(zone, day_start_ms, days, minute):
    """Wall-clock minute on each local day as UTC instants (fold 0 and 1)."""
    tz = ZoneInfo(zone)
    base = datetime.fromtimestamp(day_start_ms / 1000, tz).date()
    out = []
    for d in range(days):
        day = base + timedelta(days=d)
        for fold in (0, 1):
            dt = datetime(day.year, day.month, day.day, minute // 60, minute % 60, tzinfo=tz, fold=fold)
            out.append(ms(dt))
    return out

def around(t):
    return [t - 3600_000, t - 1000, t - 1, t, t + 1, t + 999, t + 1000, t + 1800_000, t + 3600_000]

def tiers(n):
    return [rng.choice([0, 0, 1, 2]) for _ in range(n)]

scenarios = []
def add(**s):
    s["id"] = f"s{len(scenarios):05d}"
    q = sorted(set(int(x) for x in s["queries"] if x >= s["t0"] - DAY))
    s["queries"] = q
    scenarios.append(s)

for zone in ZONES:
    trans = transitions(zone)
    # Historical stress: Samoa skipped 30 Dec 2011; Cairo/Tehran/Sao_Paulo rule changes.
    if zone == "Pacific/Apia":
        trans += transitions(zone, 2011, 2013)
    if zone in ("Africa/Cairo", "America/Sao_Paulo", "Asia/Tehran"):
        trans += transitions(zone, 2017, 2024)
    trans = sorted(set(trans))
    anchors = trans[:] or [ms(datetime(2027, 3, 14, tzinfo=timezone.utc))]

    # A. Daily-with-time publications, list region (publish shortly before a transition).
    for tr in anchors:
        for minute in MINUTES:
            t0 = tr - rng.randint(1, 3) * DAY - rng.randint(0, DAY - 1)
            n = rng.choice(POOLS)
            q = []
            q += local_minute_instants(zone, t0, 8, minute)
            q += around(tr)
            q += [t0 + rng.randint(0, 10 * DAY) for _ in range(10)]
            qq = []
            for x in q:
                qq += [x - 1, x, x + 1] if rng.random() < 0.7 else [x]
            add(zone=zone, kind="daily", mode="daily", minute=minute, t0=t0, n=n,
                tiers=tiers(n), initialOnly=rng.random() < 0.35, sync=rng.random() < 0.5,
                queries=qq)
    # B. Daily-with-time, tail region: the 96 published dates run out across a transition.
    for tr in anchors:
        for minute in MINUTES:
            t0 = tr - (96 - rng.randint(-1, 4)) * DAY - rng.randint(0, DAY - 1)
            n = rng.choice(POOLS)
            q = local_minute_instants(zone, tr - 4 * DAY, 9, minute)
            q += around(tr)
            q += [t0 + 95 * DAY + rng.randint(0, 12 * DAY) for _ in range(12)]
            qq = []
            for x in q:
                qq += [x - 1, x, x + 1]
            add(zone=zone, kind="daily-tail", mode="daily", minute=minute, t0=t0, n=n,
                tiers=tiers(n), initialOnly=rng.random() < 0.35, sync=rng.random() < 0.5,
                queries=qq)
    # C. Interval cadences with millisecond publish instants.
    for _ in range(60):
        mode = rng.choice(["hourly", "twiceDaily", "daily", "weekly", "manual"])
        base = rng.choice(anchors) if rng.random() < 0.5 else ms(datetime(2026, 10, 4, tzinfo=timezone.utc)) + rng.randint(0, 4 * 365) * DAY
        t0 = base - rng.randint(0, 3 * DAY)
        n = rng.choice(POOLS)
        I = MODES[mode] * 1000 or 3600_000
        q = []
        for k in list(range(0, 6)) + [rng.randint(6, 2000) for _ in range(6)]:
            b = t0 + k * I
            q += [b - 1000, b - 501, b - 1, b, b + 1, b + 500]
        q += [t0 + rng.randint(0, 400 * DAY) for _ in range(6)]
        add(zone=zone, kind="interval", mode=mode, minute=None, t0=t0, n=n, tiers=tiers(n),
            initialOnly=rng.random() < 0.35, sync=rng.random() < 0.5, queries=q)
    # D. Long horizons (years later, incl. past 2038-01-19 for 32-bit epoch limits).
    for _ in range(8):
        mode = rng.choice(["hourly", "daily", "weekly"])
        dailytime = mode == "daily" and rng.random() < 0.6
        minute = rng.choice(MINUTES) if dailytime else None
        t0 = ms(datetime(2026, 10, 4, tzinfo=timezone.utc)) + rng.randint(0, 3 * 365) * DAY + rng.randint(0, DAY)
        n = rng.choice(POOLS)
        q = [t0 + int(y * 365.25 * DAY) + rng.randint(0, DAY) for y in (1, 2, 5, 11.5, 12, 13)]
        q += [ms(datetime(2038, 1, 19, 3, 14, 8, tzinfo=timezone.utc)), ms(datetime(2040, 2, 29, 12, tzinfo=timezone.utc))]
        add(zone=zone, kind="long", mode=mode, minute=minute, t0=t0, n=n, tiers=tiers(n),
            initialOnly=rng.random() < 0.35, sync=rng.random() < 0.5, queries=q)
    # E. Leap days.
    for yr in (2028, 2032):
        for minute in (0, 540, 1439):
            t0 = ms(datetime(yr, 2, 27, 12, tzinfo=ZoneInfo(zone)))
            n = rng.choice(POOLS)
            q = local_minute_instants(zone, t0, 5, minute) + [t0 + 97 * DAY + k * 3600_000 for k in range(0, 48, 5)]
            add(zone=zone, kind="leap", mode="daily", minute=minute, t0=t0, n=n, tiers=tiers(n),
                initialOnly=False, sync=True, queries=[x + d for x in q for d in (-1, 0, 1)])

json.dump({"seed": SEED, "zones": ZONES, "scenarios": scenarios}, open(sys.argv[1], "w"), separators=(",", ":"))
print(len(scenarios), "scenarios", sum(len(s["queries"]) for s in scenarios), "queries")
