#!/usr/bin/env python3
"""
SPY Optimizer — HTML Report Generator
======================================
Génère un rapport HTML complet avec :
  - Résumé des performances du modèle IA
  - Walk-forward backtest par jour
  - Feature importance
  - Analyse par surge_type
  - Évolution des PnL dans le temps
  - Comparaison avec/sans filtre IA

Usage:
    python generate_report.py                  # Rapport standard
    python generate_report.py --output report.html  # Chemin personnalisé
    python generate_report.py --open           # Ouvre dans le navigateur
"""
import argparse
import json
import sys
from datetime import datetime, timezone
from pathlib import Path

import numpy as np
import pandas as pd

PROJECT_DIR = Path(__file__).parent
DATA_DIR = PROJECT_DIR / "data"
MODELS_DIR = PROJECT_DIR / "models"
HISTORY_FILE = PROJECT_DIR.parent / "espion_history.json"
OPTIMIZER_RESULTS = DATA_DIR / "optimizer_results.json"
BACKTEST_RESULTS = DATA_DIR / "backtest_results.json"
GPU_RESULTS = DATA_DIR / "gpu_optimization_results.json"
DEFAULT_OUTPUT = PROJECT_DIR / "report.html"


def load_data():
    """Charge toutes les données disponibles."""
    data = {}

    # Trades
    if HISTORY_FILE.exists():
        with open(HISTORY_FILE) as f:
            all_trades = json.load(f)
        data["all_trades"] = all_trades
        START_DATE = "2026-04-01"
        data["trades"] = [t for t in all_trades if t.get("surge_type") and t.get("entry_time") and t["entry_time"][:10] >= START_DATE]
    else:
        data["all_trades"] = []
        data["trades"] = []

    # Résultats optimizer
    if OPTIMIZER_RESULTS.exists():
        data["optimizer"] = json.loads(OPTIMIZER_RESULTS.read_text())
    else:
        data["optimizer"] = None

    # Résultats backtest walk-forward
    if BACKTEST_RESULTS.exists():
        data["backtest"] = json.loads(BACKTEST_RESULTS.read_text())
    else:
        data["backtest"] = None

    # Résultats GPU (si disponibles)
    if GPU_RESULTS.exists():
        data["gpu"] = json.loads(GPU_RESULTS.read_text())
    else:
        data["gpu"] = None

    # Dataset features (si disponible)
    dataset_path = DATA_DIR / "training_dataset.parquet"
    if dataset_path.exists():
        data["dataset"] = pd.read_parquet(dataset_path)
    else:
        data["dataset"] = None

    return data


def compute_trade_stats(trades: list) -> dict:
    """Calcule les statistiques de trading."""
    if not trades:
        return {}

    pnls = [float(t.get("pnl_usdt", 0)) for t in trades]
    pnl_pcts = [float(t.get("pnl_pct", 0)) for t in trades]

    profitable = [p for p in pnls if p > 0]
    losing = [p for p in pnls if p < 0]

    # Par surge_type
    by_type = {}
    for t in trades:
        st = t.get("surge_type", "UNKNOWN")
        if st not in by_type:
            by_type[st] = {"trades": [], "pnl": 0, "wins": 0}
        by_type[st]["trades"].append(t)
        pnl = float(t.get("pnl_usdt", 0))
        by_type[st]["pnl"] += pnl
        if pnl > 0:
            by_type[st]["wins"] += 1

    # Par jour (pour courbe)
    by_date = {}
    for t in trades:
        date = t.get("entry_time", "")[:10]
        if date not in by_date:
            by_date[date] = {"pnl": 0, "count": 0, "wins": 0}
        pnl = float(t.get("pnl_usdt", 0))
        by_date[date]["pnl"] += pnl
        by_date[date]["count"] += 1
        if pnl > 0:
            by_date[date]["wins"] += 1

    # PnL cumulatif
    sorted_dates = sorted(by_date.keys())
    cumulative = 0
    cum_pnl = []
    for d in sorted_dates:
        cumulative += by_date[d]["pnl"]
        cum_pnl.append({"date": d, "daily": round(by_date[d]["pnl"], 2), "cumulative": round(cumulative, 2), "count": by_date[d]["count"], "wins": by_date[d]["wins"]})

    # Drawdown
    peak = 0
    max_dd = 0
    for p in cum_pnl:
        if p["cumulative"] > peak:
            peak = p["cumulative"]
        dd = peak - p["cumulative"]
        if dd > max_dd:
            max_dd = dd

    avg_win = sum(profitable) / len(profitable) if profitable else 0
    avg_loss = sum(losing) / len(losing) if losing else 0
    profit_factor = abs(sum(profitable) / sum(losing)) if losing and sum(losing) != 0 else float("inf")

    return {
        "total": len(trades),
        "wins": len(profitable),
        "losses": len(losing),
        "wr": len(profitable) / len(trades) * 100,
        "total_pnl": sum(pnls),
        "avg_pnl": sum(pnls) / len(pnls),
        "avg_win": avg_win,
        "avg_loss": avg_loss,
        "profit_factor": profit_factor,
        "best_trade": max(pnls),
        "worst_trade": min(pnls),
        "max_drawdown": max_dd,
        "by_type": by_type,
        "cum_pnl": cum_pnl,
        "period_start": min(t.get("entry_time", "")[:10] for t in trades),
        "period_end": max(t.get("entry_time", "")[:10] for t in trades),
    }


