// ============================================================
// Helpers — formatters, data shapes, leg config
// ============================================================

const { useState, useEffect, useMemo, useRef } = React;

// ---------- formatters ----------

function fmt(n, decimals = 2) {
  if (n == null || isNaN(n)) return '—';
  return Number(n).toFixed(decimals);
}

function fmtBTC(n, decimals = 6) {
  if (n == null || isNaN(n)) return '—';
  return Number(n).toFixed(decimals);
}

function fmtSats(btc) {
  if (btc == null || isNaN(btc)) return '—';
  return Math.round(btc * 1e8).toLocaleString();
}

function fmtUSD(n, decimals = 0) {
  if (n == null || isNaN(n)) return '—';
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD',
    maximumFractionDigits: decimals,
    minimumFractionDigits: decimals,
  }).format(n);
}

function fmtBps(n) {
  if (n == null || isNaN(n)) return '—';
  return (Number(n) * 10000).toFixed(2) + ' bps';
}

function fmtPctSigned(n, decimals = 2) {
  if (n == null || isNaN(n)) return '—';
  const v = Number(n);
  return (v >= 0 ? '+' : '') + v.toFixed(decimals) + '%';
}

function signClass(n) {
  if (n == null || isNaN(n)) return 'c-flat';
  if (n > 0) return 'c-pos';
  if (n < 0) return 'c-neg';
  return 'c-flat';
}

function deltaClass(n) {
  if (n == null || isNaN(n)) return 'flat';
  if (n > 0) return 'pos';
  if (n < 0) return 'neg';
  return 'flat';
}

function relTime(iso) {
  if (!iso) return '—';
  const d = new Date(iso);
  if (isNaN(d)) return '—';
  const diffMin = (Date.now() - d.getTime()) / 60000;
  if (Math.abs(diffMin) < 1) return 'just now';
  if (diffMin < 60) return `${Math.round(diffMin)}m ago`;
  if (diffMin < 60 * 24) return `${Math.round(diffMin / 60)}h ago`;
  const days = Math.round(diffMin / 60 / 24);
  return `${days}d ago`;
}

function formatTimeUTC(iso) {
  if (!iso) return '—';
  const d = new Date(iso);
  if (isNaN(d)) return '—';
  const hh = String(d.getUTCHours()).padStart(2, '0');
  const mm = String(d.getUTCMinutes()).padStart(2, '0');
  return `${hh}:${mm} UTC`;
}

function formatDay(iso) {
  if (!iso) return '—';
  const d = new Date(iso);
  if (isNaN(d)) return '—';
  return d.toISOString().slice(0, 10);
}

// truncate text with ellipsis
function truncate(s, n = 140) {
  if (!s) return '';
  if (s.length <= n) return s;
  return s.slice(0, n).replace(/\s+\S*$/, '') + '…';
}

// ---------- strategy leg config ----------
// Maps each strategy id to its leg breakdown: how to read current value from
// research.json + how to evaluate the trigger gate.

