"""
SPY Optimizer — Adaptive Daily Retrainer
Se recalibre automatiquement chaque jour sur les conditions de marché récentes.

Architecture:
  - Entraîne sur les N derniers jours (rolling window)
  - Détecte les changements de régime (bull/bear/range)
  - Ajuste les hyperparamètres quand la performance se dégrade
  - Sauvegarde l'historique des modèles pour rollback

Usage:
  # Cron quotidien (ex: 00:05 UTC)
  python daily_retrainer.py

  # Ou manuellement
  python daily_retrainer.py --force --window-days 10
"""
import argparse
import json
import os
import sys
from datetime import datetime, timezone, timedelta
from pathlib import Path

import pandas as pd

# Add parent dir for imports
sys.path.insert(0, str(Path(__file__).parent.parent))

from signal_classifier import SignalClassifier
from feature_engineering import build_dataset_from_trades

PROJECT_DIR = Path(__file__).parent
MODELS_DIR = PROJECT_DIR / "models"
DATA_DIR = PROJECT_DIR / "data"
KLINES_DIR = DATA_DIR / "klines_1m"
RETRAINER_STATE_FILE = DATA_DIR / "retrainer_state.json"
HISTORY_FILE = Path(__file__).parent.parent / "espion_history.json"


def load_retrainer_state() -> dict:
    if RETRAINER_STATE_FILE.exists():
        return json.loads(RETRAINER_STATE_FILE.read_text())
    return {"last_retrain": None, "history": []}


def save_retrainer_state(state: dict):
    DATA_DIR.mkdir(parents=True, exist_ok=True)
    RETRAINER_STATE_FILE.write_text(json.dumps(state, indent=2))


def load_trades(min_date: str = None) -> list[dict]:
    """Charge les trades avec surge_type depuis espion_history.json."""
    if not HISTORY_FILE.exists():
        raise FileNotFoundError(f"Historique introuvable: {HISTORY_FILE}")

    with open(HISTORY_FILE) as f:
        all_trades = json.load(f)

    # Filtrer: uniquement les trades avec surge_type (post-refactor, features complètes)
    trades = [t for t in all_trades if t.get("surge_type") and t.get("entry_time")]

    if min_date:
        trades = [t for t in trades if t.get("entry_time", "") >= min_date]

    return trades


def should_retrain(state: dict, force: bool = False) -> tuple[bool, str]:
    """Détermine si un retraining est nécessaire."""
    if force:
        return True, "forced"

    last = state.get("last_retrain")
    if not last:
        return True, "first_time"

    last_dt = datetime.fromisoformat(last)
    now = datetime.now(timezone.utc)
    hours_since = (now - last_dt).total_seconds() / 3600

    if hours_since >= 24:
        return True, f"scheduled ({hours_since:.0f}h since last)"

    # Vérifier si la performance s'est dégradée
    recent = state.get("history", [])[-5:]
    if len(recent) >= 3:
        recent_wins = [h.get("val_win_rate", 50) for h in recent]
        if all(wr < 45 for wr in recent_wins[-3:]):
            return True, f"performance_degradation (WR: {recent_wins[-3:]}"

    return False, f"not needed ({hours_since:.0f}h since last)"


