/**
 * dashboard.js — APEX AI-Powered EA Optimizer
 * Handles all SocketIO events, chart rendering, and UI state.
 */

'use strict';

/* ══════════════════════════════════════════════════════════════════════
   UTILITIES
══════════════════════════════════════════════════════════════════════ */

function el(id) { return document.getElementById(id); }

function fmtMoney(v) {
  const n = parseFloat(v) || 0;
  const abs = Math.abs(n);
  let s;
  if (abs >= 1000000) s = (n / 1000000).toFixed(2) + 'M';
  else if (abs >= 1000) s = (n / 1000).toFixed(1) + 'k';
  else s = n.toFixed(2);
  return (n >= 0 ? '$' : '-$') + (n < 0 ? s.replace('-', '') : s);
}

function fmtPct(v, decimals) {
  return (parseFloat(v) || 0).toFixed(decimals != null ? decimals : 2) + '%';
}

function fmtTime(secs) {
  const s = Math.floor(secs);
  const h = Math.floor(s / 3600);
  const m = Math.floor((s % 3600) / 60);
  const sec = s % 60;
  if (h > 0) return h + 'h ' + String(m).padStart(2, '0') + 'm';
  return String(m).padStart(2, '0') + ':' + String(sec).padStart(2, '0');
}

function nowStr() {
  return new Date().toLocaleTimeString('en-US', { hour12: false });
}

function escapeHtml(str) {
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}

function safe(id, fn) {
  const e = el(id);
  if (e) fn(e);
}

/* ══════════════════════════════════════════════════════════════════════
   GLOBAL STATE
══════════════════════════════════════════════════════════════════════ */

const state = {
  runs: 0,
  isRunning: false,
  startTime: null,
  elapsedTimer: null,
  logCount: 0,
  bestProfit: null,
  sparkData: { profit: [], dd: [], pf: [], trades: [], runs: [] },
};

/* ══════════════════════════════════════════════════════════════════════
   CHART.JS DEFAULTS
══════════════════════════════════════════════════════════════════════ */

Chart.defaults.color = '#64748b';
Chart.defaults.font.family = "'Inter', -apple-system, sans-serif";
Chart.defaults.font.size = 11;

/* ── Equity Curve ─────────────────────────────────────────────────── */

const equityChart = new Chart(el('equityChart').getContext('2d'), {
  type: 'line',
  data: {
    labels: [],
    datasets: [
      {
        label: 'Equity',
        data: [],
        borderColor: '#4f46e5',
        backgroundColor: (ctx) => {
          const { ctx: c, chartArea } = ctx.chart;
          if (!chartArea) return 'transparent';
          const g = c.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);
          g.addColorStop(0, 'rgba(79,70,229,0.22)');
          g.addColorStop(1, 'rgba(79,70,229,0)');
          return g;
        },
        borderWidth: 2, pointRadius: 0, pointHoverRadius: 4,
        pointHoverBackgroundColor: '#4f46e5', tension: 0.4, fill: true,
      },
      {
        label: 'Balance',
        data: [],
        borderColor: '#00d4aa',
        backgroundColor: 'transparent',
        borderWidth: 1.5,
        borderDash: [5, 4],
        pointRadius: 0, pointHoverRadius: 4,
        pointHoverBackgroundColor: '#00d4aa', tension: 0.4, fill: false,
      },
    ],
  },
  options: {
    responsive: true, maintainAspectRatio: false,
    animation: { duration: 400 },
    interaction: { intersect: false, mode: 'index' },
    plugins: {
      legend: { display: false },
      tooltip: {
        backgroundColor: '#0f1629', borderColor: 'rgba(255,255,255,0.1)', borderWidth: 1,
        padding: 10, titleColor: '#94a3b8', bodyColor: '#e2e8f0',
        callbacks: { label: (ctx) => ' ' + ctx.dataset.label + ': $' + ctx.parsed.y.toFixed(2) },
      },
    },
    scales: {
      x: { grid: { color: 'rgba(255,255,255,0.04)' }, ticks: { maxTicksLimit: 8, maxRotation: 0, color: '#475569', font: { size: 10 } }, border: { display: false } },
      y: { grid: { color: 'rgba(255,255,255,0.04)' }, ticks: { maxTicksLimit: 6, color: '#475569', font: { size: 10 }, callback: (v) => '$' + v.toFixed(0) }, border: { display: false } },
    },
  },
});

/* ── Drawdown Chart ───────────────────────────────────────────────── */

const drawdownChart = new Chart(el('drawdownChart').getContext('2d'), {
  type: 'line',
  data: {
    labels: [],
    datasets: [{
      label: 'Drawdown',
      data: [],
      borderColor: '#ef4444',
      backgroundColor: (ctx) => {
        const { ctx: c, chartArea } = ctx.chart;
        if (!chartArea) return 'transparent';
        const g = c.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);
        g.addColorStop(0, 'rgba(239,68,68,0.35)');
        g.addColorStop(1, 'rgba(239,68,68,0.02)');
        return g;
      },
      borderWidth: 1.5, pointRadius: 0, pointHoverRadius: 3, tension: 0.4, fill: true,
    }],
  },
  options: {
    responsive: true, maintainAspectRatio: false,
    animation: { duration: 400 },
    interaction: { intersect: false, mode: 'index' },
    plugins: {
      legend: { display: false },
      tooltip: {
        backgroundColor: '#0f1629', borderColor: 'rgba(255,255,255,0.1)', borderWidth: 1,
        padding: 8, titleColor: '#94a3b8', bodyColor: '#ef4444',
        callbacks: { label: (ctx) => ' Drawdown: ' + ctx.parsed.y.toFixed(2) + '%' },
      },
    },
    scales: {
      x: { grid: { color: 'rgba(255,255,255,0.04)' }, ticks: { maxTicksLimit: 8, maxRotation: 0, color: '#475569', font: { size: 10 } }, border: { display: false } },
      y: { grid: { color: 'rgba(255,255,255,0.04)' }, ticks: { maxTicksLimit: 4, color: '#475569', font: { size: 10 }, callback: (v) => v.toFixed(1) + '%' }, border: { display: false } },
    },
  },
});

/* ── Sparklines ───────────────────────────────────────────────────── */

const sparklineCharts = {};

function createSparkline(canvasId, color, fillColor) {
  const canvas = el(canvasId);
  if (!canvas) return null;
  return new Chart(canvas.getContext('2d'), {
    type: 'line',
    data: { labels: [], datasets: [{ data: [], borderColor: color, backgroundColor: fillColor || 'transparent', borderWidth: 1.5, pointRadius: 0, tension: 0.4, fill: !!fillColor }] },
    options: { responsive: false, animation: { duration: 250 }, plugins: { legend: { display: false }, tooltip: { enabled: false } }, scales: { x: { display: false }, y: { display: false } } },
  });
}

function initSparklines() {
  sparklineCharts.profit = createSparkline('spark-profit', '#10b981', 'rgba(16,185,129,0.12)');
  sparklineCharts.dd     = createSparkline('spark-dd',     '#ef4444', 'rgba(239,68,68,0.12)');
  sparklineCharts.pf     = createSparkline('spark-pf',     '#f59e0b', 'rgba(245,158,11,0.10)');
  sparklineCharts.trades = createSparkline('spark-trades',  '#00d4aa', 'rgba(0,212,170,0.10)');
  sparklineCharts.runs   = createSparkline('spark-runs',   '#7c6dfa', 'rgba(124,109,250,0.10)');
}

function pushSparkData(key, value) {
  const arr = state.sparkData[key];
  arr.push(value);
  if (arr.length > 20) arr.shift();
  const c = sparklineCharts[key];
  if (!c) return;
  c.data.labels = arr.map((_, i) => i);
  c.data.datasets[0].data = arr;
  c.update('none');
}

/* ══════════════════════════════════════════════════════════════════════
   METRIC CARDS
══════════════════════════════════════════════════════════════════════ */

function updateMetricCards(d) {
  const profit = parseFloat(d.net_profit  || d.best_net_profit  || 0);
  const maxDD  = parseFloat(d.max_drawdown || d.best_max_drawdown || d.max_dd || 0);
  const pf     = parseFloat(d.profit_factor || d.best_pf || d.pf || 0);
  const trades = parseInt(d.total_trades  || d.best_total_trades || d.trades || 0, 10);

  // Net Profit
  safe('m-net-profit', e => { e.textContent = fmtMoney(profit); });
  if (state.bestProfit === null) state.bestProfit = profit;
  const delta = state.bestProfit !== 0 ? ((profit - state.bestProfit) / Math.abs(state.bestProfit) * 100) : 0;
  safe('m-net-profit-delta-val', e => { e.textContent = (delta >= 0 ? '+' : '') + delta.toFixed(1) + '%'; });
  const arrow = el('m-net-profit-delta') && el('m-net-profit-delta').querySelector('span:first-child');
  if (arrow) { arrow.textContent = delta >= 0 ? '↑' : '↓'; arrow.className = delta >= 0 ? 'delta-up' : 'delta-down'; }
  pushSparkData('profit', profit);

  // Max Drawdown
  safe('m-max-dd', e => { e.textContent = fmtPct(maxDD); });
  safe('max-dd-badge', e => { e.textContent = 'Max DD: ' + fmtPct(maxDD, 1); });
  pushSparkData('dd', maxDD);

  // Profit Factor
  safe('m-pf', e => { e.textContent = pf.toFixed(2); });
  safe('m-pf-quality', e => {
    if (pf >= 2)       { e.textContent = 'Excellent'; e.style.color = 'var(--green)'; }
    else if (pf >= 1.5){ e.textContent = 'Good';      e.style.color = 'var(--teal)'; }
    else if (pf >= 1)  { e.textContent = 'Marginal';  e.style.color = 'var(--yellow)'; }
    else               { e.textContent = 'Poor';       e.style.color = 'var(--red)'; }
  });
  pushSparkData('pf', pf);

  // Trades
  safe('m-trades', e => { e.textContent = trades.toLocaleString(); });
  pushSparkData('trades', trades);
}

function updateRunsCounter() {
  state.runs++;
  safe('m-runs', e => { e.textContent = state.runs.toString(); });
  pushSparkData('runs', state.runs);
}

