// 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 btTimeframe(tf) {
  return BT_TIMEFRAMES.find(t => t.id === tf) || null;
}

// Date civile (YYYY-MM-DD) d'une bougie JOURNALIÈRE. La source horodate une
// journée à minuit Londres (23:00 UTC la veille en heure d'été, 00:00 UTC le
// jour même en heure d'hiver : Forex), à minuit New York (04:00 ou 05:00 UTC :
// or, indices) ou, pour une séance écourtée de l'or et des indices (veille de
// fête, lendemain de Thanksgiving), en cours de journée (13:30 ou 14:30 UTC).
// Règle : à partir de 20:00 UTC, l'horodatage est le minuit Londres du
// LENDEMAIN ; avant, la date est celle de l'horodatage en UTC. Vérifiée sur
// les six fichiers 1 j : dates strictement croissantes, aucun doublon, aucun
// samedi ni dimanche. Règle unique, partagée par le libellé de la bougie
// courante (backtest.jsx, btBarDay) et par l'axe du graphique
// (backtest-amchart.jsx, _amBarMs).
function btDailyBarDay(unixSeconds) {
  const d = new Date(unixSeconds * 1000);
  if (d.getUTCHours() >= 20) d.setUTCDate(d.getUTCDate() + 1);
  return d.toISOString().slice(0, 10);
}

// 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.
// Fichiers journaliers Forex : l'ouverture fournie y vaut presque toujours la
// clôture du même jour (EUR/USD : O = C sur 959 bougies sur 1 300), si bien que
// toutes les bougies s'affichaient en bâtons et la légende montrait O = C.
// Le marché des changes cotant en continu, l'ouverture d'un jour est la
// clôture de la veille : on la reconstruit ainsi, en élargissant plus haut et
// plus bas si besoin. Les autres actifs (or, indices) ont de vraies ouvertures.
function _btFixDailyOpen(assetId, bars) {
  const a = BT_ASSETS.find(x => x.id === assetId);
  if (!a || a.category !== 'Forex' || !Array.isArray(bars)) return bars;
  for (let i = 1; i < bars.length; i++) {
    const b = bars[i];
    const open = bars[i - 1].close;
    b.open = open;
    if (open > b.high) b.high = open;
    if (open < b.low) b.low = open;
  }
  return bars;
}

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;
}

// Dernière ligne d'un fichier téléchargé en cours de séance : ce n'est pas une
// bougie mais le cours à l'instant du téléchargement (horodatage avec des
// secondes, « 08:16:31 »). Les fichiers du dépôt n'en contiennent plus ; la
// garde reste pour un fichier régénéré sans ce nettoyage. Seule la DERNIÈRE
// ligne est testée, et sur la minute ronde uniquement : les séances écourtées
// de l'or et des indices ont de vraies bougies 1 h à hh:30.
function _btDropSnapshotRow(bars) {
  if (bars.length && bars[bars.length - 1].time % 60 !== 0) bars.pop();
  return bars;
}

// 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);
        // Unité de base en échec : on lève, pour que le catch ci-dessous libère
        // AUSSI le cache de l'unité dérivée (sinon « Réessayer » relirait
        // indéfiniment ce tableau vide).
        if (!base.length) throw new Error('unité de base ' + d.from + ' indisponible');
        return _btAggregate(base, d.minutes);
      }
      // cache 'no-cache' : le navigateur REVALIDE sa copie auprès du serveur
      // (réponse 304 sans corps si le fichier n'a pas changé). L'ancien mode
      // 'force-cache' resservait indéfiniment un fichier périmé après une mise
      // à jour des données. L'adresse reste sans paramètre : le service worker
      // (sw.js) ne garde pour le hors-ligne que les adresses sans paramètre.
      const res = await fetch('data/backtest/' + assetId + '_' + tf + '.json', { cache: 'no-cache' });
      if (!res || !res.ok) throw new Error('HTTP ' + (res && res.status));
      const raw = await res.json();
      const bars = _btDropSnapshotRow(_btNormalize(raw));
      return tf === '1d' ? _btFixDailyOpen(assetId, bars) : bars;
    } 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, btTimeframe,
  btBarsRange, btDailyBarDay,
  loadBtBars,
});
