#!/usr/bin/env python3
"""Construit fenetres.json pour le jeu « Vrai ou mélangé ? » / « Real or shuffled? ».

Usage : python3 build_fenetres.py ARCHIVE.csv SORTIE.json
Entrée : bougies BTCUSDT 4 h (timestamp,open,high,low,close,volume), Binance Vision, CC BY-NC-SA 4.0.
Bibliothèque standard seulement. Même entrée et même graine : même fichier, à l'octet près.

Méthode
- Seules les bougies antérieures à COUPURE sont lues.
- L'historique est coupé en NB_FENETRES tranches égales, avec une fenêtre de NB_BOUGIES bougies
  consécutives tirée dans chaque tranche. Les fenêtres ne se recouvrent donc jamais.
- Par paire de tranches voisines, une fenêtre reste réelle et l'autre est mélangée (tirage), pour que
  les deux groupes couvrent les mêmes périodes du marché.
- Chaque bougie est décrite par cinq valeurs : écart d'ouverture (ouverture / clôture précédente),
  corps (clôture / ouverture), mèche haute, mèche basse, volume. L'écart de la première bougie vaut 0.
- Mélange : une seule permutation par fenêtre, appliquée aux cinq valeurs ensemble. Chaque bougie garde
  ses mèches, son corps et son volume ; seul l'ordre change. Le dernier cours reste celui de la fenêtre,
  à l'écart de la nouvelle première bougie près (quasi nul : l'ouverture suit en général la clôture précédente).
- Réelles et mélangées suivent le même chemin de calcul : ouverture initiale = 10000, valeurs arrondies
  à l'entier, volume exprimé de 0 à 99 par rapport au maximum de la fenêtre. Ni date ni prix absolu.
- La réponse n'apparaît pas en clair : voir cle_reponse(). Ce n'est pas une protection, seulement de quoi
  éviter de la lire dans le fichier ; avec ce script et la graine, tout se recalcule.
"""
import csv, hashlib, json, math, random, sys
from datetime import datetime, timedelta

GRAINE = 20261102
COUPURE = "2026-10-17T00:00:00Z"
NB_FENETRES = 36
NB_BOUGIES = 96
PAS = timedelta(hours=4)


def melanger(suite, rng):
    """Fisher-Yates sur rng.random(), dont la suite est garantie stable par la documentation Python."""
    suite = list(suite)
    for i in range(len(suite) - 1, 0, -1):
        j = int(rng.random() * (i + 1))
        suite[i], suite[j] = suite[j], suite[i]
    return suite


def lire(chemin):
    lignes = []
    with open(chemin, newline="") as f:
        for r in csv.DictReader(f):
            if r["timestamp"] >= COUPURE:
                continue
            lignes.append((datetime.strptime(r["timestamp"], "%Y-%m-%dT%H:%M:%SZ"),
                           float(r["open"]), float(r["high"]), float(r["low"]), float(r["close"]), float(r["volume"])))
    return lignes


def valide(fen):
    for a, b in zip(fen, fen[1:]):
        if b[0] - a[0] != PAS:
            return False
    return all(0 < l <= min(o, c) and h >= max(o, c) and v > 0 for _, o, h, l, c, v in fen)


def decomposer(fen):
    barres = []
    prec = fen[0][1]
    for _, o, h, l, c, v in fen:
        barres.append((math.log(o / prec), math.log(c / o), math.log(h / max(o, c)), math.log(min(o, c) / l), v))
        prec = c
    return barres


def reconstruire(barres):
    """Depuis 10000 ; retourne la suite plate o,h,l,c,v d'entiers."""
    vmax = max(b[4] for b in barres)
    plat = []
    cours = 10000.0 / math.exp(barres[0][0])  # première ouverture = 10000 pour toutes les fenêtres
    for ecart, corps, haut, bas, v in barres:
        o = cours * math.exp(ecart)
        c = o * math.exp(corps)
        h = max(o, c) * math.exp(haut)
        l = min(o, c) / math.exp(bas)
        plat += [round(o), round(h), round(l), round(c), min(99, int(v / vmax * 100))]
        cours = c
    return plat


def empreinte_fnv(texte):
    h = 0x811C9DC5
    for octet in texte.encode():
        h = ((h ^ octet) * 0x01000193) & 0xFFFFFFFF
    return h


def cle_reponse(ident, plat, melangee, rng):
    """Bit 7 de (clé XOR empreinte des données) : 1 = mélangée, 0 = réelle. Le reste de la clé est aléatoire."""
    h = empreinte_fnv(ident + ":" + ",".join(map(str, plat)))
    cle = int(rng.random() * 2 ** 32)
    bit = ((cle ^ h) >> 7) & 1
    return cle if bit == int(melangee) else cle ^ 0x80


def construire(chemin, trace=None):
    with open(chemin, "rb") as f:
        sha = hashlib.sha256(f.read()).hexdigest()
    lignes = lire(chemin)
    rng = random.Random(GRAINE)
    tranche = len(lignes) // NB_FENETRES
    fenetres = []
    for t in range(NB_FENETRES):
        debut, fin = t * tranche, (t + 1) * tranche
        for _ in range(10000):
            d = debut + int(rng.random() * (fin - debut - NB_BOUGIES + 1))
            fen = lignes[d:d + NB_BOUGIES]
            if valide(fen):
                fenetres.append(fen)
                break
        else:
            raise SystemExit(f"aucune fenêtre valide dans la tranche {t}")
    sortie = []
    for p in range(0, NB_FENETRES, 2):
        melangee_a = rng.random() < 0.5
        for fen, m in ((fenetres[p], melangee_a), (fenetres[p + 1], not melangee_a)):
            barres = decomposer(fen)
            ordre = list(range(NB_BOUGIES))
            if m:
                ordre = melanger(range(NB_BOUGIES), rng)
                barres = [barres[i] for i in ordre]
            plat = reconstruire(barres)
            ident = f"{int(rng.random() * 2 ** 32):08x}"
            sortie.append({"i": ident, "k": cle_reponse(ident, plat, m, rng), "d": plat})
            if trace is not None:  # contrôle seulement, jamais écrit dans le fichier publié
                trace[ident] = {"fenetre": fen, "melangee": m, "ordre": ordre}
    sortie = melanger(sortie, rng)
    return {
        "meta": {
            "graine": GRAINE, "coupure": COUPURE, "bougies": NB_BOUGIES, "unite": "4h", "actif": "BTCUSDT",
            "archive_sha256": sha,
            "licence": "Binance Vision, CC BY-NC-SA 4.0 ; données normalisées et, pour une partie, réordonnées",
        },
        "fenetres": sortie,
    }


if __name__ == "__main__":
    if len(sys.argv) != 3:
        raise SystemExit("usage : build_fenetres.py ARCHIVE.csv SORTIE.json")
    with open(sys.argv[2], "w", encoding="utf-8") as f:
        json.dump(construire(sys.argv[1]), f, ensure_ascii=False, separators=(",", ":"))
        f.write("\n")