/* ══════════════════════════════════════════════════════════════════════
   AI GAUGE
══════════════════════════════════════════════════════════════════════ */

const CIRCUMFERENCE = 163.4; // 2π × 26

function updateAIGauge(pct, label) {
  const color = { high: '#10b981', medium: '#f59e0b', low: '#ef4444' }[label] || '#00d4aa';
  safe('ai-gauge-arc', e => {
    e.style.strokeDashoffset = (CIRCUMFERENCE * (1 - pct / 100)).toString();
    e.style.stroke = color;
  });
  safe('ai-gauge-pct', e => { e.textContent = pct + '%'; });
  safe('ai-conf-label', e => {
    e.textContent = label ? label.charAt(0).toUpperCase() + label.slice(1) : '—';
    e.style.color = color;
  });
}

/* ══════════════════════════════════════════════════════════════════════
   HEADER SCORE
══════════════════════════════════════════════════════════════════════ */

function updateHeaderScore(score, delta) {
  const s = parseFloat(score);
  if (isNaN(s)) return;
  safe('hdr-score', e => { e.textContent = s.toFixed(3); });
  if (delta != null) {
    const d2 = parseFloat(delta);
    if (!isNaN(d2)) {
      const sign = d2 >= 0 ? '+' : '';
      safe('hdr-score-delta', e => {
        e.textContent = sign + d2.toFixed(3) + ' improvement';
        e.style.color = d2 >= 0 ? 'var(--green)' : 'var(--red)';
      });
    }
  } else {
    safe('hdr-score-delta', e => { e.textContent = 'Latest result'; e.style.color = ''; });
  }
}

/* ══════════════════════════════════════════════════════════════════════
   AI PANEL — shared update function
══════════════════════════════════════════════════════════════════════ */

function updateAIPanel(d) {
  if (!d) return;

  safe('ai-waiting', e => { e.style.display = 'none'; });
  safe('ai-content', e => { e.style.display = ''; });

  // Preview (always visible) — headline + diagnosis
  if (d.headline) {
    safe('ai-headline', e => { e.textContent = d.headline; e.style.display = ''; });
  }
  if (d.diagnosis) {
    safe('ai-diagnosis-text', e => { e.textContent = d.diagnosis; });
  }

  // Details section — populate but keep collapsed until user clicks "View Details"
  const patterns = d.patterns || d.strengths || [];
  if (patterns.length) {
    safe('ai-strengths-section', e => { e.style.display = ''; });
    safe('ai-strengths-list', e => {
      e.innerHTML = patterns.slice(0, 4).map(p => '<li>' + escapeHtml(p) + '</li>').join('');
    });
  }

  const suggestions = d.suggestions || [];
  if (suggestions.length) {
    const s = suggestions[0];
    let txt = '';
    if (typeof s === 'string') {
      txt = s;
    } else {
      txt = s.reason || '';
      if (s.param || s.parameter) {
        const param = s.param || s.parameter;
        txt += ' (' + param + ': ' + (s.from !== undefined ? s.from : '') + ' → ' + (s.to !== undefined ? s.to : '') + ')';
      }
    }
    safe('ai-suggestion-text', e => { e.textContent = txt; });
    safe('ai-suggestion-box', e => { e.style.display = ''; });
  }

  const risks = d.risk_flags || d.risks || [];
  if (risks.length) {
    safe('ai-risks-section', e => { e.style.display = ''; });
    safe('ai-risks-list', e => {
      e.innerHTML = risks.slice(0, 3).map(r => '<li>' + escapeHtml(r) + '</li>').join('');
    });
  }

  // Confidence (always shown at bottom)
  const conf = (typeof d.confidence === 'string' ? d.confidence : 'medium').toLowerCase();
  safe('ai-panel-conf-badge', e => {
    e.textContent = conf;
    e.className = 'ai-conf-badge ' + conf;
  });

  const pctMap = { high: 87, medium: 62, low: 38 };
  const pct = typeof d.confidence_pct === 'number' ? d.confidence_pct : (pctMap[conf] || 62);
  updateAIGauge(pct, conf);

  if (d.run_id) safe('ai-run-ref', e => { e.textContent = 'Run: ' + d.run_id; });

  safe('ai-badge', e => { e.style.display = ''; });
}

/* ══════════════════════════════════════════════════════════════════════
   RECENT RUNS TABLE
══════════════════════════════════════════════════════════════════════ */

const MAX_RECENT_RUNS = 8;
const recentRuns = [];

function addRecentRun(d) {
  const profit = parseFloat(d.net_profit || 0);
  const pf     = parseFloat(d.profit_factor || d.pf || 0);
  const maxDD  = parseFloat(d.max_drawdown || d.max_dd || 0);
  const run_id = d.run_id || null;

  // Derive verdict: use explicit field if present, otherwise infer from passing
  let verdict = (d.verdict || '').toUpperCase().replace(/ /g, '_');
  if (!verdict) {
    verdict = d.passing === false ? 'NOT_RELIABLE' : (d.passing === true ? 'PASSING' : 'PENDING');
  }

  const verdictClass = {
    RECOMMENDED:  'verdict-recommended',
    PASSING:      'verdict-recommended',
    RISKY:        'verdict-risky',
    PENDING:      'verdict-risky',
    NOT_RELIABLE: 'verdict-not-reliable',
  }[verdict] || 'verdict-risky';

  const verdictLabel = {
    RECOMMENDED:  'Recommended',
    PASSING:      'Passing',
    RISKY:        'Risky',
    PENDING:      'Pending',
    NOT_RELIABLE: 'Not Reliable',
  }[verdict] || verdict.replace(/_/g, ' ');

  recentRuns.unshift({ profit, pf, maxDD, verdictClass, verdictLabel, run_id });
  if (recentRuns.length > MAX_RECENT_RUNS) recentRuns.pop();

  const tbody = el('recent-runs-tbody');
  if (!tbody) return;
  tbody.innerHTML = recentRuns.map((r, i) => `
    <tr>
      <td class="run-num">#${state.runs - i}</td>
      <td class="${r.profit >= 0 ? 'profit-pos' : 'profit-neg'}">${fmtMoney(r.profit)}</td>
      <td style="color:var(--yellow);font-weight:600;">${r.pf.toFixed(2)}</td>
      <td style="color:var(--red);">${r.maxDD.toFixed(1)}%</td>
      <td><span class="${r.verdictClass}">${r.verdictLabel}</span></td>
      <td>${r.run_id ? `<button class="detail-btn" onclick="openRunDetail('${escapeHtml(r.run_id)}')">Details</button>` : ''}</td>
    </tr>
  `).join('');
}

/* ══════════════════════════════════════════════════════════════════════
   PARAMETERS TABLE
══════════════════════════════════════════════════════════════════════ */

function updateParamsTable(params) {
  if (!params || typeof params !== 'object') return;
  const tbody = el('params-tbody');
  if (!tbody) return;
  const entries = Object.entries(params);
  if (!entries.length) return;

  tbody.innerHTML = entries.slice(0, 8).map(([key, val], i) => {
    const impactClass = i < 2 ? 'impact-high' : i < 5 ? 'impact-medium' : 'impact-low';
    const impactLabel = i < 2 ? 'High' : i < 5 ? 'Medium' : 'Low';
    const display = typeof val === 'number' ? val.toFixed(val % 1 !== 0 ? 4 : 0) : val;
    return `
      <tr>
        <td class="param-name">${escapeHtml(key)}</td>
        <td class="param-val">${escapeHtml(String(display))}</td>
        <td><span class="${impactClass}">${impactLabel}</span></td>
      </tr>
    `;
  }).join('');
}

/* ══════════════════════════════════════════════════════════════════════
   CHART UPDATES
══════════════════════════════════════════════════════════════════════ */

function pushEquityPoint(equity, balance, label) {
  const MAX = 80;
  const lbl = label || nowStr();
  equityChart.data.labels.push(lbl);
  equityChart.data.datasets[0].data.push(parseFloat(equity) || 0);
  equityChart.data.datasets[1].data.push(parseFloat(balance) || 0);
  if (equityChart.data.labels.length > MAX) {
    equityChart.data.labels.shift();
    equityChart.data.datasets.forEach(ds => ds.data.shift());
  }
  equityChart.update('none');
}

function pushDDPoint(dd, label) {
  const MAX = 80;
  const lbl = label || nowStr();
  drawdownChart.data.labels.push(lbl);
  drawdownChart.data.datasets[0].data.push(parseFloat(dd) || 0);
  if (drawdownChart.data.labels.length > MAX) {
    drawdownChart.data.labels.shift();
    drawdownChart.data.datasets[0].data.shift();
  }
  drawdownChart.update('none');
}

function populateChartsFromEquityCurve(curve) {
  if (!Array.isArray(curve) || !curve.length) return;
  equityChart.data.labels = [];
  equityChart.data.datasets[0].data = [];
  equityChart.data.datasets[1].data = [];
  drawdownChart.data.labels = [];
  drawdownChart.data.datasets[0].data = [];

  const step = Math.max(1, Math.floor(curve.length / 80));
  curve.forEach((pt, i) => {
    if (i % step !== 0 && i !== curve.length - 1) return;
    const lbl = pt.date || pt.time || String(i + 1);
    equityChart.data.labels.push(lbl);
    equityChart.data.datasets[0].data.push(parseFloat(pt.equity || pt.Equity || 0));
    equityChart.data.datasets[1].data.push(parseFloat(pt.balance || pt.Balance || 0));
    drawdownChart.data.labels.push(lbl);
    drawdownChart.data.datasets[0].data.push(parseFloat(pt.drawdown || pt.Drawdown || 0));
  });

  equityChart.update();
  drawdownChart.update();
}

/* ══════════════════════════════════════════════════════════════════════
   PROGRESS
══════════════════════════════════════════════════════════════════════ */

