"""
Núcleo de ML (sin framework web): Random Forest y Gradient Boosting con validación CRONOLÓGICA.
Para cada (sorteo t, número i) se predice si i sale en t usando solo sorteos < t.
"""
import numpy as np
from sklearn.ensemble import RandomForestClassifier, HistGradientBoostingClassifier
from sklearn.metrics import roc_auc_score, log_loss

WINDOWS = (10, 26, 52)


def build_features(M: np.ndarray, t: int, N: int, k: int) -> np.ndarray:
    """M: matriz (T, N) de 0/1. Características de todos los números para predecir el sorteo t."""
    feats = []
    past = M[:t]
    for w in WINDOWS:
        feats.append(past[-w:].mean(axis=0) if t > 0 else np.zeros(N))
    # retraso (sorteos desde la última aparición), acotado
    delay = np.full(N, float(t))
    for i in range(N):
        idx = np.nonzero(past[:, i])[0]
        if idx.size:
            delay[i] = t - 1 - idx[-1]
    feats.append(delay)
    feats.append(delay / (N / k))
    feats.append(past[-1] if t > 0 else np.zeros(N))            # salió en el último sorteo
    feats.append((np.arange(1, N + 1) % 2).astype(float))        # paridad
    feats.append(np.arange(1, N + 1) / N)                        # posición
    return np.vstack(feats).T                                    # (N, n_feat)


def to_matrix(draws, N):
    M = np.zeros((len(draws), N), dtype=np.int8)
    for t, d in enumerate(draws):
        for x in d:
            M[t, x - 1] = 1
    return M


def make_models(seed):
    return {
        "random_forest": RandomForestClassifier(
            n_estimators=200, max_depth=6, min_samples_leaf=80, n_jobs=-1, random_state=seed),
        "gradient_boosting": HistGradientBoostingClassifier(
            max_iter=120, learning_rate=0.05, max_depth=3, min_samples_leaf=80, random_state=seed),
    }


def run(draws, N, k, seed=1, val_frac=0.2, warmup=52):
    M = to_matrix(draws, N)
    T = M.shape[0]
    if T < warmup + 60:
        raise ValueError(f"Se necesitan al menos {warmup + 60} sorteos (hay {T}).")
    t_split = int(T - max(30, val_frac * (T - warmup)))
    X_all, y_all, t_all = [], [], []
    for t in range(warmup, T):
        X_all.append(build_features(M, t, N, k)); y_all.append(M[t]); t_all.append(np.full(N, t))
    X = np.vstack(X_all); y = np.concatenate(y_all); ts = np.concatenate(t_all)
    tr, va = ts < t_split, ts >= t_split
    base = k / N
    out = {}
    for name, model in make_models(seed).items():
        model.fit(X[tr], y[tr])
        p_va = np.clip(model.predict_proba(X[va])[:, 1], 1e-6, 1 - 1e-6)
        ll = log_loss(y[va], p_va, labels=[0, 1])
        ll_base = log_loss(y[va], np.full(p_va.shape, base), labels=[0, 1])
        try:
            auc = float(roc_auc_score(y[va], p_va))
        except ValueError:
            auc = 0.5
        # reentrenar con todo y predecir el próximo sorteo
        model.fit(X, y)
        p_next = np.clip(model.predict_proba(build_features(M, T, N, k))[:, 1], 1e-6, 1)
        p_next = p_next / p_next.sum()
        out[name] = {
            "probs": {str(i + 1): float(p) for i, p in enumerate(p_next)},
            "validacion": {
                "n_validacion_sorteos": int(T - t_split),
                "logloss": round(float(ll), 6),
                "logloss_azar": round(float(ll_base), 6),
                "mejora_vs_azar": round(float(ll_base - ll), 6),   # > 0 = mejor que azar
                "auc": round(auc, 4),                              # 0.5 = azar
                "lectura": "mejora_vs_azar <= 0 y AUC ~ 0.5 = el modelo no supera al azar",
            },
        }
    return {"models": out, "version": "ml_core-1"}