const LEG_CONFIG = {
  strat_001: {
    legs: [
      {
        key: 'funding',
        name: 'Funding',
        triggerLabel: '< -0.5 bps × 3',
        read: (r) => {
          const v = r?.perp_signals?.funding_rate_latest_realized;
          return {
            value: v,
            display: fmtBps(v),
            met: false, // requires 3 consecutive periods negative — proxy
            near: v != null && v < 0.00001,
            progressPct: v != null ? Math.max(0, 100 - Math.min(100, Math.abs(v * 10000 + 5) / 0.6 * 100)) : 0,
          };
        },
      },
      {
        key: 'lsratio',
        name: 'L/S ratio',
        triggerLabel: '< 0.45',
        read: (r) => {
          const v = r?.perp_signals?.long_short_ratio;
          const met = v != null && v < 0.45;
          return {
            value: v,
            display: v != null ? v.toFixed(3) : '—',
            met,
            near: v != null && v < 0.50,
            progressPct: v != null ? Math.max(0, 100 - ((v - 0.45) / 0.15) * 100) : 0,
          };
        },
      },
      {
        key: 'feargreed',
        name: 'F&G index',
        triggerLabel: '< 20',
        read: (r) => {
          const v = r?.market_snapshot?.fear_greed_index;
          const met = v != null && v < 20;
          return {
            value: v,
            display: v != null ? String(v) : '—',
            met,
            near: v != null && v < 30,
            progressPct: v != null ? Math.max(0, 100 - Math.max(0, (v - 20)) / 30 * 100) : 0,
          };
        },
      },
      {
        key: 'oi',
        name: 'OI 4h drop',
        triggerLabel: '> 3%',
        read: (r) => {
          const v = r?.perp_signals?.open_interest_4h_change_pct;
          const met = v != null && v < -3;
          return {
            value: v,
            display: v != null ? `${v.toFixed(2)}%` : '—',
            met,
            near: v != null && v < -1.5,
            progressPct: v != null ? Math.max(0, Math.min(100, (-v / 3) * 100)) : 0,
          };
        },
      },
    ],
  },
  strat_002: {
    legs: [
      {
        key: 'atmiv',
        name: 'ATM IV',
        triggerLabel: '> 70%',
        read: (r) => {
          const v = r?.options_signals?.atm_iv_pct_comparable_weekly_22MAY26 ?? r?.options_signals?.atm_iv_pct_short_dated_weekly;
          const met = v != null && v > 70;
          return {
            value: v,
            display: v != null ? `${v.toFixed(1)}%` : '—',
            met,
            near: v != null && v > 50,
            progressPct: v != null ? Math.min(100, (v / 70) * 100) : 0,
          };
        },
      },
      {
        key: 'skew',
        name: 'Put skew',
        triggerLabel: '> 15 IV pts',
        read: (r) => {
          const v = r?.options_signals?.skew_iv_diff_pct_comparable_22MAY26;
          const met = v != null && v > 15;
          return {
            value: v,
            display: v != null ? `${v.toFixed(2)}` : '—',
            met,
            near: v != null && v > 12,
            progressPct: v != null ? Math.min(100, (v / 15) * 100) : 0,
          };
        },
      },
      {
        key: 'nearlow',
        name: 'Spot vs low',
        triggerLabel: '< 3% off low',
        read: (r) => {
          const v = r?.market_snapshot?.pct_off_30d_low;
          const met = v != null && v < 3;
          return {
            value: v,
            display: v != null ? `+${v.toFixed(2)}%` : '—',
            met,
            near: v != null && v < 5,
            progressPct: v != null ? Math.max(0, 100 - (v / 3) * 50) : 0,
          };
        },
      },
    ],
  },
  strat_003: {
    legs: [
      {
        key: 'nearhigh',
        name: 'Near 30d high',
        triggerLabel: '< 3% off high',
        read: (r) => {
          const v = r?.market_snapshot?.pct_off_30d_high;
          const met = v != null && v > -3;
          return {
            value: v,
            display: v != null ? `${v.toFixed(2)}%` : '—',
            met,
            near: v != null && v > -5,
            progressPct: v != null ? Math.max(0, 100 + Math.max(-100, v * 10)) : 0,
          };
        },
      },
      {
        key: 'funding',
        name: 'Funding hot',
        triggerLabel: '> 1.5 bps × 3',
        read: (r) => {
          const v = r?.perp_signals?.funding_rate_latest_realized;
          const met = v != null && v * 10000 > 1.5;
          return {
            value: v,
            display: fmtBps(v),
            met,
            near: v != null && v * 10000 > 0.8,
            progressPct: v != null ? Math.max(0, Math.min(100, (v * 10000 / 1.5) * 100)) : 0,
          };
        },
      },
      {
        key: 'basis',
        name: 'Basis',
        triggerLabel: '> 0.25%',
        read: (r) => {
          const v = r?.perp_signals?.basis_pct;
          const met = v != null && v > 0.25;
          return {
            value: v,
            display: v != null ? `${v.toFixed(3)}%` : '—',
            met,
            near: v != null && v > 0.1,
            progressPct: v != null ? Math.max(0, Math.min(100, ((v + 0.1) / 0.35) * 100)) : 0,
          };
        },
      },
      {
        key: 'lsratio',
        name: 'L/S ratio',
        triggerLabel: '> 0.65',
        read: (r) => {
          const v = r?.perp_signals?.long_short_ratio;
          const met = v != null && v > 0.65;
          return {
            value: v,
            display: v != null ? v.toFixed(3) : '—',
            met,
            near: v != null && v > 0.58,
            progressPct: v != null ? Math.max(0, Math.min(100, ((v - 0.45) / 0.2) * 100)) : 0,
          };
        },
      },
      {
        key: 'etf',
        name: 'ETF outflows',
        triggerLabel: 'neg 3+/5d',
        read: (r) => {
          // qualitative — pull from notes
          const note = r?.market_snapshot?.active_tier1_event || '';
          const met = /-?\$?1?\.?\d+B|net outflow|outflow/i.test(note);
          return {
            value: met,
            display: met ? 'confirmed' : 'no',
            met,
            near: false,
            progressPct: met ? 100 : 0,
          };
        },
      },
    ],
  },
};