def generate_html(data: dict, output: Path):
    """Génère le rapport HTML complet."""
    now = datetime.now(timezone.utc).strftime("%d/%m/%Y %H:%M UTC")
    trades = data["trades"]
    stats = compute_trade_stats(trades)
    opt = data.get("optimizer", {}) or {}
    bt = data.get("backtest", {}) or {}
    gpu = data.get("gpu", {}) or {}
    dataset = data.get("dataset")

    metrics = opt.get("metrics", {})
    bt_results = bt.get("results", {})
    bt_windows = bt_results.get("window_details", [])

    # Feature importance depuis dataset
    feature_importance = {}
    if dataset is not None and "label" in dataset.columns:
        # Corrélation simple avec le label
        for col in dataset.columns:
            if col not in ("label", "entry_time", "symbol", "surge_type"):
                try:
                    feature_importance[col] = abs(dataset[col].corr(dataset["label"]))
                except Exception:
                    pass

    top_features = sorted(feature_importance.items(), key=lambda x: -x[1])[:15]

    # Stats par surge_type
    surge_rows = ""
    for st, s in sorted(stats.get("by_type", {}).items(), key=lambda x: -x[1]["pnl"]):
        n = len(s["trades"])
        wr = s["wins"] / n * 100
        color = "#27ae60" if s["pnl"] > 0 else "#e74c3c"
        surge_rows += f"""
        <tr>
            <td><span class="badge badge-{'green' if wr > 55 else 'red' if wr < 45 else 'yellow'}">{st}</span></td>
            <td>{n}</td>
            <td>{wr:.1f}%</td>
            <td style="color:{color};font-weight:600">${s['pnl']:+.2f}</td>
            <td>${s['pnl']/n:.2f}</td>
        </tr>"""

    # Walk-forward rows
    wf_rows = ""
    for w in bt_windows:
        delta = w.get("pnl_with_filter", 0) - w.get("pnl_without_filter", 0)
        ok = "✅" if delta >= 0 else "❌"
        dcolor = "#27ae60" if delta >= 0 else "#e74c3c"
        wf_rows += f"""
        <tr>
            <td>{w.get('val_date', '')}</td>
            <td>{w.get('n_trades', 0)} → {w.get('n_passed', 0)}</td>
            <td>{w.get('win_rate_filtered', 0):.1f}%</td>
            <td>{w.get('pnl_without_filter', 0):+.1f}%</td>
            <td>{w.get('pnl_with_filter', 0):+.1f}%</td>
            <td style="color:{dcolor};font-weight:600">{delta:+.1f}% {ok}</td>
            <td>{w.get('threshold', 0):.2f}</td>
        </tr>"""

    # Données pour graphiques Chart.js
    cum_labels = [d["date"] for d in stats.get("cum_pnl", [])]
    cum_values = [d["cumulative"] for d in stats.get("cum_pnl", [])]
    daily_values = [d["daily"] for d in stats.get("cum_pnl", [])]
    daily_counts = [d["count"] for d in stats.get("cum_pnl", [])]
    daily_wr = [round(d["wins"]/d["count"]*100, 1) for d in stats.get("cum_pnl", []) if d["count"] > 0]

    # Données backtest pour graphique
    bt_labels = [w.get("val_date", "") for w in bt_windows]
    bt_with = [w.get("pnl_with_filter", 0) for w in bt_windows]
    bt_without = [w.get("pnl_without_filter", 0) for w in bt_windows]

    # Feature importance pour graphique
    fi_labels = [f[0] for f in top_features]
    fi_values = [round(f[1], 4) for f in top_features]

    # GPU info
    gpu_section = ""
    if gpu:
        gpu_metrics = gpu.get("best_params", gpu.get("metrics", {}))
        gpu_device = gpu.get("device", "N/A")
        gpu_trials = gpu.get("n_trials", "N/A")
        gpu_auc = gpu.get("best_auc", gpu.get("auc_roc", "N/A"))
        gpu_section = f"""
        <div class="card gpu-card">
            <div class="card-header">
                <span class="card-icon">🎮</span>
                <h3>Optimisation GPU</h3>
                <span class="badge badge-green">PC Training</span>
            </div>
            <div class="metrics-grid">
                <div class="metric">
                    <span class="metric-label">Device</span>
                    <span class="metric-value">{gpu_device}</span>
                </div>
                <div class="metric">
                    <span class="metric-label">Trials Optuna</span>
                    <span class="metric-value">{gpu_trials}</span>
                </div>
                <div class="metric">
                    <span class="metric-label">Best AUC</span>
                    <span class="metric-value">{gpu_auc if isinstance(gpu_auc, str) else f'{gpu_auc:.4f}'}</span>
                </div>
            </div>
        </div>"""

    wr = stats.get("wr", 0)
    total_pnl = stats.get("total_pnl", 0)
    model_auc = metrics.get("auc_roc", 0)
    model_acc = metrics.get("accuracy", 0) * 100
    trade_rate = metrics.get("trade_rate", 1) * 100
    pnl_improvement = metrics.get("pnl_improvement", bt_results.get("pnl_improvement", 0))
    bt_positive_windows = bt_results.get("positive_windows_pct", 0)

    html = f"""<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Trading Apple IA — Rapport SPY Optimizer</title>
<link rel="icon" type="image/png" sizes="192x192" href="/crypto_trading_ia_icon.png">
<link rel="apple-touch-icon" href="/crypto_trading_ia_icon.png">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
  * {{ box-sizing: border-box; margin: 0; padding: 0; }}
  :root {{
    --bg: #0a0e1a;
    --card: #111827;
    --card2: #1a2235;
    --border: #1e2d47;
    --text: #e2e8f0;
    --text2: #94a3b8;
    --green: #10b981;
    --red: #ef4444;
    --yellow: #f59e0b;
    --blue: #3b82f6;
    --purple: #8b5cf6;
    --accent: #06b6d4;
  }}
  body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; }}
  
  /* HEADER */
  .header {{ background: linear-gradient(135deg, #0f172a 0%, #1e3a5f 100%); padding: 2rem; border-bottom: 1px solid var(--border); }}
  .header-content {{ max-width: 1400px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem; }}
  .header-left h1 {{ font-size: 1.8rem; font-weight: 700; display: flex; align-items: center; gap: 0.75rem; }}
  .header-left h1 .h1-text {{ background: linear-gradient(90deg, #06b6d4, #3b82f6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }}
  .header-left p {{ color: var(--text2); margin-top: 0.25rem; font-size: 0.9rem; }}
  .header-right {{ text-align: right; }}
  .header-right .ts {{ color: var(--accent); font-size: 0.85rem; }}
  .header-right .period {{ color: var(--text2); font-size: 0.8rem; margin-top: 0.25rem; }}
  
  /* LAYOUT */
  .container {{ max-width: 1400px; margin: 0 auto; padding: 1.5rem; }}
  
  /* KPI BAR */
  .kpi-bar {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }}
  .kpi {{ background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 1.25rem; text-align: center; position: relative; overflow: hidden; }}
  .kpi::before {{ content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; }}
  .kpi.green::before {{ background: var(--green); }}
  .kpi.red::before {{ background: var(--red); }}
  .kpi.blue::before {{ background: var(--blue); }}
  .kpi.yellow::before {{ background: var(--yellow); }}
  .kpi.purple::before {{ background: var(--purple); }}
  .kpi-label {{ font-size: 0.75rem; color: var(--text2); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; }}
  .kpi-value {{ font-size: 2rem; font-weight: 700; line-height: 1; }}
  .kpi-value.green {{ color: var(--green); }}
  .kpi-value.red {{ color: var(--red); }}
  .kpi-value.blue {{ color: var(--blue); }}
  .kpi-value.yellow {{ color: var(--yellow); }}
  .kpi-sub {{ font-size: 0.75rem; color: var(--text2); margin-top: 0.3rem; }}
  
  /* CARDS */
  .grid-2 {{ display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem; }}
  .grid-3 {{ display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem; }}
  .card {{ background: var(--card); border: 1px solid var(--border); border-radius: 16px; padding: 1.5rem; margin-bottom: 1.5rem; }}
  .card.full {{ margin-bottom: 0; }}
  .card-header {{ display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1.25rem; border-bottom: 1px solid var(--border); padding-bottom: 0.75rem; }}
  .card-icon {{ font-size: 1.25rem; }}
  .card-header h3 {{ font-size: 1rem; font-weight: 600; color: var(--text); }}
  .card-header .badge {{ margin-left: auto; }}
  
  /* BADGES */
  .badge {{ display: inline-block; padding: 0.2rem 0.6rem; border-radius: 999px; font-size: 0.7rem; font-weight: 600; text-transform: uppercase; }}
  .badge-green {{ background: rgba(16,185,129,0.15); color: var(--green); border: 1px solid rgba(16,185,129,0.3); }}
  .badge-red {{ background: rgba(239,68,68,0.15); color: var(--red); border: 1px solid rgba(239,68,68,0.3); }}
  .badge-yellow {{ background: rgba(245,158,11,0.15); color: var(--yellow); border: 1px solid rgba(245,158,11,0.3); }}
  .badge-blue {{ background: rgba(59,130,246,0.15); color: var(--blue); border: 1px solid rgba(59,130,246,0.3); }}
  
  /* METRICS GRID */
  .metrics-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 1rem; }}
  .metric {{ background: var(--card2); border-radius: 10px; padding: 1rem; text-align: center; }}
  .metric-label {{ font-size: 0.7rem; color: var(--text2); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 0.4rem; }}
  .metric-value {{ font-size: 1.4rem; font-weight: 700; color: var(--accent); }}
  .metric-value.ok {{ color: var(--green); }}
  .metric-value.ko {{ color: var(--red); }}
  
  /* TABLES */
  .table-wrap {{ overflow-x: auto; }}
  table {{ width: 100%; border-collapse: collapse; font-size: 0.85rem; }}
  th {{ padding: 0.75rem 1rem; text-align: left; color: var(--text2); font-weight: 500; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em; border-bottom: 1px solid var(--border); }}
  td {{ padding: 0.75rem 1rem; border-bottom: 1px solid rgba(30,45,71,0.5); }}
  tr:last-child td {{ border-bottom: none; }}
  tr:hover td {{ background: rgba(59,130,246,0.05); }}
  
  /* CHARTS */
  .chart-container {{ position: relative; height: 280px; }}
  .chart-container.tall {{ height: 350px; }}
  
  /* PROGRESS BAR */
  .progress-bar {{ background: var(--card2); border-radius: 999px; height: 8px; margin-top: 0.4rem; overflow: hidden; }}
  .progress-fill {{ height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--blue), var(--accent)); transition: width 0.5s; }}
  .progress-fill.green {{ background: linear-gradient(90deg, var(--green), #34d399); }}
  .progress-fill.red {{ background: linear-gradient(90deg, var(--red), #f87171); }}
  
  /* STATUS */
  .status-ok {{ color: var(--green); }}
  .status-ko {{ color: var(--red); }}
  
  /* ALERT BOX */
  .alert {{ border-radius: 10px; padding: 1rem 1.25rem; margin-bottom: 1.5rem; display: flex; gap: 0.75rem; align-items: flex-start; font-size: 0.875rem; }}
  .alert-info {{ background: rgba(59,130,246,0.1); border: 1px solid rgba(59,130,246,0.3); color: #93c5fd; }}
  .alert-warning {{ background: rgba(245,158,11,0.1); border: 1px solid rgba(245,158,11,0.3); color: #fcd34d; }}
  .alert-success {{ background: rgba(16,185,129,0.1); border: 1px solid rgba(16,185,129,0.3); color: #6ee7b7; }}
  
  /* FOOTER */
  .footer {{ text-align: center; padding: 2rem; color: var(--text2); font-size: 0.8rem; border-top: 1px solid var(--border); margin-top: 2rem; }}

  /* REPORT TABS */
  .report-tabs-nav {{ display:flex; gap:0; border-bottom:2px solid var(--border); margin-bottom:1.5rem; max-width:1400px; margin-left:auto; margin-right:auto; padding:0 1.5rem; }}
  .report-tab-btn {{ padding:0.8rem 1.5rem; font-size:0.92rem; font-weight:600; color:var(--text2); background:none; border:none; cursor:pointer; border-bottom:3px solid transparent; margin-bottom:-2px; transition:all .2s; display:flex; align-items:center; gap:0.5rem; }}
  .report-tab-btn:hover {{ color:var(--text); }}
  .report-tab-btn.active {{ color:var(--accent); border-bottom-color:var(--accent); }}
  .report-tab-pane {{ display:none; }}
  .report-tab-pane.active {{ display:block; }}

  /* ACTION BAR */
  .action-bar {{ background: linear-gradient(135deg,#0f2027,#1a3a5f); border: 1px solid var(--border); border-radius: 16px; padding: 1.25rem 1.5rem; margin-bottom: 1.5rem; display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; }}
  .action-bar .bar-title {{ font-weight: 600; color: var(--accent); font-size: 0.9rem; white-space: nowrap; }}
  .btn {{ display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.65rem 1.25rem; border-radius: 10px; font-size: 0.875rem; font-weight: 600; cursor: pointer; border: none; transition: all .2s; white-space: nowrap; }}
  .btn-primary {{ background: linear-gradient(135deg,#2563eb,#06b6d4); color: #fff; }}
  .btn-primary:hover {{ opacity: .88; transform: translateY(-1px); }}
  .btn-success {{ background: linear-gradient(135deg,#059669,#10b981); color: #fff; }}
  .btn-success:hover {{ opacity: .88; transform: translateY(-1px); }}
  .btn-danger  {{ background: linear-gradient(135deg,#991b1b,#ef4444); color: #fff; }}
  .btn:disabled {{ opacity:.45; cursor:not-allowed; transform:none; }}
  .pipeline-status {{ flex: 1; min-width: 200px; font-size: 0.82rem; }}
  .pipeline-status .ps-label {{ color: var(--text2); font-size: 0.75rem; margin-bottom: 0.25rem; }}
  .pipeline-status .ps-value {{ font-weight: 600; }}
  .ps-idle    {{ color: var(--text2); }}
  .ps-running {{ color: var(--yellow); }}
  .ps-success {{ color: var(--green); }}
  .ps-error   {{ color: var(--red); }}
  .pipeline-log {{ background: #060a14; border: 1px solid var(--border); border-radius: 10px; padding: 1rem; font-family: 'Courier New',monospace; font-size: 0.75rem; color: #94a3b8; max-height: 220px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; margin-top: 0.75rem; display: none; }}
  .pipeline-log.visible {{ display: block; }}
  .apply-result {{ background: rgba(16,185,129,.08); border: 1px solid rgba(16,185,129,.3); border-radius: 10px; padding: 0.85rem 1.1rem; font-size: 0.82rem; color: #6ee7b7; margin-top: 0.75rem; display: none; }}
  .apply-result.visible {{ display: block; }}
  .spinner {{ display: inline-block; width: 14px; height: 14px; border: 2px solid rgba(255,255,255,.3); border-top-color: #fff; border-radius: 50%; animation: spin .7s linear infinite; }}
  @keyframes spin {{ to {{ transform: rotate(360deg); }} }}

  /* PIPELINE STEPPER */
  .pipeline-steps {{ display:none; background:#0a1628; border:1px solid var(--border); border-radius:14px; padding:1.25rem 1.5rem; margin-top:0.75rem; }}
  .pipeline-steps.visible {{ display:block; }}
  .steps-header {{ font-size:0.78rem; color:var(--text2); margin-bottom:1.1rem; display:flex; justify-content:space-between; align-items:center; }}
  .steps-list {{ display:flex; gap:0; align-items:flex-start; }}
  .step-item {{ flex:1; position:relative; text-align:center; }}
  .step-item:not(:last-child)::after {{ content:''; position:absolute; top:13px; left:calc(50% + 14px); right:calc(-50% + 14px); height:2px; background:var(--border); z-index:0; transition:background .4s; }}
  .step-item.step-done:not(:last-child)::after {{ background:var(--green); }}
  .step-item.step-running:not(:last-child)::after {{ background:linear-gradient(90deg,var(--green) 30%,var(--border) 100%); }}
  .step-icon {{ width:26px; height:26px; border-radius:50%; display:inline-flex; align-items:center; justify-content:center; font-size:0.72rem; font-weight:700; border:2px solid var(--border); background:#0a1628; position:relative; z-index:1; color:var(--text2); transition:all .3s; }}
  .step-item.step-done .step-icon  {{ background:var(--green); border-color:var(--green); color:#fff; }}
  .step-item.step-running .step-icon {{ background:var(--yellow); border-color:var(--yellow); color:#000; animation:pulse-step 1.1s ease-in-out infinite; }}
  .step-item.step-error .step-icon  {{ background:var(--red); border-color:var(--red); color:#fff; }}
  .step-label {{ font-size:0.6rem; color:var(--text2); line-height:1.3; padding:0.35rem 0.1rem 0; }}
  .step-item.step-done .step-label  {{ color:var(--green); }}
  .step-item.step-running .step-label {{ color:var(--yellow); font-weight:700; }}
  .step-item.step-error .step-label  {{ color:var(--red); }}
  .steps-timer {{ font-family:monospace; color:var(--accent); font-size:0.78rem; }}
  @keyframes pulse-step {{ 0%,100%{{box-shadow:0 0 0 0 rgba(245,158,11,.5)}} 50%{{box-shadow:0 0 0 5px rgba(245,158,11,0)}} }}

  /* LOGIN MODAL */
  .login-overlay {{ display:none; position:fixed; inset:0; background:rgba(0,0,0,.8); z-index:9999; align-items:center; justify-content:center; }}
  .login-overlay.visible {{ display:flex; }}
  .login-box {{ background:#0f172a; border:1px solid var(--border); border-radius:18px; padding:2rem; width:320px; max-width:90vw; }}
  .login-box h3 {{ margin:0 0 0.4rem; font-size:1.05rem; color:#f1f5f9; }}
  .login-box p {{ color:var(--text2); font-size:0.8rem; margin-bottom:1.25rem; }}
  .login-input {{ width:100%; background:#1e293b; border:1px solid var(--border); border-radius:8px; padding:0.6rem 0.85rem; color:#f1f5f9; font-size:0.875rem; margin-bottom:0.65rem; box-sizing:border-box; }}
  .login-input:focus {{ outline:none; border-color:var(--accent); }}
  .login-err {{ color:#ef4444; font-size:0.78rem; margin-top:0.6rem; min-height:1.2em; }}

  @media (max-width: 768px) {{
    .grid-2, .grid-3 {{ grid-template-columns: 1fr; }}
    .kpi-bar {{ grid-template-columns: repeat(2, 1fr); }}
  }}
</style>
</head>
<body>

<div class="header">
  <div class="header-content">
    <div class="header-left">
      <h1><img src="/crypto_trading_ia_logo.png" alt="Trading Apple IA" style="height:42px;width:auto;border-radius:6px;flex-shrink:0;"><span class="h1-text">Trading Apple IA — SPY Optimizer Report</span></h1>
      <p>Analyse complète des performances du Signal Classifier IA</p>
    </div>
    <div class="header-right">
      <div class="ts">Généré le {now}</div>
      <div class="period">Période: {stats.get('period_start','N/A')} → {stats.get('period_end','N/A')}</div>
    </div>
  </div>
</div>

<!-- TABS NAV -->
<div class="report-tabs-nav">
  <button class="report-tab-btn active" id="tab-btn-results" onclick="switchReportTab('results')">📊 Résultats</button>
  <button class="report-tab-btn" id="tab-btn-pipeline" onclick="switchReportTab('pipeline')">🚀 Pipeline GPU</button>
</div>

<!-- LOGIN MODAL (global, hors onglets) -->
<div class="login-overlay" id="loginOverlay">
  <div class="login-box">
    <h3>🔐 Connexion requise</h3>
    <p>Entrez vos identifiants du dashboard pour contrôler le pipeline GPU</p>
    <input class="login-input" type="text" id="loginUser" placeholder="Utilisateur" autocomplete="username">
    <input class="login-input" type="password" id="loginPass" placeholder="Mot de passe" autocomplete="current-password" onkeydown="if(event.key==='Enter') submitLogin()">
    <button class="btn btn-primary" style="width:100%;justify-content:center;margin-top:0.25rem" onclick="submitLogin()">Se connecter</button>
    <div class="login-err" id="loginErr"></div>
  </div>
</div>

<!-- ═══ ONGLET 1 : RÉSULTATS ═══ -->
<div id="tab-pane-results" class="report-tab-pane active">
<div class="container">

  <!-- KPI BAR -->
  <div class="kpi-bar">
    <div class="kpi {'green' if total_pnl > 0 else 'red'}">
      <div class="kpi-label">PnL Total</div>
      <div class="kpi-value {'green' if total_pnl > 0 else 'red'}">${total_pnl:+.0f}</div>
      <div class="kpi-sub">USDT net</div>
    </div>
    <div class="kpi {'green' if wr > 55 else 'yellow' if wr > 50 else 'red'}">
      <div class="kpi-label">Win Rate</div>
      <div class="kpi-value {'green' if wr > 55 else 'yellow' if wr > 50 else 'red'}">{wr:.1f}%</div>
      <div class="kpi-sub">{stats.get('wins', 0)}/{stats.get('total', 0)} trades</div>
    </div>
    <div class="kpi blue">
      <div class="kpi-label">AUC-ROC (IA)</div>
      <div class="kpi-value blue">{model_auc:.3f}</div>
      <div class="kpi-sub">Discriminant du modèle</div>
    </div>
    <div class="kpi {'green' if model_acc > 60 else 'yellow'}">
      <div class="kpi-label">Accuracy IA</div>
      <div class="kpi-value {'green' if model_acc > 60 else 'yellow'}">{model_acc:.1f}%</div>
      <div class="kpi-sub">Validation temporelle</div>
    </div>
    <div class="kpi {'green' if pnl_improvement > 0 else 'red' if pnl_improvement < -5 else 'yellow'}">
      <div class="kpi-label">Impact Filtre IA</div>
      <div class="kpi-value {'green' if pnl_improvement > 0 else 'red' if pnl_improvement < -5 else 'yellow'}">{pnl_improvement:+.1f}%</div>
      <div class="kpi-sub">vs sans filtre</div>
    </div>
    <div class="kpi purple">
      <div class="kpi-label">Profit Factor</div>
      <div class="kpi-value purple">{min(stats.get('profit_factor', 0), 9.99):.2f}</div>
      <div class="kpi-sub">Gains / Pertes abs.</div>
    </div>
  </div>

  {'<div class="alert alert-info">ℹ️ <strong>Modèle entraîné localement (CPU)</strong> — Pour activer l\'optimisation GPU, lancez <code>connect_tunnel.ps1</code> sur votre PC puis <code>python remote_gpu_train.py</code></div>' if not gpu else '<div class="alert alert-success">✅ <strong>Modèle optimisé GPU</strong> — Le PC Windows a participé à l\'entraînement</div>'}

  <!-- ROW 1: PnL cumulatif + Backtest -->
  <div class="grid-2">
    <div class="card full">
      <div class="card-header">
        <span class="card-icon">📈</span>
        <h3>PnL Cumulatif</h3>
        <span class="badge badge-{'green' if total_pnl > 0 else 'red'}">${total_pnl:+.2f} USDT</span>
      </div>
      <div class="chart-container">
        <canvas id="chartPnl"></canvas>
      </div>
    </div>
    <div class="card full">
      <div class="card-header">
        <span class="card-icon">📊</span>
        <h3>PnL Journalier</h3>
        <span class="badge badge-blue">{len(stats.get('cum_pnl', []))} jours</span>
      </div>
      <div class="chart-container">
        <canvas id="chartDaily"></canvas>
      </div>
    </div>
  </div>

  <!-- ROW 2: Modèle IA + Walk-forward -->
  <div class="grid-2">
    <div class="card full">
      <div class="card-header">
        <span class="card-icon">🤖</span>
        <h3>Modèle IA — Signal Classifier</h3>
        <span class="badge badge-blue">LightGBM + XGBoost</span>
      </div>
      <div class="metrics-grid">
        <div class="metric">
          <div class="metric-label">AUC-ROC</div>
          <div class="metric-value">{model_auc:.4f}</div>
        </div>
        <div class="metric">
          <div class="metric-label">Accuracy</div>
          <div class="metric-value {'ok' if model_acc > 60 else 'ko'}">{model_acc:.1f}%</div>
        </div>
        <div class="metric">
          <div class="metric-label">Precision</div>
          <div class="metric-value">{metrics.get('precision', 0)*100:.1f}%</div>
        </div>
        <div class="metric">
          <div class="metric-label">Recall</div>
          <div class="metric-value">{metrics.get('recall', 0)*100:.1f}%</div>
        </div>
        <div class="metric">
          <div class="metric-label">F1-Score</div>
          <div class="metric-value">{metrics.get('f1', 0):.3f}</div>
        </div>
        <div class="metric">
          <div class="metric-label">Trade Rate</div>
          <div class="metric-value">{trade_rate:.0f}%</div>
        </div>
        <div class="metric">
          <div class="metric-label">Dataset</div>
          <div class="metric-value">{opt.get('dataset_size', 0)}</div>
        </div>
        <div class="metric">
          <div class="metric-label">WR filtré</div>
          <div class="metric-value {'ok' if metrics.get('win_rate_filtered', 0) > 55 else 'ko'}">{metrics.get('win_rate_filtered', 0):.1f}%</div>
        </div>
      </div>
      
      <div style="margin-top:1.25rem;border-top:1px solid var(--border);padding-top:1rem;">
        <div style="font-size:0.75rem;color:var(--text2);margin-bottom:0.5rem;">PnL Sans filtre vs Avec filtre IA</div>
        <div style="display:flex;gap:1rem;align-items:center;">
          <span style="color:var(--text2);font-size:0.875rem">Sans filtre:</span>
          <strong style="color:var(--text)">{metrics.get('pnl_without_filter', 0):+.1f}%</strong>
          <span style="color:var(--text2);font-size:0.875rem">→ Avec filtre:</span>
          <strong style="color:{'var(--green)' if (metrics.get('pnl_with_filter', 0) or 0) >= (metrics.get('pnl_without_filter', 0) or 0) else 'var(--red)'}">{metrics.get('pnl_with_filter', 0):+.1f}%</strong>
          <span style="font-size:0.875rem;color:{'var(--green)' if pnl_improvement >= 0 else 'var(--red)'};font-weight:600">({pnl_improvement:+.1f}%)</span>
        </div>
      </div>
    </div>
    
    <div class="card full">
      <div class="card-header">
        <span class="card-icon">⏱️</span>
        <h3>Walk-Forward Backtest</h3>
        <span class="badge badge-{'green' if bt_positive_windows > 60 else 'yellow' if bt_positive_windows > 50 else 'red'}">{bt_positive_windows:.0f}% positifs</span>
      </div>
      <div class="chart-container">
        <canvas id="chartWalkforward"></canvas>
      </div>
    </div>
  </div>

  <!-- Feature Importance + Walk-forward Table -->
  <div class="grid-2">
    <div class="card full">
      <div class="card-header">
        <span class="card-icon">🔑</span>
        <h3>Feature Importance (Top 15)</h3>
      </div>
      <div class="chart-container tall">
        <canvas id="chartFeatures"></canvas>
      </div>
    </div>
    
    <div class="card full">
      <div class="card-header">
        <span class="card-icon">📋</span>
        <h3>Détail Walk-Forward</h3>
        <span class="badge badge-blue">{len(bt_windows)} fenêtres</span>
      </div>
      <div class="table-wrap">
        <table>
          <thead>
            <tr>
              <th>Date</th>
              <th>Trades</th>
              <th>WR</th>
              <th>Sans IA</th>
              <th>Avec IA</th>
              <th>Δ</th>
              <th>Seuil</th>
            </tr>
          </thead>
          <tbody>
            {wf_rows}
          </tbody>
        </table>
      </div>
      <div style="margin-top:1rem;padding-top:0.75rem;border-top:1px solid var(--border);display:flex;justify-content:space-between;font-size:0.8rem;color:var(--text2)">
        <span>Total: {bt_results.get('total_trades', 0)} trades → {bt_results.get('total_passed', 0)} passés</span>
        <span>PnL global: <strong style="color:{'var(--green)' if bt_results.get('pnl_improvement', 0) >= 0 else 'var(--red)'}">{bt_results.get('pnl_improvement', 0):+.2f}%</strong></span>
      </div>
    </div>
  </div>

  <!-- Par Surge Type -->
  <div class="card">
    <div class="card-header">
      <span class="card-icon">⚡</span>
      <h3>Performance par Surge Type</h3>
      <span class="badge badge-blue">{len(stats.get('by_type', {}))} types</span>
    </div>
    <div class="table-wrap">
      <table>
        <thead>
          <tr>
            <th>Surge Type</th>
            <th>Trades</th>
            <th>Win Rate</th>
            <th>PnL Total</th>
            <th>PnL Moyen</th>
          </tr>
        </thead>
        <tbody>
          {surge_rows}
        </tbody>
      </table>
    </div>
  </div>

  <!-- Trading Stats -->
  <div class="card">
    <div class="card-header">
      <span class="card-icon">💰</span>
      <h3>Statistiques de Trading</h3>
      <span class="badge badge-{'green' if total_pnl > 0 else 'red'}">{stats.get('period_start','?')} → {stats.get('period_end','?')}</span>
    </div>
    <div class="metrics-grid">
      <div class="metric">
        <div class="metric-label">Total Trades</div>
        <div class="metric-value">{stats.get('total', 0)}</div>
      </div>
      <div class="metric">
        <div class="metric-label">Win Rate</div>
        <div class="metric-value {'ok' if wr > 55 else 'ko'}">{wr:.1f}%</div>
      </div>
      <div class="metric">
        <div class="metric-label">PnL Total</div>
        <div class="metric-value {'ok' if total_pnl > 0 else 'ko'}">${total_pnl:+.2f}</div>
      </div>
      <div class="metric">
        <div class="metric-label">PnL Moyen</div>
        <div class="metric-value {'ok' if stats.get('avg_pnl', 0) > 0 else 'ko'}">${stats.get('avg_pnl', 0):+.2f}</div>
      </div>
      <div class="metric">
        <div class="metric-label">Gain Moyen</div>
        <div class="metric-value ok">${stats.get('avg_win', 0):+.2f}</div>
      </div>
      <div class="metric">
        <div class="metric-label">Perte Moyenne</div>
        <div class="metric-value ko">${stats.get('avg_loss', 0):+.2f}</div>
      </div>
      <div class="metric">
        <div class="metric-label">Meilleur Trade</div>
        <div class="metric-value ok">${stats.get('best_trade', 0):+.2f}</div>
      </div>
      <div class="metric">
        <div class="metric-label">Pire Trade</div>
        <div class="metric-value ko">${stats.get('worst_trade', 0):+.2f}</div>
      </div>
      <div class="metric">
        <div class="metric-label">Profit Factor</div>
        <div class="metric-value {'ok' if stats.get('profit_factor', 0) > 1.5 else 'ko'}">{min(stats.get('profit_factor', 0), 9.99):.2f}</div>
      </div>
      <div class="metric">
        <div class="metric-label">Max Drawdown</div>
        <div class="metric-value ko">-${stats.get('max_drawdown', 0):.2f}</div>
      </div>
    </div>
  </div>

  {gpu_section}

</div><!-- /container résultats -->
</div><!-- /tab-pane-results -->

<!-- ═══ ONGLET 2 : PIPELINE GPU ═══ -->
<div id="tab-pane-pipeline" class="report-tab-pane">
<div class="container">

  <!-- ACTION BAR -->
  <div class="action-bar" id="actionBar">
    <span class="bar-title">🤖 SPY Optimizer</span>

    <button class="btn btn-primary" id="btnRunPipeline" onclick="runPipeline()">
      <span id="btnRunIcon">🚀</span>
      <span id="btnRunLabel">Lancer l'analyse GPU</span>
    </button>

    <button class="btn btn-success" id="btnApply" onclick="applyOptimizer()">
      <span id="btnApplyIcon">✅</span>
      <span id="btnApplyLabel">Appliquer les optimisations</span>
    </button>

    <div class="pipeline-status" id="pipelineStatus">
      <div class="ps-label">Statut pipeline</div>
      <div class="ps-value ps-idle" id="psValue">En attente</div>
    </div>

    <button class="btn" style="background:#1e2d47;color:#94a3b8;font-size:0.8rem;padding:0.5rem 0.85rem;" onclick="toggleLog()">📋 Log</button>
  </div>

  <!-- PIPELINE STEPS -->
  <div class="pipeline-steps" id="pipelineSteps" style="display:block;">
    <div class="steps-header">
      <span>📊 Progression du pipeline</span>
      <span class="steps-timer" id="stepsTimer"></span>
    </div>
    <div class="steps-list">
      <div class="step-item" id="pstep1">
        <div class="step-icon">1</div>
        <div class="step-label">Téléchgt<br>klines 1m</div>
      </div>
      <div class="step-item" id="pstep2">
        <div class="step-icon">2</div>
        <div class="step-label">Dataset<br>Features</div>
      </div>
      <div class="step-item" id="pstep3">
        <div class="step-icon">3</div>
        <div class="step-label">GPU<br>Optuna</div>
      </div>
      <div class="step-item" id="pstep4">
        <div class="step-icon">4</div>
        <div class="step-label">Walk-Fwd<br>Backtest</div>
      </div>
      <div class="step-item" id="pstep5">
        <div class="step-icon">5</div>
        <div class="step-label">Rapport<br>HTML</div>
      </div>
      <div class="step-item" id="pstep6">
        <div class="step-icon">✓</div>
        <div class="step-label">Déploie-<br>ment</div>
      </div>
    </div>
  </div>

  <div class="pipeline-log" id="pipelineLog"></div>
  <div class="apply-result" id="applyResult"></div>

  <!-- Dernière analyse -->
  <div style="margin-top:2rem; padding:1.5rem; background:var(--card); border:1px solid var(--border); border-radius:16px;">
    <h3 style="font-size:1rem; font-weight:600; margin-bottom:1rem; color:var(--accent);">📈 Dernière analyse GPU</h3>
    <div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(180px,1fr)); gap:1rem;">
      <div><div style="color:var(--text2);font-size:0.78rem;">AUC-ROC</div><div style="font-size:1.4rem;font-weight:700;color:var(--blue);">{model_auc:.4f}</div></div>
      <div><div style="color:var(--text2);font-size:0.78rem;">Accuracy</div><div style="font-size:1.4rem;font-weight:700;color:{'var(--green)' if model_acc > 60 else 'var(--yellow)'};">{model_acc:.1f}%</div></div>
      <div><div style="color:var(--text2);font-size:0.78rem;">Win Rate filtré</div><div style="font-size:1.4rem;font-weight:700;color:{'var(--green)' if metrics.get('win_rate_filtered',0) > 55 else 'var(--yellow)'};">{metrics.get('win_rate_filtered',0):.1f}%</div></div>
      <div><div style="color:var(--text2);font-size:0.78rem;">Impact IA</div><div style="font-size:1.4rem;font-weight:700;color:{'var(--green)' if pnl_improvement >= 0 else 'var(--red)'};">{pnl_improvement:+.2f}%</div></div>
      <div><div style="color:var(--text2);font-size:0.78rem;">Trades analysés</div><div style="font-size:1.4rem;font-weight:700;color:var(--text);">{stats.get('total',0)}</div></div>
      <div><div style="color:var(--text2);font-size:0.78rem;">Période</div><div style="font-size:0.95rem;font-weight:600;color:var(--text);">{stats.get('period_start','?')} → {stats.get('period_end','?')}</div></div>
    </div>
  </div>

</div><!-- /container pipeline -->
</div><!-- /tab-pane-pipeline -->

<div class="footer">
  Trading Apple IA — SPY Optimizer Report • Généré le {now} • LightGBM + XGBoost + Optuna
</div>

<script>
// ─── Tabs ───
function switchReportTab(name) {{
  document.querySelectorAll('.report-tab-pane').forEach(el => el.classList.remove('active'));
  document.querySelectorAll('.report-tab-btn').forEach(el => el.classList.remove('active'));
  document.getElementById('tab-pane-' + name).classList.add('active');
  document.getElementById('tab-btn-' + name).classList.add('active');
  if (name === 'pipeline') {{
    // Rafraîchir seulement si un poll est actif (pipeline lancé cette session)
    if (_pollInterval) {{
      fetchStatus();
    }} else {{
      updateStepper(0, 'idle');
    }}
  }}
}}

// ─── Chart.js defaults ───
Chart.defaults.color = '#94a3b8';
Chart.defaults.borderColor = '#1e2d47';
Chart.defaults.font.family = "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";

const GREEN = '#10b981', RED = '#ef4444', BLUE = '#3b82f6', YELLOW = '#f59e0b', PURPLE = '#8b5cf6', CYAN = '#06b6d4';

// ─── PnL Cumulatif ───
new Chart(document.getElementById('chartPnl'), {{
  type: 'line',
  data: {{
    labels: {json.dumps(cum_labels)},
    datasets: [{{
      label: 'PnL Cumulatif (USDT)',
      data: {json.dumps(cum_values)},
      borderColor: CYAN,
      backgroundColor: 'rgba(6,182,212,0.1)',
      borderWidth: 2,
      fill: true,
      tension: 0.3,
      pointRadius: 3,
      pointBackgroundColor: CYAN,
    }}]
  }},
  options: {{
    responsive: true, maintainAspectRatio: false,
    plugins: {{ legend: {{ display: false }}, tooltip: {{ callbacks: {{ label: ctx => ` ${{ctx.parsed.y >= 0 ? '+' : ''}}${{ctx.parsed.y.toFixed(2)}} USDT` }} }} }},
    scales: {{
      x: {{ ticks: {{ maxTicksLimit: 8 }}, grid: {{ display: false }} }},
      y: {{ ticks: {{ callback: v => '$' + v }}, grid: {{ color: 'rgba(30,45,71,0.5)' }} }}
    }}
  }}
}});

// ─── PnL Journalier ───
new Chart(document.getElementById('chartDaily'), {{
  type: 'bar',
  data: {{
    labels: {json.dumps(cum_labels)},
    datasets: [{{
      label: 'PnL journalier',
      data: {json.dumps(daily_values)},
      backgroundColor: {json.dumps(daily_values)}.map(v => v >= 0 ? 'rgba(16,185,129,0.7)' : 'rgba(239,68,68,0.7)'),
      borderRadius: 4,
    }}]
  }},
  options: {{
    responsive: true, maintainAspectRatio: false,
    plugins: {{ legend: {{ display: false }}, tooltip: {{ callbacks: {{ label: ctx => ` ${{ctx.parsed.y >= 0 ? '+' : ''}}${{ctx.parsed.y.toFixed(2)}} USDT` }} }} }},
    scales: {{
      x: {{ ticks: {{ maxTicksLimit: 8 }}, grid: {{ display: false }} }},
      y: {{ ticks: {{ callback: v => '$' + v }}, grid: {{ color: 'rgba(30,45,71,0.5)' }} }}
    }}
  }}
}});

// ─── Walk-Forward ───
new Chart(document.getElementById('chartWalkforward'), {{
  type: 'bar',
  data: {{
    labels: {json.dumps(bt_labels)},
    datasets: [
      {{ label: 'Sans IA', data: {json.dumps(bt_without)}, backgroundColor: 'rgba(148,163,184,0.6)', borderRadius: 4 }},
      {{ label: 'Avec IA', data: {json.dumps(bt_with)}, backgroundColor: {json.dumps(bt_with)}.map(v => v >= 0 ? 'rgba(16,185,129,0.8)' : 'rgba(239,68,68,0.8)'), borderRadius: 4 }}
    ]
  }},
  options: {{
    responsive: true, maintainAspectRatio: false,
    plugins: {{ legend: {{ position: 'top', labels: {{ boxWidth: 12 }} }}, tooltip: {{ callbacks: {{ label: ctx => ` ${{ctx.parsed.y.toFixed(1)}}%` }} }} }},
    scales: {{
      x: {{ ticks: {{ maxTicksLimit: 8 }}, grid: {{ display: false }} }},
      y: {{ ticks: {{ callback: v => v + '%' }}, grid: {{ color: 'rgba(30,45,71,0.5)' }} }}
    }}
  }}
}});

// ─── Feature Importance ───
new Chart(document.getElementById('chartFeatures'), {{
  type: 'bar',
  data: {{
    labels: {json.dumps(fi_labels)},
    datasets: [{{
      label: 'Corrélation avec label',
      data: {json.dumps(fi_values)},
      backgroundColor: {json.dumps(fi_values)}.map((v, i) => `hsl(${{220 + i * 8}}, 70%, 60%)`),
      borderRadius: 4,
    }}]
  }},
  options: {{
    indexAxis: 'y',
    responsive: true, maintainAspectRatio: false,
    plugins: {{ legend: {{ display: false }} }},
    scales: {{
      x: {{ ticks: {{ callback: v => v.toFixed(3) }}, grid: {{ color: 'rgba(30,45,71,0.5)' }} }},
      y: {{ ticks: {{ font: {{ size: 11 }} }}, grid: {{ display: false }} }}
    }}
  }}
}});
</script>

<script>
// ═══════════════════════════════════════════════════════
//  ACTION BAR — Pipeline GPU + Apply Optimizer
// ═══════════════════════════════════════════════════════
const API_BASE = window.location.port === '8889'
  ? 'http://' + window.location.hostname + ':8889'
  : (window.location.origin || 'https://trading-pascal.duckdns.org');

let _pollInterval = null;
let _logVisible   = false;
let _loginCb      = null;   // callback en attente après login
let _startedAt    = null;   // heure de démarrage pour le timer
let _wasPolling   = false;  // true seulement si le pipeline a été lancé depuis cette session

// ─── AUTH ───
function getAuthHeaders() {{
  const basic = localStorage.getItem('spy_basic_auth') || '';
  if (basic) return {{'Authorization': 'Basic ' + basic, 'Content-Type': 'application/json'}};
  const tok   = sessionStorage.getItem('auth_token') || localStorage.getItem('auth_token') || '';
  return tok   ? {{'Authorization': 'Bearer ' + tok,  'Content-Type': 'application/json'}}
               : {{'Content-Type': 'application/json'}};
}}

function showLoginModal(cb) {{
  _loginCb = cb || null;
  document.getElementById('loginErr').textContent = '';
  document.getElementById('loginOverlay').classList.add('visible');
  setTimeout(() => document.getElementById('loginUser').focus(), 100);
}}
function hideLoginModal() {{
  document.getElementById('loginOverlay').classList.remove('visible');
}}

async function submitLogin() {{
  const user = document.getElementById('loginUser').value.trim();
  const pass = document.getElementById('loginPass').value;
  if (!user || !pass) {{ document.getElementById('loginErr').textContent = 'Identifiants requis'; return; }}

  const encoded = btoa(user + ':' + pass);
  // Tester les credentials
  try {{
    const r = await fetch(API_BASE + '/api/spy-pipeline-status', {{
      headers: {{'Authorization': 'Basic ' + encoded, 'Content-Type': 'application/json'}}
    }});
    if (r.status === 401) {{
      document.getElementById('loginErr').textContent = '❌ Identifiants incorrects';
      return;
    }}
    // OK — sauvegarder
    localStorage.setItem('spy_basic_auth', encoded);
    hideLoginModal();
    if (_loginCb) {{ _loginCb(); _loginCb = null; }}
  }} catch(e) {{
    document.getElementById('loginErr').textContent = '❌ Erreur réseau: ' + e.message;
  }}
}}

// intercepte une 401 et propose login
async function authFetch(url, opts) {{
  const r = await fetch(url, opts);
  if (r.status === 401) {{
    return new Promise(resolve => {{
      showLoginModal(async () => {{
        // refaire la requête avec le nouveau token
        const r2 = await fetch(url, {{...opts, headers: getAuthHeaders()}});
        resolve(r2);
      }});
    }});
  }}
  return r;
}}

// ─── STATUS & STEPPER ───
function setStatus(txt, cls) {{
  const el = document.getElementById('psValue');
  el.textContent = txt;
  el.className = 'ps-value ' + (cls || 'ps-idle');
}}

const STEP_LABELS = ['', 'Téléchgt klines', 'Dataset Features', 'GPU Optuna', 'Walk-Fwd Backtest', 'Rapport HTML', 'Déploiement'];

function updateStepper(stepNum, pipelineStatus) {{
  const stepsDiv = document.getElementById('pipelineSteps');
  // Le stepper est toujours rendu (l'onglet Pipeline le garde display:block)
  // En idle, on affiche quand même les 6 étapes en état "en attente"
  stepsDiv.classList.add('visible');

  for (let n = 1; n <= 6; n++) {{
    const el = document.getElementById('pstep' + n);
    const icon = el.querySelector('.step-icon');
    el.className = 'step-item';
    if (pipelineStatus === 'success') {{
      el.classList.add('step-done');
      icon.textContent = '✓';
    }} else if (pipelineStatus === 'error') {{
      if (n < stepNum)        {{ el.classList.add('step-done'); icon.textContent = '✓'; }}
      else if (n === stepNum) {{ el.classList.add('step-error'); icon.textContent = n; }}
      else                    {{ icon.textContent = n; }}
    }} else if (pipelineStatus === 'running') {{
      if (n < stepNum)        {{ el.classList.add('step-done'); icon.textContent = '✓'; }}
      else if (n === stepNum) {{ el.classList.add('step-running'); icon.textContent = n; }}
      else                    {{ icon.textContent = n; }}
    }} else {{
      // idle — étapes grises, numéros affichés
      icon.textContent = n <= 5 ? n : '✓';
    }}
  }}

  // Timer (uniquement si pipeline en cours)
  if (_startedAt && pipelineStatus === 'running') {{
    const elapsed = Math.round((Date.now() - _startedAt) / 1000);
    const m = Math.floor(elapsed / 60), s = elapsed % 60;
    document.getElementById('stepsTimer').textContent = `⏱ ${{m}}min ${{String(s).padStart(2,'0')}}s`;
  }} else if (pipelineStatus !== 'running') {{
    document.getElementById('stepsTimer').textContent = '';
  }}
}}

function stripAnsi(s) {{
  return s.replace(/\\x1B\\[[0-9;]*m/g, '').replace(/\\[\\d+m/g, '');
}}

function showLog(lines) {{
  const el = document.getElementById('pipelineLog');
  const clean = (lines || []).map(stripAnsi).join('\\n');
  el.textContent = clean;
  el.scrollTop = el.scrollHeight;
}}

function toggleLog() {{
  _logVisible = !_logVisible;
  document.getElementById('pipelineLog').classList.toggle('visible', _logVisible);
}}

// ─── POLL STATUS ───
async function fetchStatus() {{
  try {{
    const r = await fetch(API_BASE + '/api/spy-pipeline-status', {{headers: getAuthHeaders()}});
    if (!r.ok) return;
    const d = await r.json();
    if (!d.success) return;

    showLog(d.log);

    // Détecter step actuel depuis le log (pattern "ÉTAPE N:")
    let stepNum = d.step_num || 0;

    if (d.status === 'running') {{
      setStatus('⏳ ' + (STEP_LABELS[stepNum] ? 'Étape ' + stepNum + '/6 — ' + STEP_LABELS[stepNum] : 'En cours…'), 'ps-running');
      document.getElementById('btnRunPipeline').disabled = true;
      document.getElementById('btnRunIcon').innerHTML = '<span class="spinner"></span>';
      document.getElementById('btnRunLabel').textContent = 'Analyse en cours…';
      updateStepper(stepNum, 'running');
    }} else if (d.status === 'success') {{
      setStatus('✅ Terminé — ' + (d.completed_at || '').substring(0,16).replace('T',' '), 'ps-success');
      resetRunBtn(false);
      updateStepper(6, 'success');
      stopPoll();
      // N'afficher le confirm que si le pipeline a été lancé depuis cette session
      if (_wasPolling) {{
        _wasPolling = false;
        setTimeout(() => {{ if(confirm('✅ Analyse GPU terminée ! Recharger le rapport ?')) location.reload(); }}, 800);
      }}
    }} else if (d.status === 'error') {{
      // Afficher l'erreur seulement si le pipeline a été lancé depuis cette session
      if (_wasPolling) {{
        setStatus('❌ ' + (d.error || 'Erreur inconnue'), 'ps-error');
        updateStepper(stepNum || 1, 'error');
        _wasPolling = false;
      }} else {{
        updateStepper(0, 'idle');
      }}
      resetRunBtn(false);
      stopPoll();
    }} else {{
      setStatus('En attente', 'ps-idle');
      resetRunBtn(false);
      // garder le stepper visible si récemment terminé
      if (document.getElementById('pipelineSteps').classList.contains('visible')) {{
        // ne pas masquer immédiatement
      }}
    }}
  }} catch(e) {{
    console.warn('Status poll error:', e);
  }}
}}

function resetRunBtn(running) {{
  const btn = document.getElementById('btnRunPipeline');
  btn.disabled = running;
  document.getElementById('btnRunIcon').textContent = running ? '' : '🚀';
  if (running) document.getElementById('btnRunIcon').innerHTML = '<span class="spinner"></span>';
  document.getElementById('btnRunLabel').textContent = running ? 'Analyse en cours…' : "Lancer l'analyse GPU";
}}

function startPoll() {{
  stopPoll();
  _wasPolling = true;
  _pollInterval = setInterval(fetchStatus, 3000);
}}
function stopPoll() {{
  if (_pollInterval) {{ clearInterval(_pollInterval); _pollInterval = null; }}
}}

// ─── RUN PIPELINE ───
async function runPipeline() {{
  if (!confirm('🚀 Lancer le pipeline complet ?\\n\\n• Mise à jour klines\\n• Envoi au PC GPU (RTX 5060 Ti)\\n• 300 trials Optuna\\n• Backtest walk-forward\\n• Nouveau rapport HTML\\n\\nDurée estimée: 15-25 min')) return;

  const doRun = async () => {{
    switchReportTab('pipeline');
    resetRunBtn(true);
    setStatus('⏳ Démarrage…', 'ps-running');
    document.getElementById('pipelineLog').classList.add('visible');
    _logVisible = true;
    document.getElementById('pipelineSteps').classList.add('visible');
    updateStepper(0, 'running');

    try {{
      const r = await authFetch(API_BASE + '/api/spy-run-pipeline', {{
        method: 'POST',
        headers: getAuthHeaders(),
        body: JSON.stringify({{gpu_trials: 300}})
      }});
      const d = await r.json();
      if (d.success) {{
        _startedAt = Date.now();
        setStatus('⏳ Lancé — Étape 1/6', 'ps-running');
        updateStepper(1, 'running');
        showLog(['🚀 Pipeline démarré: PID ' + d.pid + ', GPU trials=' + d.gpu_trials, 'Polling toutes les 3s…']);
        startPoll();
      }} else {{
        setStatus('❌ ' + (d.error || 'Erreur'), 'ps-error');
        resetRunBtn(false);
        showLog([d.error || 'Erreur inconnue']);
        updateStepper(1, 'error');
      }}
    }} catch(e) {{
      setStatus('❌ Erreur réseau', 'ps-error');
      resetRunBtn(false);
      showLog(['Erreur: ' + e.message]);
      updateStepper(1, 'error');
    }}
  }};

  // Vérifier qu'on a des credentials, sinon demander login d'abord
  if (!localStorage.getItem('spy_basic_auth') && !sessionStorage.getItem('auth_token') && !localStorage.getItem('auth_token')) {{
    showLoginModal(doRun);
  }} else {{
    doRun();
  }}
}}

// ─── APPLY OPTIMIZER ───
async function applyOptimizer() {{
  const btn = document.getElementById('btnApply');
  const res = document.getElementById('applyResult');

  if (!confirm('✅ Appliquer les optimisations du dernier run ?\\n\\n• Mise à jour du seuil ML du Signal Classifier\\n• Redémarrage du spy testnet\\n\\nConfirmer ?')) return;

  const doApply = async () => {{
    btn.disabled = true;
    document.getElementById('btnApplyIcon').innerHTML = '<span class="spinner"></span>';
    document.getElementById('btnApplyLabel').textContent = 'Application…';
    res.classList.remove('visible');

    try {{
      const r = await authFetch(API_BASE + '/api/spy-apply-optimizer', {{
        method: 'POST',
        headers: getAuthHeaders(),
        body: JSON.stringify({{}})
      }});
      const d = await r.json();
      btn.disabled = false;
      document.getElementById('btnApplyIcon').textContent = '✅';
      document.getElementById('btnApplyLabel').textContent = 'Appliquer les optimisations';

      if (d.success) {{
        res.style.cssText = '';
        res.innerHTML = '<strong>✅ Appliqué avec succès</strong><br>' +
          (d.changes || []).map(c => '• ' + c).join('<br>') +
          '<br><small style="color:#94a3b8">Threshold: ' + (d.threshold||0).toFixed(3) +
          ' | AUC: ' + (d.auc||0).toFixed(3) +
          ' | WR filtered: ' + (d.win_rate_filtered||0).toFixed(1) + '%</small>';
        res.classList.add('visible');
      }} else {{
        res.innerHTML = '<strong style="color:#ef4444">❌ Erreur:</strong> ' + (d.error || 'inconnue');
        res.style.background = 'rgba(239,68,68,.08)';
        res.style.borderColor = 'rgba(239,68,68,.3)';
        res.style.color = '#fca5a5';
        res.classList.add('visible');
      }}
    }} catch(e) {{
      btn.disabled = false;
      document.getElementById('btnApplyIcon').textContent = '✅';
      document.getElementById('btnApplyLabel').textContent = 'Appliquer les optimisations';
      res.innerHTML = '<strong style="color:#ef4444">❌ Erreur réseau:</strong> ' + e.message;
      res.classList.add('visible');
    }}
  }};

  if (!localStorage.getItem('spy_basic_auth') && !sessionStorage.getItem('auth_token') && !localStorage.getItem('auth_token')) {{
    showLoginModal(doApply);
  }} else {{
    doApply();
  }}
}}

// Init: vérifier statut au chargement
document.addEventListener('DOMContentLoaded', () => {{
  // Au chargement initial, on ignore les erreurs résiduelles (pipeline précédent)
  fetch(API_BASE + '/api/spy-pipeline-status', {{headers: getAuthHeaders()}})
    .then(r => r.ok ? r.json() : null)
    .then(d => {{
      if (!d || !d.success) return;
      if (d.status === 'running') {{
        // Pipeline actif → basculer sur onglet Pipeline + démarrer le poll
        switchReportTab('pipeline');
        const stepNum = d.step_num || 1;
        setStatus('⏳ ' + (STEP_LABELS[stepNum] ? 'Étape ' + stepNum + '/6 — ' + STEP_LABELS[stepNum] : 'En cours…'), 'ps-running');
        document.getElementById('btnRunPipeline').disabled = true;
        document.getElementById('btnRunIcon').innerHTML = '<span class="spinner"></span>';
        document.getElementById('btnRunLabel').textContent = 'Analyse en cours…';
        updateStepper(stepNum, 'running');
        showLog(d.log);
        startPoll();
      }}
      // error / idle / success → rien d'affiché au départ (affichage propre)
      // Mais on initialise quand même le stepper en idle pour afficher les étapes
      if (d.status !== 'running') {{
        updateStepper(0, 'idle');
      }}
    }}).catch(() => {{ updateStepper(0, 'idle'); }});
}});
</script>
</body>
</html>"""

    output.write_text(html, encoding="utf-8")
    print(f"  ✅ Rapport généré: {output}")
    print(f"     Taille: {output.stat().st_size / 1024:.1f} KB")
    return output