function updateProgress(d) {
  // Use progress_pct if available, fall back to run_number/total
  let pct = null;
  if (d.progress_pct !== undefined) pct = d.progress_pct;
  else if (d.run_number !== undefined && d.total) pct = Math.round(d.run_number / d.total * 100);

  if (pct !== null) {
    const p = Math.min(100, pct);
    safe('opt-progress-fill', e => { e.style.width = p + '%'; });
    safe('hdr-prog-fill',     e => { e.style.width = p + '%'; });
    safe('hdr-pct',           e => { e.textContent = p + '% Complete'; });
  }

  if (d.run_number !== undefined && d.total !== undefined) {
    safe('opt-gen',       e => { e.textContent = 'Run ' + d.run_number + ' / ' + d.total; });
    safe('hdr-gen-cur',   e => { e.textContent = d.run_number; });
    safe('hdr-gen-total', e => { e.textContent = d.total; });
    safe('opt-remaining', e => {
      if (!state.startTime || !d.run_number) return;
      const elapsed = (Date.now() - state.startTime) / 1000;
      const rem = (elapsed / d.run_number) * (d.total - d.run_number);
      e.textContent = fmtTime(rem);
    });
  }
}

/* ══════════════════════════════════════════════════════════════════════
   ELAPSED TIMER
══════════════════════════════════════════════════════════════════════ */

function startElapsedTimer() {
  if (!state.startTime) state.startTime = Date.now();
  stopElapsedTimer();
  state.elapsedTimer = setInterval(() => {
    const secs = Math.floor((Date.now() - state.startTime) / 1000);
    safe('opt-elapsed', e => { e.textContent = fmtTime(secs); });
  }, 1000);
}

function stopElapsedTimer() {
  if (state.elapsedTimer) { clearInterval(state.elapsedTimer); state.elapsedTimer = null; }
}

/* ══════════════════════════════════════════════════════════════════════
   RUNNING STATE TOGGLE
══════════════════════════════════════════════════════════════════════ */

function setRunningState(isRunning, label) {
  state.isRunning = isRunning;

  safe('run-indicator', e => { e.classList.toggle('active', isRunning); });
  safe('btn-stop',  e => { e.classList.toggle('active', isRunning); });
  // (btn-pause removed — pipeline doesn't support pause yet)

  safe('hdr-dot',        e => { e.classList.toggle('running', isRunning); });
  safe('hdr-status',     e => { e.textContent = isRunning ? (label || 'Optimizing') : 'System Idle'; });
  safe('hdr-status-sub', e => { e.textContent = isRunning ? 'Optimization in progress...' : 'Ready to optimize'; });

  if (isRunning) {
    if (label) safe('run-indicator-label', e => { e.textContent = label; });
    startElapsedTimer();
  } else {
    stopElapsedTimer();
  }
}

/* ══════════════════════════════════════════════════════════════════════
   PHASE STEPS
══════════════════════════════════════════════════════════════════════ */

const phaseMap = {
  phase1: 1, phase_1: 1, broad: 1, scan: 1,
  phase2: 2, phase_2: 2, deep: 2, refine: 2,
  phase3: 3, phase_3: 3, validate: 3, validation: 3, phase3_oos: 3, phase3_sens: 3,
};

function setPhaseActive(phaseNum, meta) {
  for (let i = 1; i <= 3; i++) {
    const step = el('phase-step-' + i);
    if (!step) continue;
    step.classList.remove('active', 'done');
    if (i < phaseNum) step.classList.add('done');
    if (i === phaseNum) step.classList.add('active');
  }
  const labels = { 1: 'Exploration', 2: 'Iteration', 3: 'Validation' };
  safe('opt-phase', e => { e.textContent = labels[phaseNum] || 'Phase ' + phaseNum; });
  if (meta && typeof meta.total === 'number') {
    const mode = meta.mode === 'autonomous' ? ' · AI' : '';
    safe('phase-sub-' + phaseNum, e => {
      e.textContent = `0/${meta.total}${mode}`;
    });
  }
}

function updatePhaseSubProgress(phaseNum, current, total) {
  safe('phase-sub-' + phaseNum, e => {
    e.textContent = `${current}/${total}`;
  });
}

/* ══════════════════════════════════════════════════════════════════════
   LIVE LOG
══════════════════════════════════════════════════════════════════════ */

const MAX_LOG_LINES = 200;

function addLog(msg, level) {
  const lvl = level || 'info';
  state.logCount++;
  safe('log-count', e => { e.textContent = state.logCount; });

  const feed = el('log-feed');
  if (!feed) return;

  const line = document.createElement('div');
  line.className = 'log-line ' + lvl;
  line.innerHTML = '<span class="log-time">' + nowStr() + '</span><span class="log-msg">' + escapeHtml(String(msg)) + '</span>';
  feed.appendChild(line);
  while (feed.children.length > MAX_LOG_LINES) feed.removeChild(feed.firstChild);
  feed.scrollTop = feed.scrollHeight;
}

function toggleLog() {
  const body = el('log-body');
  const icon = el('log-toggle-icon');
  if (body) body.classList.toggle('open');
  if (icon) icon.classList.toggle('open');
}

/* ══════════════════════════════════════════════════════════════════════
   MT5 STATUS
══════════════════════════════════════════════════════════════════════ */

function updateMT5Status(statusStr, label) {
  const dot = el('mt5-dot');
  if (dot) dot.classList.toggle('disconnected', statusStr === 'disconnected');
  safe('mt5-state', e => { e.textContent = label || statusStr; });
}

/* ══════════════════════════════════════════════════════════════════════
   VERDICT OVERLAY
══════════════════════════════════════════════════════════════════════ */

function showVerdict(d) {
  const icons   = { RECOMMENDED: '✅', RISKY: '⚠️', NOT_RELIABLE: '❌' };
  const colors  = { RECOMMENDED: '#10b981', RISKY: '#f59e0b', NOT_RELIABLE: '#ef4444' };
  const subs    = {
    RECOMMENDED:  'Consistent returns with controlled drawdown. Ready to deploy.',
    RISKY:        'Profitable in-sample but shows fragility. Deploy with caution.',
    NOT_RELIABLE: 'Results are inconsistent. Try different settings or date ranges.',
  };

  const verdict = (d.verdict || 'RISKY').toUpperCase().replace(/ /g, '_');

  safe('verdict-icon',     e => { e.textContent = icons[verdict] || '?'; });
  safe('verdict-title',    e => { e.textContent = verdict.replace(/_/g, ' '); e.style.color = colors[verdict] || '#e2e8f0'; });
  safe('verdict-subtitle', e => { e.textContent = subs[verdict] || ''; });

  const profit  = parseFloat(d.net_profit  || 0);
  const calmar  = parseFloat(d.calmar      || 0);
  const winRate = parseFloat(d.win_rate    || 0);
  const maxDD   = parseFloat(d.max_drawdown || d.max_dd || 0);

  safe('verdict-metrics', e => {
    e.innerHTML = `
      <div class="vm"><div class="vm-val" style="color:#10b981">${fmtMoney(profit)}</div><div class="vm-lbl">Net Profit</div></div>
      <div class="vm"><div class="vm-val">${calmar.toFixed(2)}</div><div class="vm-lbl">Calmar</div></div>
      <div class="vm"><div class="vm-val">${winRate.toFixed(1)}%</div><div class="vm-lbl">Win Rate</div></div>
      <div class="vm"><div class="vm-val" style="color:#ef4444">${maxDD.toFixed(1)}%</div><div class="vm-lbl">Max DD</div></div>
    `;
  });

  safe('verdict-actions', e => {
    e.innerHTML = `
      ${d.set_file_url ? '<a href="' + d.set_file_url + '" class="verdict-btn primary" download>⬇ Download .set File</a>' : ''}
      <a href="/reports" class="verdict-btn secondary">📋 View Report</a>
      <a href="/setup"   class="verdict-btn secondary">⚡ New Run</a>
      <button class="verdict-btn ghost" onclick="document.getElementById('verdict-overlay').style.display='none'">✕ Close</button>
    `;
  });

  safe('verdict-overlay', e => { e.style.display = 'flex'; });
  stopElapsedTimer();
  setRunningState(false);
}

/* ══════════════════════════════════════════════════════════════════════
   STATUS POLLING FALLBACK
   Polls /api/status every 5 seconds when SocketIO is connected but
   pipeline state is unknown (e.g., user opens dashboard mid-run).
══════════════════════════════════════════════════════════════════════ */

let pollInterval = null;

function startPolling() {
  if (pollInterval) return;
  pollInterval = setInterval(async () => {
    try {
      const resp = await fetch('/api/status');
      const d = await resp.json();
      syncFromStatus(d);
    } catch (_) {}
  }, 5000);
}

function stopPolling() {
  if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
}

function syncFromStatus(d) {
  const isRunning = d.state === 'running';

  if (isRunning && !state.isRunning) {
    setRunningState(true, d.phase || 'Optimizing...');
    const ph = phaseMap[d.phase] || 1;
    setPhaseActive(ph);
  } else if (!isRunning && state.isRunning) {
    setRunningState(false);
  }

  if (d.run_count) {
    state.runs = d.run_count;
    safe('m-runs', e => { e.textContent = state.runs.toString(); });
  }

  // Populate metric cards from flat status fields
  if (d.best_net_profit != null || d.best_max_drawdown != null) {
    updateMetricCards({
      net_profit:    d.best_net_profit,
      max_drawdown:  d.best_max_drawdown,
      profit_factor: d.best_pf,
      total_trades:  d.best_total_trades,
    });
  }

  // Restore header score
  if (d.best_score != null) updateHeaderScore(d.best_score);

  // Restore AI panel
  if (d.latest_insight) {
    updateAIPanel(d.latest_insight);
    addLog('AI insight restored from server (run: ' + (d.latest_insight.run_id || '?') + ')', 'info');
  }

  if (d.elapsed_s) {
    // Approximate startTime from elapsed
    if (!state.startTime) state.startTime = Date.now() - d.elapsed_s * 1000;
  }
}

/* ══════════════════════════════════════════════════════════════════════
   SOCKET.IO
══════════════════════════════════════════════════════════════════════ */

const socket = io({ transports: ['websocket', 'polling'] });
if (typeof window !== 'undefined') window.socket = socket;

socket.on('connect', () => {
  addLog('Connected to APEX server.', 'success');
  updateMT5Status('connected', 'Connected');
  // Also poll once to sync state immediately
  fetch('/api/status').then(r => r.json()).then(syncFromStatus).catch(() => {});
});

socket.on('disconnect', (reason) => {
  addLog('Disconnected: ' + reason, 'warn');
  updateMT5Status('disconnected', 'Disconnected');
});

