/* ============================================================
   market-sensor-core.jsx — helpers, state metadata, colour map,
   formatters. Loaded first; everything else reads window.msr*.
   ============================================================ */

/* color NAME (from JSON state.color) -> hex token (dark-band tuned) */
const MSR_COLOR = {
  green:  '#69B083',
  navy:   '#9CA3AE',  /* slate — navy-on-navy would vanish on the dark band */
  amber:  '#8197CE',
  red:    '#CC7A6C',
  purple: '#9B8FB5',
  grey:   '#9CA3AE',
};
const msrTone = (name) => MSR_COLOR[name] || '#9CA3AE';

/* per-state display metadata + UX flags (colour still comes from JSON) */
const MSR_STATE_META = {
  CONFIRMING:       { title: 'Confirming',       glyph: '\u25B2', sub: 'Trend confirming — trade your plan' },
  MILD_BULL:        { title: 'Mild Bull',        glyph: '\u25B2', sub: 'Mild bullish tilt' },
  NEUTRAL:          { title: 'Neutral',          glyph: '\u25CF', sub: 'Calm / range conditions' },
  MILD_BEAR:        { title: 'Mild Bear',        glyph: '\u25BE', sub: 'Informational — not a sell signal', informational: true },
  WHIPSAW:          { title: 'Whipsaw',          glyph: '\u2195', sub: 'Wide-range warning — size down', pulse: true,
                      why: 'A wide-range day: both longs AND shorts get stopped out (in the backtest, shorts suffered ~6× baseline). This is a volatility warning, not a bearish call.' },
  DIRECTIONAL_RISK: { title: 'Directional Risk', glyph: '\u25BC', sub: 'Confirmed downside risk', alarm: true },
  EVENT_RISK:       { title: 'Event Risk',       glyph: '\u25C8', sub: 'Scheduled event pending — wait it out', override: true },
  UNKNOWN:          { title: 'Unknown',          glyph: '?',      sub: 'Sensor data unavailable', unknown: true },
};
const msrStateMeta = (label) => MSR_STATE_META[label] || MSR_STATE_META.UNKNOWN;

/* formatters */
const msrFmtZ = (v) => (v == null || isNaN(v)) ? '—' : (v >= 0 ? '+' : '\u2212') + Math.abs(v).toFixed(2);
const msrFmtPct = (v) => (v == null || isNaN(v)) ? '—' : (v >= 0 ? '+' : '\u2212') + Math.abs(v).toFixed(2) + '%';
const msrFmtPrice = (v) => {
  if (v == null || isNaN(v)) return '—';
  if (Math.abs(v) >= 1000) return v.toLocaleString('en-US', { maximumFractionDigits: 2 });
  if (Math.abs(v) >= 100) return v.toFixed(2);
  return v.toFixed(2);
};

/* relative time: ISO -> "5h ago" / "2d ago" / "just now" */
function msrRelTime(iso) {
  if (!iso) return '';
  const t = new Date(iso).getTime();
  if (isNaN(t)) return '';
  const diff = Date.now() - t;
  const mins = Math.round(diff / 60000);
  if (mins < 1) return 'just now';
  if (mins < 60) return mins + 'm ago';
  const hrs = Math.round(mins / 60);
  if (hrs < 24) return hrs + 'h ago';
  const days = Math.round(hrs / 24);
  return days + 'd ago';
}

/* Pick the authoritative UTC instant from a meta object (or a bare string).
   run_time_utc is correctly tz-stamped; run_time_et in the live feed carries a
   bogus +00:00 suffix while actually holding ET wall-clock, so prefer UTC. */
function msrInstant(metaOrIso) {
  if (!metaOrIso) return null;
  if (typeof metaOrIso === 'string') return metaOrIso;
  return metaOrIso.run_time_utc || metaOrIso.run_time_et || metaOrIso.run_time_sgt || null;
}

/* stale check: run older than 24h */
function msrStale(metaOrIso) {
  const iso = msrInstant(metaOrIso);
  if (!iso) return { stale: false };
  const t = new Date(iso).getTime();
  if (isNaN(t)) return { stale: false };
  const ageH = (Date.now() - t) / 3600000;
  return { stale: ageH > 24, label: msrRelTime(iso), ageH };
}

/* freshness label prettifier */
const MSR_FRESH_LABEL = {
  previous_session_close: 'prev close',
  previous_close_only: 'prev close only',
  last_48h: 'last 48h',
  live: 'live',
  unavailable: 'offline',
};
const msrFresh = (v) => MSR_FRESH_LABEL[v] || (v || '—');

/* format the run time -> "09:11 ET". Derive from the true UTC instant so it is
   correct even when run_time_et is mislabelled with a +00:00 offset. */
function msrRunClockET(metaOrIso) {
  const iso = msrInstant(metaOrIso);
  if (!iso) return '—';
  try {
    return new Date(iso).toLocaleTimeString('en-US', { timeZone: 'America/New_York', hour: '2-digit', minute: '2-digit', hour12: false }) + ' ET';
  } catch (e) { return '—'; }
}

Object.assign(window, {
  MSR_COLOR, msrTone, MSR_STATE_META, msrStateMeta,
  msrFmtZ, msrFmtPct, msrFmtPrice, msrRelTime, msrStale, msrFresh, msrRunClockET, msrInstant,
});
