#!/usr/bin/env python3
"""Atelier pédagogique local. Python 3.9+, aucune dépendance ni connexion."""
import copy
import json
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo

PARIS = ZoneInfo("Europe/Paris")
NOW = datetime(2026, 11, 2, 9, 0, tzinfo=PARIS)
CUTOFF = datetime(2026, 11, 2, 0, 0, tzinfo=PARIS)
DEADLINE = datetime(2026, 11, 2, 8, 30, tzinfo=PARIS)
STORES = ("Lille", "Lyon", "Nantes")


def iso(value):
    return value.astimezone(timezone.utc).isoformat()


def dt(value):
    parsed = datetime.fromisoformat(value)
    if parsed.tzinfo is None:
        raise ValueError("Horodatage sans fuseau")
    return parsed


def baseline():
    # Une ligne peut représenter ici un total synthétique, pas une vraie vente.
    rows = [{"store": s, "batch_id": "B-" + s,
             "event_at": iso(CUTOFF - timedelta(minutes=1)),
             "ingested_at": iso(NOW - timedelta(minutes=5))} for s in STORES]
    batches = [{"store": s, "batch_id": "B-" + s, "status": "complete",
                "covered_until": iso(CUTOFF), "expected_rows": 1} for s in STORES]
    return {"rows": rows, "batches": batches,
            "snapshot_generated_at": iso(NOW - timedelta(minutes=2)),
            "snapshot_batches": ["B-" + s for s in STORES]}


def check(data, now=NOW):
    reasons = []
    for store in STORES:
        candidates = [b for b in data["batches"] if b["store"] == store
                      and dt(b["covered_until"]) == CUTOFF]
        if len(candidates) != 1:
            reasons.append(store + ": lot cible absent ou ambigu")
            continue
        batch = candidates[0]
        actual = sum(r["store"] == store and r["batch_id"] == batch["batch_id"]
                     for r in data["rows"])
        if batch["status"] != "complete":
            reasons.append(store + ": lot non terminé")
        if actual != batch["expected_rows"]:
            reasons.append(store + ": compte de lignes différent du manifeste source")
        if batch["batch_id"] not in data["snapshot_batches"]:
            reasons.append(store + ": lot absent du snapshot BI")
    if any(dt(r[key]) > now for r in data["rows"] for key in ("event_at", "ingested_at")):
        reasons.append("horodatage de ligne futur")
    generated = dt(data["snapshot_generated_at"])
    if generated > now or now - generated >= timedelta(minutes=15):
        reasons.append("snapshot BI futur ou trop ancien")
    return {"decision": "ready" if not reasons else ("awaiting" if now < DEADLINE else "not_ready"),
            "reasons": reasons,
            "latest_ingestion": max((r["ingested_at"] for r in data["rows"]), default=None),
            "latest_event": max((r["event_at"] for r in data["rows"]), default=None),
            "delivery_deadline_met": None}


def scenarios():
    good = baseline()
    missing = copy.deepcopy(good)
    missing["batches"] = [b for b in missing["batches"] if b["store"] != "Nantes"]
    replay = copy.deepcopy(good)
    replay["batches"][-1]["covered_until"] = iso(CUTOFF - timedelta(days=1))
    replay["rows"][-1]["event_at"] = iso(CUTOFF - timedelta(days=1, minutes=1))
    empty = copy.deepcopy(good)
    empty["rows"] = [r for r in empty["rows"] if r["store"] != "Nantes"]
    empty["batches"][-1]["expected_rows"] = 0
    partial = copy.deepcopy(good)
    partial["batches"][-1]["expected_rows"] = 2
    stale = copy.deepcopy(good)
    stale["snapshot_generated_at"] = iso(NOW - timedelta(hours=1))
    future = copy.deepcopy(good)
    future["rows"][-1]["event_at"] = iso(NOW + timedelta(hours=1))
    old_snapshot = copy.deepcopy(good)
    old_snapshot["snapshot_batches"].remove("B-Nantes")
    return [("lot_complet", good, "ready"),
            ("magasin_absent", missing, "not_ready"),
            ("rejeu_ancien", replay, "not_ready"),
            ("zero_vente_confirme", empty, "ready"),
            ("lot_partiel", partial, "not_ready"),
            ("dashboard_ancien", stale, "not_ready"),
            ("horloge_future", future, "not_ready"),
            ("snapshot_sans_lot", old_snapshot, "not_ready")]


if __name__ == "__main__":
    results = []
    for name, data, expected in scenarios():
        result = check(data)
        assert result["decision"] == expected, (name, result)
        results.append({"scenario": name, **result})
    pending_data = copy.deepcopy(scenarios()[1][1])
    early = NOW - timedelta(hours=1)
    for row in pending_data["rows"]:
        row["ingested_at"] = iso(early - timedelta(minutes=5))
    pending_data["snapshot_generated_at"] = iso(early - timedelta(minutes=2))
    pending = check(pending_data, early)
    assert pending["decision"] == "awaiting"
    # Une heure locale sans décalage est refusée, jamais interprétée en UTC.
    try:
        dt("2026-11-02T08:55:00")
        raise AssertionError("Un fuseau manquant doit être refusé")
    except ValueError:
        pass
    print(json.dumps({"checked_at": iso(NOW), "target_cutoff": iso(CUTOFF),
                      "deadline": iso(DEADLINE), "scenarios": results,
                      "additional_checks": ["awaiting_before_deadline", "reject_naive_timestamp"]},
                     ensure_ascii=False, indent=2))