socket.on('connect_error', (err) => {
  addLog('Connection error: ' + err.message, 'error');
  updateMT5Status('disconnected', 'Error');
});

/* ── status_sync ── full state on reconnect ───────────────────────── */

socket.on('status_sync', (d) => {
  addLog('Status sync received.', 'info');
  syncFromStatus(d);
});

/* ── status_change ────────────────────────────────────────────────── */

socket.on('status_change', (d) => {
  // Backend emits d.state NOT d.status
  const running = d.state === 'running' || d.status === 'running';
  const label = d.label || d.phase || (running ? 'Optimizing...' : 'Idle');
  setRunningState(running, label);
  addLog('Status: ' + (d.state || d.status || (running ? 'running' : 'idle')), 'info');

  // Reset live-intelligence panels when a fresh run starts
  if (running && (d.state === 'running' || d.status === 'running') && d.phase === 'setup') {
    thinkingFeedState.entries = [];
    paramChangesState.iterations = [];
    validationState.runs = [];
    validationState.planned = 0;
    validationState.complete = 0;
    validationState.active = false;
    renderThinkingFeed();
    renderParamChanges();
    renderValidation();
    safe('tf-badge', e => { e.textContent = 'Live'; e.className = 'badge live'; });
    safe('pc-badge', e => { e.textContent = '—'; e.className = 'badge'; });
    safe('val-badge', e => { e.textContent = 'Pending'; e.className = 'badge'; });
    dismissEarlyTerm();
  }
});

/* ── run_complete ─────────────────────────────────────────────────── */

socket.on('run_complete', (d) => {
  updateRunsCounter();

  // Only update metric cards if we have meaningful data
  if (d.net_profit != null) updateMetricCards(d);

  // Charts: use equity curve if available, otherwise push single point
  if (d.equity_curve) {
    populateChartsFromEquityCurve(d.equity_curve);
  } else if (d.net_profit != null) {
    pushEquityPoint(d.net_profit, d.balance || d.net_profit, 'Run ' + state.runs);
    pushDDPoint(d.max_drawdown || 0, 'Run ' + state.runs);
  }

  if (d.params) updateParamsTable(d.params);
  if (d.score != null) updateHeaderScore(d.score);

  addRecentRun(d);
  updateProgress(d);

  // Update per-phase sub-progress label under phase dot
  const phase = (d.phase || '').toLowerCase();
  if (phase === 'phase1' && d.run_number && d.total) {
    updatePhaseSubProgress(1, d.run_number, d.total);
  } else if (phase.startsWith('phase2') && d.progress_pct != null) {
    // Rough count: we don't always have iteration here; UI updates via ai_iteration_* handler
  } else if (phase.startsWith('phase3')) {
    // Updated via validation_run_complete handler
  }

  const msg = 'Run ' + state.runs + ' (' + (d.phase || '?') + ') '
    + (d.net_profit != null ? '— Net Profit: ' + fmtMoney(d.net_profit) : '')
    + (d.profit_factor != null ? ', PF: ' + parseFloat(d.profit_factor).toFixed(2) : '')
    + (d.max_drawdown != null ? ', Max DD: ' + fmtPct(d.max_drawdown) : '');
  addLog(msg, d.passing === false ? 'warn' : 'success');
});

/* ── phase_start ──────────────────────────────────────────────────── */

socket.on('phase_start', (d) => {
  const phaseNum = phaseMap[d.phase] || parseInt(d.phase_num || d.phase || '1', 10);
  setPhaseActive(phaseNum, { total: d.total, mode: d.mode });
  const label = d.label
    || ({ 1: 'Exploration', 2: (d.mode === 'autonomous' ? 'AI Iteration' : 'Iteration'), 3: 'Validation' }[phaseNum])
    || ('Phase ' + phaseNum);
  addLog('Phase started: ' + label, 'info');
  safe('run-indicator-label', e => { e.textContent = label + '...'; });
  setRunningState(true, label);
});

/* ── phase1_complete ──────────────────────────────────────────────── */

socket.on('phase1_complete', (d) => {
  addLog('Phase 1 complete — ' + (d.n_passing || 0) + '/' + (d.total_tested || '?') + ' configs passed.', 'success');
  setPhaseActive(2);

  // Backend emits top_results (NOT top_configs)
  const topResults = d.top_results || d.top_configs || [];
  if (topResults.length) {
    if (topResults[0] && topResults[0].params) updateParamsTable(topResults[0].params);
    topResults.forEach(cfg => addRecentRun(cfg));
  }
});

/* ── phase2_complete ──────────────────────────────────────────────── */

socket.on('phase2_complete', (d) => {
  addLog('Phase 2 complete — best: ' + (d.best_run_id || '?') + ', profit=$' + (d.best_profit || 0), 'success');
  setPhaseActive(3);

  // Phase2 complete has flat fields: best_profit, best_calmar
  if (d.best_profit != null) {
    updateMetricCards({ net_profit: d.best_profit, calmar: d.best_calmar });
  }
});

/* ── optimization_complete ────────────────────────────────────────── */

socket.on('optimization_complete', (d) => {
  addLog('✅ Optimization complete! Verdict: ' + (d.verdict || '?'), 'success');
  for (let i = 1; i <= 3; i++) {
    const step = el('phase-step-' + i);
    if (step) { step.classList.remove('active'); step.classList.add('done'); }
  }
  safe('opt-progress-fill', e => { e.style.width = '100%'; });
  safe('hdr-prog-fill',     e => { e.style.width = '100%'; });
  safe('hdr-pct',           e => { e.textContent = '100% Complete'; });
  safe('opt-phase', e => { e.textContent = 'Complete'; });
  if (d.score != null) updateHeaderScore(d.score);
  showVerdict(d);
});

/* ── ai_insight ───────────────────────────────────────────────────── */

socket.on('ai_insight', (d) => {
  updateAIPanel(d);
  addLog('AI Insight received (confidence: ' + (d.confidence || '?') + ')', 'info');
});

/* ── log ──────────────────────────────────────────────────────────── */

socket.on('log', (d) => {
  const msg   = typeof d === 'string' ? d : (d.msg || d.message || JSON.stringify(d));
  const level = typeof d === 'object' ? (d.level || 'info') : 'info';
  addLog(msg, level);
});

/* ── no_profitable_config ─────────────────────────────────────────── */

socket.on('no_profitable_config', (d) => {
  safe('no-profit-banner', e => {
    e.classList.add('active');
    // Backend emits d.msg (not d.message)
    const text = (d && (d.msg || d.message)) || 'No profitable configuration found.';
    const msgEl = el('no-profit-msg');
    if (msgEl) msgEl.textContent = ' ' + text;
  });
  setRunningState(false);
  addLog('No profitable configuration found.', 'warn');
});

/* ── error ────────────────────────────────────────────────────────── */

socket.on('error', (d) => {
  const msg = (d && d.msg) || 'Unknown server error';
  addLog('Server error: ' + msg, 'error');
  setRunningState(false);
});

/* ── Autonomous AI Loop events ─────────────────────────────────────── */

socket.on('ai_iteration_start', (d) => {
  const iter = d.iteration || '?';
  const max  = d.max_iterations || '?';
  safe('hdr-status',     e => { e.textContent = 'AI Loop'; });
  safe('hdr-status-sub', e => { e.textContent = `Iteration ${iter} / ${max}`; });

  let msg = `🤖 AI Iteration ${iter}/${max}`;
  if (d.is_stuck_escape) {
    msg += ' [escape mode]';
  } else if (d.changes && d.changes.length) {
    const changes = d.changes.map(c => `${c.param}=${c.value}`).join(', ');
    msg += ` — trying: ${changes}`;
  }
  addLog(msg, 'info');
  if (d.analysis) addLog(`  AI reasoning: ${d.analysis}`, 'info');
});

socket.on('ai_iteration_complete', (d) => {
  const iter   = d.iteration || '?';
  const max    = d.max_iterations || 0;
  const pf     = parseFloat(d.profit_factor || 0).toFixed(2);
  const calmar = parseFloat(d.calmar || 0).toFixed(2);
  const dd     = parseFloat(d.max_drawdown || 0).toFixed(1);
  const status = d.passing ? '✅' : '❌';

  addLog(
    `  ${status} iter=${iter} | PF=${pf} | Calmar=${calmar} | DD=${dd}% | `
    + `confidence=${(d.confidence || 0).toFixed(2)}`,
    d.passing ? 'success' : 'warn'
  );

  if (typeof iter === 'number' && max) updatePhaseSubProgress(2, iter, max);
  if (d.best_score != null) updateHeaderScore(d.best_score);

  if (d.goal_status) {
    const gs = d.goal_status;
    const flags = [
      gs.profit_factor_met ? '✓ PF'     : '✗ PF',
      gs.drawdown_ok       ? '✓ DD'     : '✗ DD',
      gs.calmar_met        ? '✓ Calmar' : '✗ Calmar',
    ].join('  ');
    addLog(`  Targets: ${flags}`, 'info');
  }

  if (d.targets_met) addLog('🎯 All targets met! Stopping AI loop early.', 'success');
});

socket.on('ai_targets_met', (d) => {
  addLog(
    `🎯 Targets achieved after ${d.iteration} iterations — `
    + `PF=${parseFloat(d.profit_factor || 0).toFixed(2)}, `
    + `Calmar=${parseFloat(d.calmar || 0).toFixed(2)}, `
    + `DD=${parseFloat(d.max_drawdown || 0).toFixed(1)}%`,
    'success'
  );
});

socket.on('ai_stuck', (d) => {
  addLog(`⚠ AI loop stuck at iteration ${d.iteration} — applying random escape`, 'warn');
});

/* ══════════════════════════════════════════════════════════════════════
   LIVE INTELLIGENCE — AI Thinking Feed / Param Changes / Validation
══════════════════════════════════════════════════════════════════════ */

const MAX_THINKING_ENTRIES = 150;
const MAX_PARAM_CHANGE_ITERATIONS = 20;
const thinkingFeedState = { entries: [], active: false };
const paramChangesState = { iterations: [] };
const validationState   = { runs: [], planned: 0, complete: 0, active: false };

function _formatVal(v) {
  if (v === null || v === undefined) return '—';
  if (typeof v === 'number') {
    if (Number.isInteger(v)) return String(v);
    return Number(v).toFixed(Math.abs(v) < 10 ? 4 : 2);
  }
  return String(v);
}

