"""Preregistered, finite-period schedule comparison. Standard library only."""
from collections import Counter, defaultdict
from datetime import date, datetime, timedelta, timezone
import csv
import gzip
import hashlib
import json
import math
from pathlib import Path
from statistics import fmean

ROOT = Path(__file__).resolve().parents[1]
START = datetime(2023, 1, 1, tzinfo=timezone.utc)
END = datetime(2026, 1, 1, tzinfo=timezone.utc)
SEASONS = ("DJF", "MAM", "JJA", "SON")
NAMES = ("midnight", "evening", "global", "seasonal", "hindsight")


def season(day):
    return SEASONS[(day.month % 12) // 3]


def parse_time(value):
    stamp = datetime.fromisoformat(value.replace("Z", "+00:00"))
    if stamp.tzinfo is None:
        raise ValueError("Timezone required")
    return stamp.astimezone(timezone.utc)


def ingest(rows):
    unique = {}
    duplicates = 0
    for row in rows:
        start = parse_time(row["from"])
        if not START <= start < END:
            continue
        end = parse_time(row["to"])
        if end - start != timedelta(minutes=30) or start.minute not in (0, 30) or start.second or start.microsecond:
            raise ValueError("Invalid interval: " + row["from"])
        if start in unique:
            if unique[start] != row:
                raise ValueError("Contradictory duplicate: " + row["from"])
            duplicates += 1
        unique[start] = row
    grouped = defaultdict(dict)
    for start, row in unique.items():
        grouped[start.date()][start.hour * 2 + start.minute // 30] = row["intensity"]["actual"]
    days, excluded = {}, []
    day = START.date()
    while day < END.date():
        slots = grouped.get(day, {})
        missing = sorted(set(range(48)) - slots.keys())
        invalid = sorted(k for k, value in slots.items() if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or value < 0)
        if missing or invalid:
            excluded.append({"date": str(day), "missing_slots": missing, "invalid_slots": invalid})
        else:
            days[day] = [slots[k] for k in range(48)]
        day += timedelta(days=1)
    return days, excluded, {"unique_intervals": len(unique), "identical_duplicates": duplicates}


def load():
    all_rows = []
    for source in json.loads((ROOT / "data/SOURCES.json").read_text()):
        packed = (ROOT / source["path"]).read_bytes()
        assert "sha256:" + hashlib.sha256(packed).hexdigest() == source["compressed_sha256"]
        raw = gzip.decompress(packed)
        assert "sha256:" + hashlib.sha256(raw).hexdigest() == source["sha256"]
        assert len(raw) == source["bytes"]
        all_rows.extend(json.loads(raw)["data"])
    return ingest(all_rows)


def window_sums(values):
    prefix = [0]
    for value in values:
        prefix.append(prefix[-1] + value)
    return [prefix[start + 6] - prefix[start] for start in range(43)]


def learn(days):
    training = {day: window_sums(values) for day, values in days.items() if day.year == 2023}
    selections = {}
    for name in ("global",) + SEASONS:
        rows = [v for day, v in training.items() if name == "global" or season(day) == name]
        if len(rows) < 60:
            raise ValueError("Insufficient training days for " + name)
        totals = [math.fsum(row[start] for row in rows) for start in range(43)]
        choice = min(range(43), key=lambda start: (totals[start], start))
        selections[name] = {"start_slot": choice, "start_utc": f"{choice // 2:02d}:{(choice % 2) * 30:02d}", "end_utc": f"{(choice + 6) // 2:02d}:{((choice + 6) % 2) * 30:02d}", "training_days": len(rows), "training_intensity_gco2_per_kwh": totals[choice] / (6 * len(rows))}
    return selections


def daily_metrics(days, selections):
    records = []
    for day, values in sorted(days.items()):
        if day.year not in (2024, 2025):
            continue
        windows = window_sums(values)
        record = {"date": str(day), "year": day.year, "month": day.month, "season": season(day), "seasonal_start_slot": selections[season(day)]["start_slot"], "global_start_slot": selections["global"]["start_slot"]}
        starts = {"midnight": 0, "evening": 36, "global": record["global_start_slot"], "seasonal": record["seasonal_start_slot"]}
        record.update({name: windows[start] / 6 for name, start in starts.items()})
        record["hindsight"] = min(windows) / 6
        records.append(record)
    return records


def contrast(means, baseline, treatment):
    absolute = means[baseline] - means[treatment]
    return {"absolute_reduction_gco2_per_kwh": absolute, "percent_reduction": 100 * absolute / means[baseline] if means[baseline] else None}


def summarize(rows):
    if not rows:
        return {"days": 0}
    means = {name: fmean(row[name] for row in rows) for name in NAMES}
    better = sum(row["seasonal"] < row["midnight"] for row in rows)
    equal = sum(row["seasonal"] == row["midnight"] for row in rows)
    worse = len(rows) - better - equal
    gap = means["midnight"] - means["hindsight"]
    return {"days": len(rows), "mean_intensity_gco2_per_kwh": means,
            "seasonal_vs_midnight": contrast(means, "midnight", "seasonal"),
            "global_vs_midnight": contrast(means, "midnight", "global"),
            "seasonal_vs_global": contrast(means, "global", "seasonal"),
            "seasonal_vs_evening": contrast(means, "evening", "seasonal"),
            "global_vs_evening": contrast(means, "evening", "global"),
            "midnight_vs_evening": contrast(means, "evening", "midnight"),
            "day_counts": {"better": better, "equal": equal, "worse": worse},
            "day_fractions": {"better": better / len(rows), "equal": equal / len(rows), "worse": worse / len(rows)},
            "hindsight_gap_fraction_captured": (means["midnight"] - means["seasonal"]) / gap if gap > 0 else None}


def main():
    output = ROOT / "results"
    output.mkdir(exist_ok=True)
    days, excluded, audit = load()
    selections = learn(days)
    (output / "selections.json").write_text(json.dumps(selections, indent=2) + "\n")
    rows = daily_metrics(days, selections)
    years, coverage = {}, {}
    for year in (2023, 2024, 2025):
        eligible = sum(day.year == year for day in days)
        expected = (date(year + 1, 1, 1) - date(year, 1, 1)).days
        coverage[str(year)] = {"complete_days": eligible, "expected_days": expected, "fraction": eligible / expected, "passes_95pct": eligible / expected >= .95}
        if year > 2023:
            annual = summarize([row for row in rows if row["year"] == year])
            annual["monthly"] = {str(month): summarize([row for row in rows if row["year"] == year and row["month"] == month]) for month in range(1, 13)}
            monthly_reductions = [x["seasonal_vs_midnight"]["percent_reduction"] for x in annual["monthly"].values() if x["days"]]
            annual["monthly_reduction_min"] = min(monthly_reductions)
            annual["monthly_reduction_max"] = max(monthly_reductions)
            annual["months_improved"] = sum(x > 0 for x in monthly_reductions)
            years[str(year)] = annual
    results = {"units": "gCO2/kWh", "primary_test_year": 2025, "training_year": 2023, "coverage": coverage, "audit": audit, "selections": selections, "years": years,
               "excluded_days": len(excluded), "excluded_by_month": dict(sorted(Counter(x["date"][:7] for x in excluded).items())),
               "uncertainty_scope": "Finite observed-period summaries; no inferential interval or causal emissions claim. Provider estimation and revision error are unquantified."}
    (output / "R1.json").write_text(json.dumps(results, indent=2, allow_nan=False) + "\n")
    def display(value):
        if isinstance(value, float):
            return round(value, 2)
        if isinstance(value, dict):
            return {key: display(item) for key, item in value.items()}
        if isinstance(value, list):
            return [display(item) for item in value]
        return value
    (output / "R2.json").write_text(json.dumps(display(results), indent=2, allow_nan=False) + "\n")
    (output / "exclusions.json").write_text(json.dumps(excluded, indent=2) + "\n")
    with (output / "daily.csv").open("w", newline="") as handle:
        writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
        writer.writeheader()
        writer.writerows(rows)
    print(json.dumps({"coverage": coverage, "selections": selections, "annual": {k: {x: v for x, v in year.items() if x != "monthly"} for k, year in years.items()}}, indent=2))


if __name__ == "__main__":
    main()