// ---------- agent / pipeline ----------

const AGENTS = [
  { key: 'orchestrator', name: 'Orchestrator', scheduleLabel: 'Daily 1:00 UTC', scheduleHour: 1,  badge: 'O', color: 'var(--accent)',
    blurb: 'Sets allocation, retires strategies, runs weekly review on Sunday.' },
  { key: 'researcher',   name: 'Researcher',   scheduleLabel: 'Daily 6:00 UTC', scheduleHour: 6,  badge: 'R', color: 'var(--green)',
    blurb: 'Gathers market data, generates hypotheses, scores regime.' },
  { key: 'tester',       name: 'Strategy Tester', scheduleLabel: 'Daily 8:00 UTC', scheduleHour: 8, badge: 'S', color: 'var(--blue)',
    blurb: 'Evaluates entry triggers, scores strategies, emits signals.' },
  { key: 'trader',       name: 'Trader',       scheduleLabel: 'Daily 9:00 UTC', scheduleHour: 9,  badge: 'T', color: 'var(--amber)',
    blurb: 'Executes signals on Bybit, manages exits, updates portfolio.' },
  { key: 'reporter',     name: 'Reporter',     scheduleLabel: 'Daily 19:00 UTC', scheduleHour: 19, badge: 'P', color: 'var(--pink)',
    blurb: 'Compiles daily summary, posts to Discord.' },
];

// agent name in system-log → display key
const LOG_AGENT_MAP = {
  orchestrator: 'orchestrator',
  researcher: 'researcher',
  strategy_tester: 'tester',
  tester: 'tester',
  trader: 'trader',
  reviewer: 'orchestrator', // reviewer is weekly-mode orchestrator
  reporter: 'reporter',
};

// utility — find last log entry by agent
function findLastByAgent(entries, agentKey) {
  if (!entries?.length) return null;
  return [...entries].reverse().find((e) => {
    const mapped = LOG_AGENT_MAP[e.agent] || e.agent;
    return mapped === agentKey;
  });
}

// next run time for an agent — given UTC hour schedule
function nextRunFor(agent) {
  const now = new Date();
  const next = new Date(now);
  next.setUTCHours(agent.scheduleHour, 0, 0, 0);
  if (next <= now) next.setUTCDate(next.getUTCDate() + 1);
  const diffMin = Math.round((next - now) / 60000);
  if (diffMin < 60) return `in ${diffMin}m`;
  return `in ${Math.round(diffMin / 60)}h`;
}

// ---------- status label helpers ----------

function statusBadge(status) {
  const map = {
    active: 'active',
    paper_testing: 'testing',
    ready_to_trade: 'ready',
    underperforming: 'under',
    retired: 'retired',
  };
  return map[status] || 'paper';
}

function priorityColor(p) {
  return { explore: 'var(--blue)', exploit: 'var(--green)', defensive: 'var(--red)' }[p] || 'var(--text)';
}

window.BTC = {
  fmt, fmtBTC, fmtSats, fmtUSD, fmtBps, fmtPctSigned,
  signClass, deltaClass, relTime, formatTimeUTC, formatDay, truncate,
  LEG_CONFIG, AGENTS, LOG_AGENT_MAP, findLastByAgent, nextRunFor,
  statusBadge, priorityColor,
};