function _markerFor(kind) {
  return {
    info:       '•',
    reasoning:  '✦',
    decision:   '→',
    success:    '✓',
    warning:    '⚠',
    hypothesis: '?',
  }[kind] || '•';
}

function renderThinkingFeed() {
  const box = el('thinking-feed');
  if (!box) return;
  if (!thinkingFeedState.entries.length) {
    box.innerHTML = '<div class="live-intel-empty">Waiting for the optimizer to start…<br><span style="font-size:0.68rem">The AI\'s reasoning will stream here in real time.</span></div>';
    return;
  }
  box.innerHTML = thinkingFeedState.entries.map(e => {
    const kind = e.kind || 'info';
    const iter = e.iteration ? `<span class="tf-chip">iter ${e.iteration}</span>` : '';
    const phase = e.phase ? `<span class="tf-chip">${escapeHtml(e.phase)}</span>` : '';
    const time = e.time ? `<span>${escapeHtml(e.time)}</span>` : '';
    const cursor = e.streaming ? '<span class="tf-cursor">▌</span>' : '';
    return `<div class="tf-entry ${kind}${e.streaming ? ' streaming' : ''}">
      <div class="tf-marker">${_markerFor(kind)}</div>
      <div class="tf-body">
        <div class="tf-msg">${escapeHtml(e.msg)}${cursor}</div>
        <div class="tf-meta">${time}${iter}${phase}</div>
      </div>
    </div>`;
  }).join('');
  // Auto-scroll to bottom so latest entry is visible
  box.scrollTop = box.scrollHeight;
}

function addThinking(d) {
  // For replayed events, use the original ts from the server. For live ones, use now.
  let timeLabel = nowStr();
  if (d.ts) {
    try {
      const dt = new Date(d.ts);
      if (!isNaN(dt)) {
        timeLabel = dt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
      }
    } catch (_) {}
  }
  const entry = {
    msg:       d.msg || '',
    kind:      d.kind || 'info',
    iteration: d.iteration,
    phase:     d.phase,
    time:      timeLabel,
  };
  thinkingFeedState.entries.push(entry);
  if (thinkingFeedState.entries.length > MAX_THINKING_ENTRIES) {
    thinkingFeedState.entries.shift();
  }
  thinkingFeedState.active = true;
  safe('tf-badge', e => { e.textContent = 'Live'; e.className = 'badge live'; });
  renderThinkingFeed();
}

function renderParamChanges() {
  const box = el('param-changes-list');
  if (!box) return;
  if (!paramChangesState.iterations.length) {
    box.innerHTML = '<div class="live-intel-empty">No iterations yet.<br><span style="font-size:0.68rem">AI-driven parameter edits appear per iteration.</span></div>';
    return;
  }
  // Newest first
  const items = [...paramChangesState.iterations].reverse();
  box.innerHTML = items.map(it => {
    const changes = (it.changes || []).map(c => `
      <div class="pc-change">
        <span class="pc-param">${escapeHtml(c.param || '')}</span>:
        <span class="pc-oldval">${escapeHtml(_formatVal(c.from))}</span>
        <span class="pc-arrow">→</span>
        <span class="pc-newval">${escapeHtml(_formatVal(c.to))}</span>
        ${c.reason ? `<div class="pc-reason">${escapeHtml(c.reason)}</div>` : ''}
      </div>
    `).join('');
    const conf = (typeof it.confidence === 'number')
      ? `conf ${(it.confidence * 100).toFixed(0)}%`
      : '';
    return `<div class="pc-iteration">
      <div class="pc-iter-header">
        <span class="pc-iter-label">iter ${it.iteration}</span>
        <span class="pc-iter-conf">${escapeHtml(conf)}</span>
      </div>
      ${changes || '<div class="pc-change" style="color:var(--muted);font-style:italic">(no param changes — random escape)</div>'}
    </div>`;
  }).join('');
}

function addParamChanges(d) {
  paramChangesState.iterations.push({
    iteration:  d.iteration,
    changes:    d.changes || d.change_records || [],
    analysis:   d.analysis || '',
    confidence: d.confidence,
  });
  if (paramChangesState.iterations.length > MAX_PARAM_CHANGE_ITERATIONS) {
    paramChangesState.iterations.shift();
  }
  safe('pc-badge', e => { e.textContent = `iter ${d.iteration}`; e.className = 'badge live'; });
  renderParamChanges();
}

function renderValidation() {
  const box = el('validation-panel');
  if (!box) return;
  if (!validationState.active && !validationState.runs.length) {
    box.innerHTML = '<div class="live-intel-empty">Validation runs after optimization.<br><span style="font-size:0.68rem">Out-of-sample + sensitivity tests.</span></div>';
    return;
  }
  const planned = validationState.planned || validationState.runs.length;
  const done    = validationState.complete;
  const pct     = planned > 0 ? Math.round(done / planned * 100) : 0;
  const progress = `
    <div class="val-progress-row">
      <span>${done}/${planned}</span>
      <div class="val-progress-bar"><div class="val-progress-fill" style="width:${pct}%"></div></div>
      <span>${pct}%</span>
    </div>`;
  const runsHtml = validationState.runs.map(r => {
    let clsExtra = r.status === 'running' ? 'active' : (r.passing === true ? 'pass' : (r.passing === false ? 'fail' : ''));
    const statusBadge = r.status === 'running'
      ? '<span class="val-spinner"></span>'
      : (r.passing ? '<span style="color:var(--green)">✓</span>'
                   : r.passing === false ? '<span style="color:var(--red)">✗</span>' : '');
    const metrics = (r.status === 'complete') ? `
      <div class="val-run-metrics">
        <div class="val-metric"><div class="val-metric-lbl">Profit</div><div class="val-metric-val" style="color:${r.net_profit >= 0 ? 'var(--green)' : 'var(--red)'}">${fmtMoney(r.net_profit)}</div></div>
        <div class="val-metric"><div class="val-metric-lbl">PF</div><div class="val-metric-val" style="color:var(--yellow)">${Number(r.profit_factor || 0).toFixed(2)}</div></div>
        <div class="val-metric"><div class="val-metric-lbl">Calmar</div><div class="val-metric-val" style="color:var(--teal)">${Number(r.calmar || 0).toFixed(2)}</div></div>
        <div class="val-metric"><div class="val-metric-lbl">DD</div><div class="val-metric-val" style="color:var(--red)">${Number(r.max_drawdown || 0).toFixed(1)}%</div></div>
      </div>` : '';
    return `<div class="val-run ${clsExtra}">
      <div class="val-run-title">
        ${statusBadge}
        <span>${escapeHtml(r.label || r.kind || 'Run')}</span>
        <span class="val-run-kind ${escapeHtml(r.kind || '')}">${escapeHtml(r.kind || '')}</span>
      </div>
      ${r.description ? `<div class="val-run-desc">${escapeHtml(r.description)}</div>` : ''}
      ${metrics}
    </div>`;
  }).join('');
  box.innerHTML = progress + runsHtml;
}

function validationRunStart(d) {
  validationState.active = true;
  validationState.planned = d.total || validationState.planned;
  // Replace any existing run with same run_id
  validationState.runs = validationState.runs.filter(r => r.run_id !== d.run_id);
  validationState.runs.push({
    run_id:      d.run_id,
    kind:        d.kind,
    label:       d.label,
    description: d.description,
    index:       d.index,
    status:      'running',
  });
  safe('val-badge', e => { e.textContent = `Running ${d.index}/${d.total || validationState.planned}`; e.className = 'badge live'; });
  renderValidation();
}

function validationRunComplete(d) {
  const idx = validationState.runs.findIndex(r => r.run_id === d.run_id);
  const base = idx >= 0 ? validationState.runs[idx] : { run_id: d.run_id, kind: d.kind, label: d.label };
  const merged = {
    ...base,
    status:        'complete',
    net_profit:    d.net_profit,
    profit_factor: d.profit_factor,
    calmar:        d.calmar,
    max_drawdown:  d.max_drawdown,
    total_trades:  d.total_trades,
    passing:       d.passing,
  };
  if (idx >= 0) validationState.runs[idx] = merged;
  else validationState.runs.push(merged);
  validationState.complete++;
  renderValidation();
}

function validationDone(d) {
  validationState.active = false;
  const label = ({
    RECOMMENDED:  'Passed',
    RISKY:        'Mixed',
    NOT_RELIABLE: 'Failed',
  })[d.verdict] || 'Done';
  const cls = d.verdict === 'RECOMMENDED' ? 'live' : '';
  safe('val-badge', e => { e.textContent = label; e.className = `badge ${cls}`; });
  renderValidation();
}

function showEarlyTermination(d) {
  const banner = el('early-term-banner');
  if (!banner) return;
  const reason = d.reason || 'unknown';
  const cls = {
    targets_met:       'success',
    no_profit:         'critical',
    budget_exhausted:  'warning',
    stuck_escape:      'warning',
    user_stop:         'warning',
  }[reason] || 'warning';
  banner.className = `early-term-banner ${cls} active`;

  const icon = {
    targets_met: '🎯',
    no_profit:   '⚠',
    budget_exhausted: '⏱',
    stuck_escape: '↻',
    user_stop: '⏹',
  }[reason] || '⚠';
  safe('early-term-icon', e => { e.textContent = icon; });

  const heading = {
    targets_met:      'Targets Met — Optimization Complete',
    no_profit:        'Optimization Stopped Early',
    budget_exhausted: 'Time Budget Exhausted',
    stuck_escape:     'Optimizer Stuck',
    user_stop:        'Stopped by User',
  }[reason] || 'Optimization Event';
  safe('early-term-heading', e => { e.textContent = heading; });

  safe('early-term-msg', e => { e.textContent = d.message || ''; });

  // Render details as chips
  const det = d.details || {};
  const chips = Object.entries(det)
    .filter(([k, v]) => typeof v !== 'object' && v !== null && v !== undefined && v !== '')
    .map(([k, v]) => `<span>${escapeHtml(k)}: <strong style="color:var(--text)">${escapeHtml(String(v))}</strong></span>`)
    .join('');
  safe('early-term-details', e => { e.innerHTML = chips; });
}