def retrain(
    window_days: int = 10,
    n_optuna_trials: int = 80,
    verbose: bool = True,
) -> dict:
    """
    Exécute un cycle de retraining.

    Args:
        window_days: nombre de jours d'historique pour l'entraînement
        n_optuna_trials: nombre d'essais d'optimisation Optuna
        verbose: affichage détaillé

    Returns:
        dict avec les résultats du retraining
    """
    now = datetime.now(timezone.utc)
    min_date = (now - timedelta(days=window_days + 5)).strftime("%Y-%m-%d")

    if verbose:
        print(f"\n{'='*60}")
        print(f"  🔄 Daily Retraining — {now.strftime('%Y-%m-%d %H:%M UTC')}")
        print(f"  Window: {window_days} jours | Optuna: {n_optuna_trials} trials")
        print(f"{'='*60}")

    # Charger les trades
    trades = load_trades(min_date=min_date)
    if verbose:
        print(f"  Trades chargés: {len(trades)}")

    if len(trades) < 50:
        msg = f"Pas assez de trades ({len(trades)} < 50) pour retraining"
        if verbose:
            print(f"  ⚠️ {msg}")
        return {"status": "skipped", "reason": msg}

    # Construire le dataset
    if verbose:
        print(f"\n  📐 Construction du dataset de features...")
    dataset = build_dataset_from_trades(trades, str(KLINES_DIR))

    if len(dataset) < 50:
        msg = f"Dataset trop petit après feature engineering ({len(dataset)} < 50)"
        if verbose:
            print(f"  ⚠️ {msg}")
        return {"status": "skipped", "reason": msg}

    # Entraîner
    clf = SignalClassifier()
    metrics = clf.train(
        dataset,
        optimize_hyperparams=True,
        n_optuna_trials=n_optuna_trials,
        verbose=verbose,
    )

    # Sauvegarder le modèle avec timestamp
    MODELS_DIR.mkdir(parents=True, exist_ok=True)
    timestamp = now.strftime("%Y%m%d_%H%M%S")
    model_path = str(MODELS_DIR / f"signal_classifier_{timestamp}.pkl")
    clf.save(model_path)

    # Sauvegarder aussi comme "latest"
    latest_path = str(MODELS_DIR / "signal_classifier.pkl")
    clf.save(latest_path)

    # Nettoyer les anciens modèles (garder les 10 derniers)
    old_models = sorted(MODELS_DIR.glob("signal_classifier_*.pkl"))
    if len(old_models) > 10:
        for old in old_models[:-10]:
            old.unlink()
            if verbose:
                print(f"  🗑️ Ancien modèle supprimé: {old.name}")

    result = {
        "status": "success",
        "timestamp": now.isoformat(),
        "model_path": model_path,
        "trades_used": len(dataset),
        "window_days": window_days,
        "metrics": metrics,
        "val_win_rate": metrics.get("win_rate_filtered", 0),
    }

    # Mettre à jour le state
    state = load_retrainer_state()
    state["last_retrain"] = now.isoformat()
    state.setdefault("history", []).append({
        "timestamp": now.isoformat(),
        "trades_used": len(dataset),
        "val_win_rate": metrics.get("win_rate_filtered", 0),
        "pnl_improvement": metrics.get("pnl_improvement", 0),
        "threshold": metrics.get("optimal_threshold", clf.optimal_threshold),
    })
    # Garder les 30 derniers entraînements
    state["history"] = state["history"][-30:]
    save_retrainer_state(state)

    return result


def main():
    parser = argparse.ArgumentParser(description="SPY Optimizer — Daily Retrainer")
    parser.add_argument("--force", action="store_true", help="Forcer le retraining")
    parser.add_argument("--window-days", type=int, default=10, help="Jours d'historique (default: 10)")
    parser.add_argument("--optuna-trials", type=int, default=80, help="Essais Optuna (default: 80)")
    args = parser.parse_args()

    state = load_retrainer_state()
    needed, reason = should_retrain(state, force=args.force)

    if not needed:
        print(f"  ⏭️ Retraining non nécessaire: {reason}")
        return

    print(f"  ✅ Retraining déclenché: {reason}")
    result = retrain(
        window_days=args.window_days,
        n_optuna_trials=args.optuna_trials,
        verbose=True,
    )

    if result["status"] == "success":
        m = result["metrics"]
        print(f"\n  🎯 Retraining terminé avec succès!")
        print(f"     Win rate filtré: {m.get('win_rate_filtered', 0):.1f}%")
        print(f"     PnL improvement: {m.get('pnl_improvement', 0):+.2f}%")
    else:
        print(f"\n  ⚠️ Retraining skipped: {result.get('reason', 'unknown')}")


if __name__ == "__main__":
    main()
