// Tempo — Backtester (bar-replay) : données & catalogue
// ─────────────────────────────────────────────────────────────────────────
// Module pur (pas de DOM, pas de React). Expose le catalogue d'actifs / TF et
// loadBtBars(assetId, tf) qui charge un fichier JSON pré-téléchargé sous
// /data/backtest/{ASSET}_{TF}.json, le convertit en bougies normalisées,
// le trie / dédoublonne et le met en cache mémoire.
//
// Format fichier source : tableau de [unixSeconds, open, high, low, close, volume].
// Format renvoyé       : [{ time, open, high, low, close, volume }] (time = unixSeconds).
// ─────────────────────────────────────────────────────────────────────────

// Catalogue des actifs.
//  decimals = précision d'affichage des prix.
//  contract = taille d'un « lot/contrat » → P&L = (sortie-entrée) × qty × contract.
//             Forex : 1 lot standard = 100 000 unités (1 pip = 10 $ sur 1 lot).
//             Or    : 1 lot = 100 oz (1 $ de mouvement = 100 $ sur 1 lot).
//             Indices CFD : 1 contrat = 1 $ par point.
//  unit = libellé de la quantité (lot / contrat).
//  spread = écart appliqué à l'entrée des ordres marché (unités de prix,
//           valeurs réalistes type prop-firm).
const BT_ASSETS = [
  { id: 'EURUSD', label: 'EUR/USD',           category: 'Forex',   decimals: 5, contract: 100000, unit: 'lot',     spread: 0.00010 },
  { id: 'GBPUSD', label: 'GBP/USD',           category: 'Forex',   decimals: 5, contract: 100000, unit: 'lot',     spread: 0.00012 },
  { id: 'AUDUSD', label: 'AUD/USD',           category: 'Forex',   decimals: 5, contract: 100000, unit: 'lot',     spread: 0.00012 },
  { id: 'XAUUSD', label: 'XAU/USD · Or',      category: 'Métaux',  decimals: 2, contract: 100,    unit: 'lot',     spread: 0.30 },
  { id: 'NAS100', label: 'NAS100 · Nasdaq',   category: 'Indices', decimals: 2, contract: 1,      unit: 'contrat', spread: 1.0 },
  { id: 'US30',   label: 'US30 · Dow',        category: 'Indices', decimals: 2, contract: 1,      unit: 'contrat', spread: 2.0 },
];

// Catégories ordonnées pour le sélecteur groupé.
const BT_CATEGORIES = ['Forex', 'Métaux', 'Indices'];

// Timeframes proposés. M1→D. 1m/5m/15m/30m/1h/1d ont leur fichier ;
// 3m et 4h sont DÉRIVÉS par agrégation (voir BT_DERIVED).
const BT_TIMEFRAMES = [
  { id: '1m',  label: '1 min',  minutes: 1 },
  { id: '3m',  label: '3 min',  minutes: 3 },
  { id: '5m',  label: '5 min',  minutes: 5 },
  { id: '15m', label: '15 min', minutes: 15 },
  { id: '30m', label: '30 min', minutes: 30 },
  { id: '1h',  label: '1 h',    minutes: 60 },
  { id: '4h',  label: '4 h',    minutes: 240 },
  { id: '1d',  label: '1 j',    minutes: 1440 },
];

// Timeframes dérivés (pas de fichier) : agrégés depuis un timeframe de base.
const BT_DERIVED = {
  '3m': { from: '1m', minutes: 3 },
  '4h': { from: '1h', minutes: 240 },
};

// Agrège des bougies vers un timeframe plus grand (buckets alignés sur l'epoch).
function _btAggregate(bars, tfMinutes) {
  if (!Array.isArray(bars) || !bars.length || !tfMinutes) return bars || [];
  const sec = tfMinutes * 60;
  const out = [];
  let cur = null, curBucket = null;
  for (const b of bars) {
    const bucket = Math.floor(b.time / sec) * sec;
    if (curBucket !== bucket) {
      if (cur) out.push(cur);
      cur = { time: bucket, open: b.open, high: b.high, low: b.low, close: b.close, volume: b.volume || 0 };
      curBucket = bucket;
    } else {
      cur.high = Math.max(cur.high, b.high);
      cur.low = Math.min(cur.low, b.low);
      cur.close = b.close;
      cur.volume += b.volume || 0;
    }
  }
  if (cur) out.push(cur);
  return out;
}