function dismissEarlyTerm() {
  safe('early-term-banner', e => { e.classList.remove('active'); });
}

/* ── Socket wiring for new events ─────────────────────────────────── */

socket.on('ai_thinking', (d) => {
  addThinking(d);
});

/* ── Live AI token streaming ─────────────────────────────────────
   Claude streams the reasoning text token-by-token via SSE; the
   pipeline forwards each chunk as `ai_thinking_chunk`. We render
   it as a single growing bubble that finalises on `end`. */
let _streamingEntry = null;     // index into thinkingFeedState.entries while open
socket.on('ai_thinking_chunk', (d) => {
  const evt = d.event;
  if (evt === 'start') {
    // Open a new entry. Render-loop will paint as deltas arrive.
    thinkingFeedState.entries.push({
      msg:       '',
      kind:      'reasoning',
      streaming: true,
      time:      nowStr(),
      phase:     d.phase,
    });
    if (thinkingFeedState.entries.length > MAX_THINKING_ENTRIES) {
      thinkingFeedState.entries.shift();
    }
    _streamingEntry = thinkingFeedState.entries.length - 1;
    safe('tf-badge', e => { e.textContent = 'Streaming'; e.className = 'badge live'; });
    renderThinkingFeed();
  } else if (evt === 'delta' && _streamingEntry != null) {
    const e = thinkingFeedState.entries[_streamingEntry];
    if (e) {
      e.msg += d.text || '';
      renderThinkingFeed();
    }
  } else if (evt === 'end' && _streamingEntry != null) {
    const e = thinkingFeedState.entries[_streamingEntry];
    if (e) {
      e.streaming = false;
      renderThinkingFeed();
    }
    _streamingEntry = null;
    safe('tf-badge', e => { e.textContent = 'Live'; e.className = 'badge live'; });
  } else if (evt === 'error') {
    if (_streamingEntry != null) {
      const e = thinkingFeedState.entries[_streamingEntry];
      if (e) { e.kind = 'warning'; e.streaming = false; e.msg += ' [stream error: ' + (d.error || '?') + ']'; }
    }
    _streamingEntry = null;
    renderThinkingFeed();
  }
});

socket.on('param_changes', (d) => {
  addParamChanges(d);
});

socket.on('validation_start', (d) => {
  // Reset the panel for a fresh validation phase
  validationState.runs = [];
  validationState.planned = d.planned_runs || 0;
  validationState.complete = 0;
  validationState.active = true;
  safe('val-badge', e => { e.textContent = `0/${d.planned_runs}`; e.className = 'badge live'; });
  renderValidation();
  addLog(`Validation: planning ${d.planned_runs} test runs`, 'info');
});

socket.on('validation_run_start', (d) => {
  validationRunStart(d);
});

socket.on('validation_run_complete', (d) => {
  validationRunComplete(d);
});

socket.on('validation_done', (d) => {
  validationDone(d);
});

socket.on('early_termination', (d) => {
  showEarlyTermination(d);
  addLog(`⚠ ${d.message}`, d.reason === 'targets_met' ? 'success' : 'warn');
});

/* ── Also capture change_records from ai_iteration_start as a fallback ── */
socket.on('ai_iteration_start', (d) => {
  // (existing handler runs first — this just ensures param-changes panel is populated
  //  even if the dedicated param_changes event is missed for any reason)
  if (d.change_records && d.change_records.length) {
    const already = paramChangesState.iterations.find(i => i.iteration === d.iteration);
    if (!already) {
      addParamChanges({
        iteration:  d.iteration,
        changes:    d.change_records,
        analysis:   d.analysis,
        confidence: d.confidence,
      });
    }
  }
});

/* ══════════════════════════════════════════════════════════════════════
   STOP BUTTON — uses HTTP POST, not socket emit
══════════════════════════════════════════════════════════════════════ */

safe('btn-stop', stopBtn => {
  stopBtn.addEventListener('click', async () => {
    addLog('Stopping optimization...', 'warn');
    stopBtn.classList.remove('active');
    try {
      await fetch('/api/stop', { method: 'POST' });
      setRunningState(false);
      addLog('Stop signal sent.', 'warn');
    } catch (e) {
      addLog('Stop request failed: ' + e.message, 'error');
    }
  });
});

/* Pause button removed — see dashboard.html. Pipeline only supports Stop. */

/* ══════════════════════════════════════════════════════════════════════
   HISTORY RESTORATION
   Loads past runs from /api/history and /api/ai_insight/latest so
   reopening the dashboard shows progressive data, not a blank slate.
══════════════════════════════════════════════════════════════════════ */

async function restoreHistory() {
  try {
    const [histResp, aiResp, liveResp] = await Promise.all([
      fetch('/api/history'),
      fetch('/api/ai_insight/latest'),
      fetch('/api/live_activity'),
    ]);

    const runs = await histResp.json();
    if (Array.isArray(runs) && runs.length) {
      // Populate recent runs table (last 8, newest first)
      const sorted = runs.slice().reverse();
      state.runs = runs.length;
      safe('m-runs', e => { e.textContent = state.runs; });

      sorted.slice(0, 8).forEach(r => addRecentRun(r));

      // Push equity sparkline from net profits
      runs.forEach(r => {
        if (r.net_profit != null) {
          pushEquityPoint(r.net_profit, r.net_profit, r.run_id || '');
          pushDDPoint(r.max_drawdown || 0, r.run_id || '');
          pushSparkData('profit', r.net_profit);
          pushSparkData('dd', r.max_drawdown || 0);
          pushSparkData('pf', r.profit_factor || 0);
          pushSparkData('trades', r.total_trades || 0);
          pushSparkData('runs', state.runs);
        }
      });

      // Show best metrics
      const passing = runs.filter(r => r.passing);
      if (passing.length) {
        const best = passing.reduce((a, b) => (a.net_profit > b.net_profit ? a : b));
        updateMetricCards(best);
        addLog('Restored ' + runs.length + ' past runs from history.', 'info');
      }
    }

    // Restore AI panel if insight exists
    const insight = await aiResp.json();
    if (insight) {
      updateAIPanel(insight);
      addLog('AI insight restored (run: ' + (insight.run_id || '?') + ').', 'info');
    }

    // ── Restore Live Intelligence (thinking feed, param changes, validation, banner) ──
    // This is what makes a refresh-mid-run feel like nothing happened.
    const live = await liveResp.json();
    if (live) {
      // Thinking feed
      const thinking = Array.isArray(live.thinking) ? live.thinking : [];
      thinking.forEach(t => addThinking(t));

      // Param changes
      const pcs = Array.isArray(live.param_changes) ? live.param_changes : [];
      pcs.forEach(pc => addParamChanges(pc));

      // Validation panel
      const val = Array.isArray(live.validation) ? live.validation : [];
      val.forEach(v => {
        if (v.event === 'validation_start') {
          validationState.runs = [];
          validationState.planned = v.planned_runs || 0;
          validationState.complete = 0;
          validationState.active = true;
        } else if (v.event === 'validation_run_start') {
          validationRunStart(v);
        } else if (v.event === 'validation_run_complete') {
          validationRunComplete(v);
        } else if (v.event === 'validation_done') {
          validationDone(v);
        }
      });
      if (val.length) renderValidation();

      // Early-termination banner
      if (live.early_termination) {
        showEarlyTermination(live.early_termination);
      }

      // Phase tracker — restore active phase even if no events have arrived yet
      const phaseMapLocal = { phase1: 1, phase2: 2, phase2_ai: 2, phase3: 3, phase3_oos: 3, phase3_sens: 3 };
      const ph = phaseMapLocal[live.phase] || (live.running ? 1 : 0);
      if (ph) {
        setPhaseActive(ph, { mode: live.phase_mode });
      }
      if (live.running) {
        setRunningState(true, live.phase || 'Optimizing...');
      }

      if (thinking.length || pcs.length || val.length) {
        addLog(
          `Restored live activity: ${thinking.length} thinking, `
          + `${pcs.length} param-change iterations, ${val.length} validation events.`,
          'info'
        );
      }
    }
  } catch (e) {
    addLog('History restore: ' + e.message, 'warn');
  }
}

/* ══════════════════════════════════════════════════════════════════════
   SETTINGS MODAL
══════════════════════════════════════════════════════════════════════ */

function openSettings() {
  safe('settings-overlay', e => { e.style.display = 'flex'; });
  loadSettings();
}

function closeSettings() {
  safe('settings-overlay', e => { e.style.display = 'none'; });
}

function switchSettingsTab(tab) {
  document.querySelectorAll('.settings-tab').forEach(t => {
    t.classList.toggle('active', t.dataset.tab === tab);
  });
  document.querySelectorAll('.settings-panel').forEach(p => {
    p.classList.toggle('active', p.id === 'stab-' + tab);
  });
}

function toggleSettingsBool(btn) {
  btn.classList.toggle('on');
}

function setBoolToggle(id, value) {
  const btn = el(id);
  if (!btn) return;
  btn.classList.toggle('on', !!value);
}

function getBoolToggle(id) {
  const btn = el(id);
  return btn ? btn.classList.contains('on') : false;
}

function setVal(id, value) {
  const e = el(id);
  if (e) e.value = value != null ? value : '';
}

function getVal(id) {
  const e = el(id);
  return e ? e.value : '';
}