def main():
    parser = argparse.ArgumentParser(description="SPY Optimizer — HTML Report Generator")
    parser.add_argument("--output", type=str, default=str(DEFAULT_OUTPUT), help="Chemin du fichier HTML")
    parser.add_argument("--open", action="store_true", help="Ouvrir dans le navigateur")
    args = parser.parse_args()

    output = Path(args.output)

    print(f"\n{'='*60}")
    print(f"  📊 SPY Optimizer — Génération du rapport HTML")
    print(f"{'='*60}")
    print(f"  📂 Chargement des données...")

    data = load_data()
    trades = data["trades"]
    print(f"     {len(trades)} trades exploitables")
    if data["optimizer"]:
        print(f"     ✅ Résultats optimizer chargés ({data['optimizer'].get('timestamp','')[:10]})")
    if data["backtest"]:
        bw = data["backtest"].get("results", {}).get("windows", 0)
        print(f"     ✅ Walk-forward backtest chargé ({bw} fenêtres)")
    if data["gpu"]:
        print(f"     ✅ Résultats GPU chargés")
    if data["dataset"] is not None:
        print(f"     ✅ Dataset features: {len(data['dataset'])} samples × {len(data['dataset'].columns)} colonnes")

    print(f"\n  ⚙️  Génération du HTML...")
    generate_html(data, output)

    if args.open:
        import webbrowser
        webbrowser.open(f"file://{output.resolve()}")

    print(f"\n  📁 Rapport disponible sur:")
    print(f"     Local:  {output}")
    print(f"     Web:    https://trading-pascal.duckdns.org/spy_report.html")
    print(f"\n  💡 Pour servir le rapport via le dashboard:")
    print(f"     cp {output} /home/ubuntu/crypto_trading_bot/spy_report.html")
    print(f"{'='*60}")


if __name__ == "__main__":
    main()