// Accès rapides.
function btAsset(assetId) {
  return BT_ASSETS.find(a => a.id === assetId) || null;
}
function btAssetDecimals(assetId) {
  const a = btAsset(assetId);
  return a ? a.decimals : 5;
}
function btAssetContract(assetId) {
  const a = btAsset(assetId);
  return a && a.contract > 0 ? a.contract : 1;
}
function btAssetUnit(assetId) {
  const a = btAsset(assetId);
  return (a && a.unit) || 'lot';
}
function btAssetSpread(assetId) {
  const a = btAsset(assetId);
  return a && Number.isFinite(a.spread) && a.spread > 0 ? a.spread : 0;
}
function btTimeframe(tf) {
  return BT_TIMEFRAMES.find(t => t.id === tf) || null;
}

// Recherche binaire : dernier index dont time <= t (−1 si aucun / entrée invalide).
// Copie UI du helper interne du moteur — sert au popover de saut à date
// (bornes min/max, préview) sans dépendre du hook.
function btFindIndexByTime(bars, unixSeconds) {
  if (!Array.isArray(bars) || !bars.length || !Number.isFinite(unixSeconds)) return -1;
  let lo = 0, hi = bars.length - 1, res = -1;
  while (lo <= hi) {
    const mid = (lo + hi) >> 1;
    if (bars[mid].time <= unixSeconds) { res = mid; lo = mid + 1; }
    else { hi = mid - 1; }
  }
  return res;
}

// Bornes temporelles d'une série (unix seconds) — null si vide / invalide.
function btBarsRange(bars) {
  if (!Array.isArray(bars) || !bars.length) return null;
  return { first: bars[0].time, last: bars[bars.length - 1].time };
}

// Cache mémoire : clé "ASSET_TF" → Promise<bars>. On cache la Promise pour
// éviter les doubles fetch concurrents et garder l'API simple.
const _BT_CACHE = {};

// Convertit + trie + dédoublonne un tableau brut de tuples.
function _btNormalize(raw) {
  if (!Array.isArray(raw)) return [];
  const out = [];
  for (let i = 0; i < raw.length; i++) {
    const row = raw[i];
    if (!Array.isArray(row) || row.length < 5) continue;
    const time = Number(row[0]);
    const open = Number(row[1]);
    const high = Number(row[2]);
    const low = Number(row[3]);
    const close = Number(row[4]);
    const volume = row.length > 5 ? Number(row[5]) || 0 : 0;
    // Rejette toute bougie corrompue (NaN).
    if (!Number.isFinite(time) || !Number.isFinite(open) || !Number.isFinite(high) ||
        !Number.isFinite(low) || !Number.isFinite(close)) continue;
    out.push({ time, open, high, low, close, volume });
  }
  // Tri croissant par temps.
  out.sort((a, b) => a.time - b.time);
  // Déduplication : on garde la dernière bougie d'un même timestamp.
  const dedup = [];
  for (let i = 0; i < out.length; i++) {
    if (dedup.length && dedup[dedup.length - 1].time === out[i].time) {
      dedup[dedup.length - 1] = out[i];
    } else {
      dedup.push(out[i]);
    }
  }
  return dedup;
}

// Charge (et met en cache) les bougies d'un actif / timeframe.
// Renvoie toujours un tableau (vide en cas d'erreur réseau / parsing).
async function loadBtBars(assetId, tf) {
  if (!assetId || !tf) return [];
  const key = assetId + '_' + tf;
  if (_BT_CACHE[key]) {
    try { return await _BT_CACHE[key]; }
    catch (e) { delete _BT_CACHE[key]; return []; }
  }
  const p = (async () => {
    try {
      // Timeframe dérivé : on charge le timeframe de base (mis en cache) et on agrège.
      const d = BT_DERIVED[tf];
      if (d) {
        const base = await loadBtBars(assetId, d.from);
        return _btAggregate(base, d.minutes);
      }
      const res = await fetch('data/backtest/' + assetId + '_' + tf + '.json', { cache: 'force-cache' });
      if (!res || !res.ok) throw new Error('HTTP ' + (res && res.status));
      const raw = await res.json();
      return _btNormalize(raw);
    } catch (e) {
      // Échec réseau / parsing : on log discrètement et on libère le cache pour
      // permettre une nouvelle tentative ultérieure.
      try { console.warn('[Tempo] loadBtBars a échoué pour', key, e); } catch (_) {}
      delete _BT_CACHE[key];
      return [];
    }
  })();
  _BT_CACHE[key] = p;
  return p;
}

Object.assign(window, {
  BT_ASSETS, BT_CATEGORIES, BT_TIMEFRAMES,
  btAsset, btAssetDecimals, btAssetContract, btAssetUnit, btAssetSpread, btTimeframe,
  btFindIndexByTime, btBarsRange,
  loadBtBars,
});