async function loadSettings() {
  try {
    const resp = await fetch('/api/settings');
    const cfg = await resp.json();

    // AI tab
    setBoolToggle('s-ai-enabled', cfg.ai && cfg.ai.enabled !== false);
    setVal('s-ai-key', (cfg.ai && cfg.ai.anthropic_api_key) || '');
    setVal('s-ai-model', (cfg.ai && cfg.ai.model) || 'claude-opus-4-7');
    setVal('s-ai-timeout', (cfg.ai && cfg.ai.timeout_seconds) || 30);

    // MT5 tab
    setVal('s-mt5-exe',     (cfg.mt5 && cfg.mt5.terminal_exe) || '');
    setVal('s-mt5-appdata', (cfg.mt5 && cfg.mt5.appdata_path) || '');
    setVal('s-mt5-mql5',   (cfg.mt5 && cfg.mt5.mql5_files_path) || '');
    setVal('s-mt5-timeout', (cfg.mt5 && cfg.mt5.tester_timeout_seconds) || 120);
    setVal('s-mt5-model',   (cfg.mt5 && cfg.mt5.tester_model) || 1);

    // Broker tab
    setVal('s-broker-tz',      (cfg.broker && cfg.broker.timezone_offset_hours) != null ? cfg.broker.timezone_offset_hours : 3);
    setVal('s-broker-deposit',  (cfg.broker && cfg.broker.deposit) || 10000);
    setVal('s-broker-leverage', (cfg.broker && cfg.broker.leverage) || 500);

    // Thresholds tab
    setVal('s-thr-trades',  (cfg.thresholds && cfg.thresholds.min_trades) || 30);
    setVal('s-thr-pf',      (cfg.thresholds && cfg.thresholds.min_profit_factor) || 1.2);
    setVal('s-thr-calmar',  (cfg.thresholds && cfg.thresholds.min_calmar) || 0.5);
    setVal('s-thr-oos',     (cfg.thresholds && cfg.thresholds.max_oos_degradation) || 0.3);
    setVal('s-thr-sens',    (cfg.thresholds && cfg.thresholds.sensitivity_tolerance) || 0.15);
  } catch (e) {
    showSettingsMsg('Failed to load settings: ' + e.message, false);
  }
}

async function saveSettings() {
  const btn = el('settings-save-btn');
  if (btn) { btn.disabled = true; btn.textContent = 'Saving…'; }

  const payload = {
    ai: {
      enabled:           getBoolToggle('s-ai-enabled'),
      anthropic_api_key: getVal('s-ai-key').trim(),
      model:             getVal('s-ai-model'),
      timeout_seconds:   parseFloat(getVal('s-ai-timeout')) || 30,
    },
    mt5: {
      terminal_exe:           getVal('s-mt5-exe').trim(),
      appdata_path:           getVal('s-mt5-appdata').trim(),
      mql5_files_path:        getVal('s-mt5-mql5').trim(),
      tester_timeout_seconds: parseInt(getVal('s-mt5-timeout'), 10) || 120,
      tester_model:           parseInt(getVal('s-mt5-model'), 10) || 1,
    },
    broker: {
      timezone_offset_hours: parseFloat(getVal('s-broker-tz')) || 3,
      deposit:               parseFloat(getVal('s-broker-deposit')) || 10000,
      leverage:              parseInt(getVal('s-broker-leverage'), 10) || 500,
    },
    thresholds: {
      min_trades:            parseInt(getVal('s-thr-trades'), 10) || 30,
      min_profit_factor:     parseFloat(getVal('s-thr-pf')) || 1.2,
      min_calmar:            parseFloat(getVal('s-thr-calmar')) || 0.5,
      max_oos_degradation:   parseFloat(getVal('s-thr-oos')) || 0.3,
      sensitivity_tolerance: parseFloat(getVal('s-thr-sens')) || 0.15,
    },
  };

  try {
    const resp = await fetch('/api/settings', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });
    const data = await resp.json();
    if (data.ok) {
      showSettingsMsg('Settings saved.', true);
      addLog('Settings saved successfully.', 'success');
      setTimeout(closeSettings, 1200);
    } else {
      showSettingsMsg('Error: ' + (data.error || 'Unknown error'), false);
    }
  } catch (e) {
    showSettingsMsg('Save failed: ' + e.message, false);
  } finally {
    if (btn) { btn.disabled = false; btn.textContent = 'Save Settings'; }
  }
}

function showSettingsMsg(msg, ok) {
  safe('settings-save-msg', e => {
    e.textContent = msg;
    e.className = 'settings-save-msg ' + (ok ? 'ok' : 'err');
    e.style.opacity = '1';
    setTimeout(() => { e.style.opacity = '0'; }, 3000);
  });
}

/* ══════════════════════════════════════════════════════════════════════
   AI PANEL TOGGLE
══════════════════════════════════════════════════════════════════════ */

function toggleAIDetails() {
  const details = el('ai-details');
  if (!details) return;
  const isOpen = details.classList.contains('open');
  details.classList.toggle('open', !isOpen);
  const label = isOpen ? 'View Details →' : '▲ Collapse';
  safe('ai-expand-btn',  e => { e.textContent = label; });
  safe('ai-view-details', e => { e.textContent = label; });
}

/* ══════════════════════════════════════════════════════════════════════
   RUN DETAIL MODAL
══════════════════════════════════════════════════════════════════════ */

async function openRunDetail(run_id) {
  if (!run_id) return;

  // Show modal immediately with loading state
  safe('run-detail-overlay', e => { e.style.display = 'flex'; });
  safe('rd-run-id',       e => { e.textContent = run_id; });
  safe('rd-meta',         e => { e.textContent = 'Loading…'; });
  safe('rd-metrics',      e => { e.innerHTML = ''; });
  safe('rd-params-tbody', e => { e.innerHTML = '<tr><td colspan="2" class="empty-state">Loading…</td></tr>'; });
  safe('rd-ai-section',   e => { e.style.display = 'none'; });
  safe('rd-footer',       e => { e.innerHTML = '<button class="verdict-btn ghost" onclick="closeRunDetail()">Close</button>'; });
  // Clear any lingering evolution section from a previous openBestResult call
  const existingEvo = document.getElementById('rd-evolution-section');
  if (existingEvo) existingEvo.remove();

  try {
    const resp = await fetch('/api/run/' + encodeURIComponent(run_id));
    if (!resp.ok) throw new Error('HTTP ' + resp.status);
    const d = await resp.json();

    // Header
    safe('rd-run-id', e => { e.textContent = d.run_id || run_id; });
    const phase = (d.phase || '—').replace(/_/g, ' ');
    const ts    = d.ts ? new Date(d.ts).toLocaleString() : '—';
    safe('rd-meta', e => { e.textContent = 'Phase: ' + phase + '  ·  ' + ts; });

    // Metrics grid
    const profit = parseFloat(d.net_profit     || 0);
    const pf     = parseFloat(d.profit_factor  || 0);
    const calmar = parseFloat(d.calmar         || 0);
    const dd     = parseFloat(d.drawdown_pct   || d.max_drawdown || 0);
    const wr     = parseFloat(d.win_rate       || 0);
    const trades = parseInt(d.total_trades     || 0, 10);
    const score  = parseFloat(d.score          || 0);

    const metrics = [
      { label: 'Net Profit',    val: fmtMoney(profit),          color: profit >= 0 ? 'var(--green)' : 'var(--red)' },
      { label: 'Profit Factor', val: pf.toFixed(2),             color: pf >= 1.5 ? 'var(--green)' : pf >= 1 ? 'var(--yellow)' : 'var(--red)' },
      { label: 'Calmar Ratio',  val: calmar.toFixed(2),         color: 'var(--teal)' },
      { label: 'Max Drawdown',  val: dd.toFixed(1) + '%',       color: 'var(--red)' },
      { label: 'Win Rate',      val: wr.toFixed(1) + '%',       color: 'var(--text)' },
      { label: 'Total Trades',  val: trades.toLocaleString(),   color: 'var(--text)' },
      { label: 'Score',         val: score.toFixed(4),          color: 'var(--accent2)' },
      { label: 'Phase',         val: phase,                     color: 'var(--muted)' },
    ];
    safe('rd-metrics', e => {
      e.innerHTML = metrics.map(m =>
        `<div class="rd-metric">
          <div class="rd-metric-label">${escapeHtml(m.label)}</div>
          <div class="rd-metric-val" style="color:${m.color}">${escapeHtml(String(m.val))}</div>
        </div>`
      ).join('');
    });

    // Parameters
    const params = d.params || {};
    const paramEntries = Object.entries(params);
    safe('rd-params-tbody', e => {
      if (!paramEntries.length) {
        e.innerHTML = '<tr><td colspan="2" class="empty-state">No parameters available</td></tr>';
        return;
      }
      e.innerHTML = paramEntries.map(([key, val]) => {
        const display = typeof val === 'number' ? val.toFixed(val % 1 !== 0 ? 4 : 0) : val;
        return `<tr><td class="param-name">${escapeHtml(key)}</td><td class="param-val">${escapeHtml(String(display))}</td></tr>`;
      }).join('');
    });

    // AI Reasoning
    const ai = d.ai_insight;
    if (ai) {
      safe('rd-ai-section', e => { e.style.display = ''; });
      safe('rd-ai-content', e => {
        let html = '<div class="rd-ai-box">';
        const analysisText = ai.analysis || ai.diagnosis || ai.headline || '';
        if (analysisText) {
          html += '<div class="rd-ai-analysis">' + escapeHtml(analysisText) + '</div>';
        }
        const changes = ai.changes || ai.suggestions || [];
        if (changes.length) {
          html += '<div class="ai-sub-title" style="margin-bottom:0.4rem;">Suggested Changes</div>';
          html += '<div class="rd-ai-changes">';
          changes.forEach(c => {
            if (typeof c === 'object' && (c.param || c.parameter)) {
              const p = c.param || c.parameter;
              const v = c.value !== undefined ? c.value : (c.to !== undefined ? c.to : '?');
              html += `<div class="rd-ai-change">
                <span class="rd-ai-change-param">${escapeHtml(p)}</span>
                <span class="rd-ai-change-arrow">→</span>
                <span class="rd-ai-change-val">${escapeHtml(String(v))}</span>
              </div>`;
            } else if (typeof c === 'string') {
              html += `<div class="rd-ai-change"><span style="color:var(--text)">${escapeHtml(c)}</span></div>`;
            }
          });
          html += '</div>';
        }
        const conf = ai.confidence;
        if (conf !== undefined) {
          const confStr = typeof conf === 'number' ? (conf * 100).toFixed(0) + '%' : String(conf);
          html += `<div style="margin-top:0.65rem;font-size:0.7rem;color:var(--muted)">Confidence: <strong style="color:var(--text)">${escapeHtml(confStr)}</strong></div>`;
        }
        const gs = ai.goal_status;
        if (gs) {
          const flags = [
            gs.profit_factor_met ? '✓ PF' : '✗ PF',
            gs.drawdown_ok       ? '✓ DD' : '✗ DD',
            gs.calmar_met        ? '✓ Calmar' : '✗ Calmar',
          ].join('  ·  ');
          html += `<div style="margin-top:0.4rem;font-size:0.7rem;color:var(--muted)">Targets: ${escapeHtml(flags)}</div>`;
        }
        html += '</div>';
        e.innerHTML = html;
      });
    }

    // Footer
    safe('rd-footer', e => {
      let btns = '';
      if (d.set_url) {
        btns += `<a href="${escapeHtml(d.set_url)}" class="verdict-btn primary" download>⬇ Download .set</a>`;
      }
      const reportHref = '/reports/' + encodeURIComponent(d.run_id || run_id) + '/summary.html';
      btns += `<a href="${reportHref}" class="verdict-btn secondary" target="_blank">📋 View Report</a>`;
      btns += '<button class="verdict-btn ghost" onclick="closeRunDetail()">Close</button>';
      e.innerHTML = btns;
    });

  } catch (err) {
    safe('rd-meta', e => { e.textContent = 'Error: ' + err.message; });
  }
}

function closeRunDetail() {
  safe('run-detail-overlay', e => { e.style.display = 'none'; });
}

/* ══════════════════════════════════════════════════════════════════════
   BEST RESULT — shows full params, AI reasoning, AND evolution path
══════════════════════════════════════════════════════════════════════ */

async function openBestResult() {
  try {
    const resp = await fetch('/api/best_result');
    if (!resp.ok) {
      // No best yet — open a toast-like state in the run detail modal
      safe('run-detail-overlay', e => { e.style.display = 'flex'; });
      safe('rd-run-id',       e => { e.textContent = 'Best Result'; });
      safe('rd-meta',         e => { e.textContent = 'No best result yet — optimization must complete at least one passing run.'; });
      safe('rd-metrics',      e => { e.innerHTML = ''; });
      safe('rd-params-tbody', e => { e.innerHTML = '<tr><td colspan="2" class="empty-state">—</td></tr>'; });
      safe('rd-ai-section',   e => { e.style.display = 'none'; });
      safe('rd-footer',       e => { e.innerHTML = '<button class="verdict-btn ghost" onclick="closeRunDetail()">Close</button>'; });
      return;
    }
    const d = await resp.json();
    // Delegate to the existing openRunDetail, then append evolution path
    await openRunDetail(d.run_id);
    // Append evolution path as an extra section inside the modal body
    const body = document.querySelector('.run-detail-body');
    if (!body) return;
    let evoSection = document.getElementById('rd-evolution-section');
    if (!evoSection) {
      evoSection = document.createElement('div');
      evoSection.id = 'rd-evolution-section';
      body.appendChild(evoSection);
    }
    const evo = d.evolution || [];
    if (!evo.length) {
      evoSection.innerHTML = '<div class="rd-section-title">Evolution Path</div><div style="font-size:0.76rem;color:var(--muted);padding:0.5rem 0;">No evolution data available for this run.</div>';
      return;
    }
    // ── Replay scrubber: slider that walks through evolution one step at a time ──
    const bestIdx = Math.max(0, evo.findIndex(r => r.is_best));
    evoSection.innerHTML = `
      <div class="rd-section-title">
        Evolution Path
        <span style="font-weight:500;color:var(--muted);letter-spacing:0.02em;text-transform:none;font-size:0.65rem">— ${evo.length} steps to this best result · drag the slider to replay</span>
      </div>

      <div id="evo-scrubber-wrap" style="background:rgba(255,255,255,0.02);border:1px solid var(--border);border-radius:10px;padding:0.85rem 1rem;margin-bottom:0.5rem;">

        <!-- Step indicator + slider -->
        <div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:0.6rem;">
          <button id="evo-prev-btn" style="background:rgba(255,255,255,0.05);border:1px solid var(--border);color:var(--text);font-family:inherit;font-size:0.85rem;width:28px;height:28px;border-radius:6px;cursor:pointer;flex-shrink:0;">‹</button>
          <input type="range" id="evo-slider" min="0" max="${evo.length - 1}" value="${bestIdx}"
                 style="flex:1;accent-color:var(--accent2);cursor:pointer;">
          <button id="evo-next-btn" style="background:rgba(255,255,255,0.05);border:1px solid var(--border);color:var(--text);font-family:inherit;font-size:0.85rem;width:28px;height:28px;border-radius:6px;cursor:pointer;flex-shrink:0;">›</button>
          <button id="evo-play-btn" style="background:rgba(79,70,229,0.15);border:1px solid rgba(79,70,229,0.4);color:var(--accent2);font-family:inherit;font-size:0.7rem;padding:4px 10px;border-radius:6px;cursor:pointer;flex-shrink:0;font-weight:600;">▶ Play</button>
        </div>

        <!-- Step header -->
        <div style="display:flex;align-items:center;gap:0.5rem;font-size:0.72rem;flex-wrap:wrap;margin-bottom:0.5rem;">
          <span id="evo-step-num" style="font-family:'JetBrains Mono',monospace;color:var(--muted);font-weight:600;"></span>
          <span id="evo-step-id" style="font-family:'JetBrains Mono',monospace;color:var(--accent2);"></span>
          <span id="evo-step-phase" style="color:var(--muted);font-size:0.6rem;text-transform:uppercase;letter-spacing:0.06em;"></span>
          <span id="evo-step-best"></span>
          <span style="margin-left:auto;font-size:0.65rem;font-family:'JetBrains Mono',monospace;color:var(--muted);">score <span id="evo-step-score" style="color:var(--text);font-weight:600;"></span></span>
        </div>

        <!-- Metrics for this step -->
        <div id="evo-step-metrics" style="display:grid;grid-template-columns:repeat(4,1fr);gap:0.4rem;margin-bottom:0.55rem;"></div>

        <!-- Changes -->
        <div id="evo-step-changes" style="font-size:0.7rem;font-family:'JetBrains Mono',monospace;color:var(--teal);line-height:1.5;"></div>

        <!-- AI analysis -->
        <div id="evo-step-analysis" style="font-size:0.72rem;color:#94a3b8;margin-top:0.45rem;line-height:1.55;"></div>
      </div>
    `;

    // Wire scrubber
    const slider  = document.getElementById('evo-slider');
    const stepNum = document.getElementById('evo-step-num');
    const stepId  = document.getElementById('evo-step-id');
    const stepPh  = document.getElementById('evo-step-phase');
    const stepBst = document.getElementById('evo-step-best');
    const stepScr = document.getElementById('evo-step-score');
    const stepMtr = document.getElementById('evo-step-metrics');
    const stepChg = document.getElementById('evo-step-changes');
    const stepAna = document.getElementById('evo-step-analysis');
    const prevBtn = document.getElementById('evo-prev-btn');
    const nextBtn = document.getElementById('evo-next-btn');
    const playBtn = document.getElementById('evo-play-btn');

    function paintStep(i) {
      const r = evo[i];
      if (!r) return;
      stepNum.textContent = `Step ${i + 1} / ${evo.length}`;
      stepId.textContent  = r.run_id || '';
      stepPh.textContent  = (r.phase || '').replace(/_/g, ' ');
      stepBst.innerHTML   = r.is_best ? ' <span style="background:rgba(16,185,129,0.18);color:var(--green);padding:0.06rem 0.4rem;border-radius:4px;font-size:0.55rem;font-weight:700;letter-spacing:0.04em;">BEST</span>' : '';
      stepScr.textContent = Number(r.score || 0).toFixed(3);

      const cells = [
        { l: 'PF',     v: Number(r.profit_factor || 0).toFixed(2),     c: 'var(--yellow)' },
        { l: 'Calmar', v: Number(r.calmar || 0).toFixed(2),            c: 'var(--teal)' },
        { l: 'DD',     v: Number(r.max_drawdown || 0).toFixed(1) + '%', c: 'var(--red)' },
        { l: 'Profit', v: fmtMoney(r.net_profit || 0),                  c: r.net_profit >= 0 ? 'var(--green)' : 'var(--red)' },
      ];
      stepMtr.innerHTML = cells.map(c => `<div style="background:rgba(255,255,255,0.03);padding:5px 8px;border-radius:5px;"><div style="font-size:0.55rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em;">${c.l}</div><div style="font-size:0.85rem;font-weight:700;font-family:'JetBrains Mono',monospace;color:${c.c};">${escapeHtml(c.v)}</div></div>`).join('');

      const changes = r.changes || [];
      if (changes.length) {
        stepChg.innerHTML = '<strong style="color:var(--muted);font-weight:500;text-transform:uppercase;letter-spacing:0.06em;font-size:0.6rem;">Δ</strong> ' + changes.slice(0, 4).map(c => {
          const p = c.param || c.parameter || '?';
          const v = c.value !== undefined ? c.value : (c.to !== undefined ? c.to : '?');
          return `${escapeHtml(p)}=${escapeHtml(String(v))}`;
        }).join(', ');
      } else {
        stepChg.innerHTML = '<em style="color:var(--muted);">— no parameter changes (LHS seed)</em>';
      }
      stepAna.textContent = r.analysis || '';
      slider.value = i;
    }

    slider.addEventListener('input', () => paintStep(parseInt(slider.value, 10)));
    prevBtn.addEventListener('click', () => paintStep(Math.max(0, parseInt(slider.value, 10) - 1)));
    nextBtn.addEventListener('click', () => paintStep(Math.min(evo.length - 1, parseInt(slider.value, 10) + 1)));

    // Play: auto-step ~700ms intervals
    let playTimer = null;
    playBtn.addEventListener('click', () => {
      if (playTimer) {
        clearInterval(playTimer); playTimer = null;
        playBtn.textContent = '▶ Play';
        return;
      }
      playBtn.textContent = '⏸ Pause';
      let pos = parseInt(slider.value, 10);
      if (pos >= evo.length - 1) pos = 0;
      playTimer = setInterval(() => {
        pos++;
        if (pos >= evo.length) {
          clearInterval(playTimer); playTimer = null;
          playBtn.textContent = '▶ Play';
          return;
        }
        paintStep(pos);
      }, 700);
    });

    paintStep(bestIdx);  // start showing the best step
  } catch (err) {
    addLog('Failed to open best result: ' + err.message, 'error');
  }
}

/* ══════════════════════════════════════════════════════════════════════
   INIT
══════════════════════════════════════════════════════════════════════ */

(function init() {
  initSparklines();
  restoreHistory();
  startPolling();
  addLog('APEX Dashboard initialized.', 'success');
})();
