/* ══ OM Group ERP — Analysis Studio · shared primitives ═══════════════════
   Atoms and one plotting primitive shared by every Studio panel. The plot is
   deliberately geometry-first: it hands its scales to an `overlay` render
   prop so the annotation layer can anchor drawings in DATA space (bucket
   index + value) rather than screen pixels — annotations therefore survive
   new transactions, rescaling, filter changes and grain changes.         */
const { useState: stSt, useEffect: stEf, useMemo: stMemo, useRef: stRef, useCallback: stCb } = React;
const DE = window.DecisionEngine;
const IEs = window.IntelEngine;
const stClamp = (v, a, b) => Math.max(a, Math.min(b, v));

/* ── section shell (always visible; Studio panels are already deferred) ── */
function StSec({ title, sub, right, children, id, icon }) {
  return (
    <section className="i-sec in" id={id} data-screen-label={title}>
      <div className="i-sec-hd">
        <div><div className="i-sec-t">{window.OMSecIcon ? <window.OMSecIcon name={icon} title={typeof title === 'string' ? title : ''} /> : <span className="i-dot"></span>}{title}</div>{sub && <div className="i-sec-s">{sub}</div>}</div>
        {right}
      </div>
      {children}
    </section>
  );
}

function StTag({ level, children }) { return <span className={'st-tag ' + (level || 'n')}>{children}</span>; }

function StMeter({ label, pct, color }) {
  return (
    <span className="st-meter" style={color ? { '--oc': color } : null}>
      <i><b style={{ width: stClamp(pct, 0, 100) + '%' }}></b></i>{label}
    </span>
  );
}

function StEmpty({ title, msg }) {
  return (
    <div className="st-empty">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6"><circle cx="12" cy="12" r="9" /><path d="M12 8v4M12 16h.01" /></svg>
      <b>{title}</b><span>{msg}</span>
    </div>
  );
}

/* ── lever ─────────────────────────────────────────────────────────────── */
function StLever({ def, value, onChange }) {
  const [open, setOpen] = stSt(false);
  const v = Number(value) || 0;
  const fill = ((v - def.min) / (def.max - def.min)) * 100;
  const sign = v > 0 ? '+' : '';
  return (
    <div className={'st-lev' + (v !== 0 ? ' act' : '')}>
      <div className="st-lev-top">
        <div style={{ minWidth: 0 }}>
          <div className="st-lev-l">{def.label}</div>
          <div className="st-lev-b">{def.baseFmt}</div>
        </div>
        <div className="st-lev-v">{sign}{def.step < 1 ? v.toFixed(1) : v}{def.unit === '%' ? '%' : def.unit === 'pp' ? ' pp' : ' d'}</div>
      </div>
      <input className="st-range" type="range" min={def.min} max={def.max} step={def.step} value={v}
        style={{ '--fill': fill + '%' }} onChange={e => onChange(def.id, Number(e.target.value))}
        aria-label={def.label} />
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 }}>
        <span style={{ fontSize: 9, color: 'var(--iInk3)', fontWeight: 700 }}>{def.min}{def.unit === '%' ? '%' : ''}</span>
        <button className="i-crumb" style={{ padding: '3px 8px', fontSize: 9.5 }} onClick={() => setOpen(o => !o)}>{open ? 'Hide' : 'Why'}</button>
        <span style={{ fontSize: 9, color: 'var(--iInk3)', fontWeight: 700 }}>+{def.max}{def.unit === '%' ? '%' : ''}</span>
      </div>
      {open && <div className="st-lev-h">{def.hint}</div>}
    </div>
  );
}

/* ── result tile ───────────────────────────────────────────────────────── */
function StRes({ label, value, base, kind, color, good, note }) {
  const d = base != null && base !== 0 ? (value - base) / Math.abs(base) * 100 : (base === 0 && value !== 0 ? 100 : 0);
  const better = good === 'down' ? d < 0 : d > 0;
  const cls = Math.abs(d) < 0.05 ? 'fl' : better ? 'up' : 'dn';
  return (
    <div className="st-res-c" style={{ '--rc': color || 'var(--iAccent)', '--rf': Math.min(1, Math.abs(d) / 25) }}>
      <div className="st-res-k">{label}</div>
      <div className="st-res-v"><window.IC.Num value={value} kind={kind || 'cur'} /></div>
      <div className="st-res-b">
        {base != null && <span className={'i-delta ' + cls}>{Math.abs(d) < 0.05 ? '—' : (d > 0 ? '+' : '−') + Math.abs(d).toFixed(1) + '%'}</span>}
        <span className="st-res-base">{note || (base != null ? 'from ' + window.IC.fmt(base, kind || 'cur') : '')}</span>
      </div>
    </div>
  );
}

/* ══ PLOT ═════════════════════════════════════════════════════════════════
   labels: [str]  series: [{key,label,color,values:[n|null],dashed,area}]
   band:   { lo:[n], hi:[n], color }   splitAt: index history ends at
   overlay(geom) → svg nodes           pointer: {down,move,up,leave}
   view:   {x0,x1} in bucket-index units, {y0,y1} in value units — the
           VISIBLE domain. onView(next, animate) where next is a view or a
           reducer (current, base) => view.

   ── viewport model ──────────────────────────────────────────────────────
   The pixel box is fixed forever: padL / padT / iw / ih never change with
   zoom. Zooming moves the DOMAIN RECTANGLE that box maps — exactly the model
   TradingView, Illustrator and AutoCAD use. Consequences, all of them the
   point: the chart can never grow past its frame, never reach the toolbar,
   never cover the axis; drawings stay pinned to their data coordinates
   because g.X / g.Y simply re-project them; and everything drawable is
   clipped to the plot rect, so nothing escapes into the surrounding UI.  */
/* zoom-out is deliberately generous: like TradingView you may pull the data
   back to a small block in the middle of the frame (down to ~5%). Nothing
   overlaps or under-laps on the way out because the axes re-tick on nice
   numbers and both label sets thin by PIXEL spacing, not by bucket count. */
const ST_VIEW = { xMinSpan: 1.5, yIn: 18, xOut: 20, yOut: 20 };
function stClampView(v, b) {
  if (!v || !b) return b;
  const bx = Math.max(1e-9, b.x1 - b.x0), by = Math.max(1e-9, b.y1 - b.y0);
  const xs = stClamp(v.x1 - v.x0, Math.min(bx, ST_VIEW.xMinSpan), bx * ST_VIEW.xOut);
  const ys = stClamp(v.y1 - v.y0, by / ST_VIEW.yIn, by * ST_VIEW.yOut);
  let cx = (v.x0 + v.x1) / 2, cy = (v.y0 + v.y1) / 2;
  const xl = b.x0 + xs / 2, xh = b.x1 - xs / 2;
  cx = xl <= xh ? stClamp(cx, xl, xh) : (b.x0 + b.x1) / 2;
  const yl = b.y0 + ys / 2 - by * 0.3, yh = b.y1 - ys / 2 + by * 0.3;
  cy = yl <= yh ? stClamp(cy, yl, yh) : (b.y0 + b.y1) / 2;
  return { x0: cx - xs / 2, x1: cx + xs / 2, y0: cy - ys / 2, y1: cy + ys / 2 };
}
/* a 1 / 2 / 2.5 / 5 × 10ⁿ step, so gridlines land on readable numbers at
   every zoom level instead of on arbitrary fractions of the visible span */
function stNiceStep(span, count) {
  if (!isFinite(span) || span <= 0) return 1;
  const raw = span / Math.max(1, count), mag = Math.pow(10, Math.floor(Math.log10(Math.abs(raw)))), r = raw / mag;
  return (r <= 1 ? 1 : r <= 2 ? 2 : r <= 2.5 ? 2.5 : r <= 5 ? 5 : 10) * mag;
}

function StPlot({ height, labels, series, yKind, splitAt, band, overlay, pointer, cursor, hideTip, extraLabels, view, onView, wheelMode }) {
  const H = height || 300;
  const [w, wrap] = window.IC.useWidth(760);
  const svgRef = stRef(null);
  const [hi, setHi] = stSt(null);
  const [panning, setPanning] = stSt(false);
  const spaceRef = stRef(false);
  const panRef = stRef(null);
  const padL = 58, padR = 16, padT = 18, padB = 30;
  const iw = Math.max(40, w - padL - padR), ih = Math.max(40, H - padT - padB);
  const n = labels.length;
  const uid = stMemo(() => 'stp' + Math.random().toString(36).slice(2, 7), []);
  const clipId = 'clip-' + uid;

  /* the natural, fit-to-data domain — the view is always expressed against it */
  const base = stMemo(() => {
    const vals = [];
    series.forEach(s => (s.values || []).forEach(v => { if (v != null && isFinite(v)) vals.push(+v); }));
    if (band) { (band.lo || []).forEach(v => { if (v != null && isFinite(v)) vals.push(+v); }); (band.hi || []).forEach(v => { if (v != null && isFinite(v)) vals.push(+v); }); }
    let hiV = vals.length ? Math.max.apply(null, vals) : 1;
    let loV = vals.length ? Math.min.apply(null, vals) : 0;
    loV = Math.min(0, loV);
    const span0 = (hiV - loV) || Math.abs(hiV) || 1;
    hiV = hiV + span0 * 0.12;
    return { x0: 0, x1: Math.max(1, n - 1), y0: loV, y1: hiV };
  }, [JSON.stringify(series.map(s => s.values)), band && JSON.stringify([band.lo, band.hi]), n]);

  const dom = stMemo(() => stClampView(view, base), [view, base]);

  const geom = stMemo(() => {
    const xs = Math.max(1e-9, dom.x1 - dom.x0), ys = Math.max(1e-9, dom.y1 - dom.y0);
    const X = i => padL + ((i - dom.x0) / xs) * iw;
    const Y = v => padT + ih - (((Number(v) || 0) - dom.y0) / ys) * ih;
    const invX = px => dom.x0 + (px - padL) / iw * xs;
    const invY = py => dom.y0 + ((padT + ih - py) / ih) * ys;
    return {
      X, Y, invX, invY, padL, padR, padT, padB, iw, ih, w, H, n,
      min: dom.y0, max: dom.y1, x0: dom.x0, x1: dom.x1, dom, base,
      yKind: yKind || 'short', zoom: (base.x1 - base.x0) / xs,
    };
  }, [dom, base, w, H, iw, ih, n, yKind]);

  const ticks = stMemo(() => {
    const span = geom.max - geom.min;
    const step = stNiceStep(span, stClamp(Math.round(ih / 58), 3, 8));
    const out = [];
    for (let v = Math.ceil(geom.min / step) * step; v <= geom.max + step * 1e-6 && out.length < 24; v += step) out.push(v);
    if (out.length < 2) { const o = []; for (let i = 0; i <= 4; i++) o.push(geom.min + span * i / 4); return o; }
    return out;
  }, [geom.min, geom.max, ih]);

  function seg(vals) {
    const parts = []; let cur = [];
    for (let i = 0; i < vals.length; i++) {
      const v = vals[i];
      if (v == null || !isFinite(v)) { if (cur.length) { parts.push(cur); cur = []; } continue; }
      cur.push([geom.X(i), geom.Y(v)]);
    }
    if (cur.length) parts.push(cur);
    return parts.map(pts => {
      if (pts.length === 1) return 'M' + pts[0][0].toFixed(1) + ' ' + pts[0][1].toFixed(1) + 'l0 0';
      let d = 'M' + pts[0][0].toFixed(1) + ' ' + pts[0][1].toFixed(1);
      for (let i = 1; i < pts.length; i++) {
        const px = pts[i - 1][0], py = pts[i - 1][1], x = pts[i][0], y = pts[i][1], cx = (px + x) / 2;
        d += ' C' + cx.toFixed(1) + ' ' + py.toFixed(1) + ' ' + cx.toFixed(1) + ' ' + y.toFixed(1) + ' ' + x.toFixed(1) + ' ' + y.toFixed(1);
      }
      return d;
    }).join(' ');
  }
  function bandPath() {
    if (!band) return '';
    const up = [], dn = [];
    for (let i = 0; i < n; i++) { const a = band.hi[i], b = band.lo[i]; if (a == null || b == null) continue; up.push([geom.X(i), geom.Y(a)]); dn.unshift([geom.X(i), geom.Y(b)]); }
    if (!up.length) return '';
    return 'M' + up.concat(dn).map(p => p[0].toFixed(1) + ' ' + p[1].toFixed(1)).join(' L') + ' Z';
  }
  function scale(e) {
    const svg = svgRef.current; if (!svg) return null;
    const box = svg.getBoundingClientRect();
    if (!box.width || !box.height) return null;
    return { box, sx: w / box.width, sy: H / box.height };
  }
  function toData(e) {
    const s = scale(e); if (!s) return null;
    const cx = (e.clientX != null ? e.clientX : (e.touches && e.touches[0] ? e.touches[0].clientX : 0));
    const cy = (e.clientY != null ? e.clientY : (e.touches && e.touches[0] ? e.touches[0].clientY : 0));
    const px = (cx - s.box.left) * s.sx, py = (cy - s.box.top) * s.sy;
    return { px, py, ix: geom.invX(px), val: geom.invY(py) };
  }

  /* ── navigation ──────────────────────────────────────────────────────── */
  stEf(() => {
    if (!onView) return;
    const field = t => /input|textarea|select/i.test((t && t.tagName) || '') || (t && t.isContentEditable);
    const dn = e => { if (e.code === 'Space' && !field(e.target)) spaceRef.current = true; };
    const up = e => { if (e.code === 'Space') spaceRef.current = false; };
    const blur = () => { spaceRef.current = false; };
    window.addEventListener('keydown', dn); window.addEventListener('keyup', up); window.addEventListener('blur', blur);
    return () => { window.removeEventListener('keydown', dn); window.removeEventListener('keyup', up); window.removeEventListener('blur', blur); };
  }, [onView]);

  function panPx(dpx, dpy) {
    const xs = dom.x1 - dom.x0, ys = dom.y1 - dom.y0;
    const dx = -dpx / iw * xs, dy = dpy / ih * ys;
    onView(cur => ({ x0: cur.x0 + dx, x1: cur.x1 + dx, y0: cur.y0 + dy, y1: cur.y1 + dy }), false);
  }
  function endPan(e) {
    if (!panRef.current) return false;
    panRef.current = null; setPanning(false);
    try { e && e.currentTarget && e.currentTarget.releasePointerCapture && e.currentTarget.releasePointerCapture(e.pointerId); } catch (err) { }
    return true;
  }
  function wheel(e) {
    if (!onView) return;
    /* embedded in a scrolling page the wheel belongs to the page — zoom then
       needs ⌘/Ctrl. Full screen owns the viewport, so the wheel zooms bare. */
    if (wheelMode !== 'always' && !(e.ctrlKey || e.metaKey)) return;
    const s = scale(e); if (!s) return;
    e.preventDefault();
    const px = (e.clientX - s.box.left) * s.sx, py = (e.clientY - s.box.top) * s.sy;
    const rx = stClamp((px - padL) / iw, 0, 1), ry = stClamp((padT + ih - py) / ih, 0, 1);
    const horiz = Math.abs(e.deltaX) > Math.abs(e.deltaY) * 1.3;
    if (horiz || e.shiftKey) { /* trackpad scroll / shift-wheel pans the timeline */
      const d = stClamp(horiz ? e.deltaX : e.deltaY, -120, 120);
      onView(cur => { const k = d / iw * (cur.x1 - cur.x0); return { x0: cur.x0 + k, x1: cur.x1 + k, y0: cur.y0, y1: cur.y1 }; }, false);
      return;
    }
    const k = Math.exp(stClamp(e.deltaY, -90, 90) * 0.0032);
    onView(cur => {
      const cxs = cur.x1 - cur.x0, cys = cur.y1 - cur.y0;
      const xs = cxs * k, ys = cys * k;
      const fx = cur.x0 + rx * cxs, fy = cur.y0 + ry * cys;
      return { x0: fx - rx * xs, x1: fx + (1 - rx) * xs, y0: fy - ry * ys, y1: fy + (1 - ry) * ys };
    }, true);
  }
  const wheelRef = stRef(wheel); wheelRef.current = wheel;
  stEf(() => {
    const el = svgRef.current; if (!el || !onView) return;
    const h = e => wheelRef.current && wheelRef.current(e);
    el.addEventListener('wheel', h, { passive: false });
    return () => el.removeEventListener('wheel', h);
  }, [onView, wheelMode]);

  function onDown(e) {
    if (onView && (e.button === 1 || spaceRef.current)) {
      e.preventDefault();
      panRef.current = { x: e.clientX, y: e.clientY }; setPanning(true);
      try { e.currentTarget.setPointerCapture(e.pointerId); } catch (err) { }
      return;
    }
    const d = toData(e); if (pointer && pointer.down && d) pointer.down(e, d);
  }
  function onMove(e) {
    if (panRef.current) {
      const s = scale(e); if (!s) return;
      panPx((e.clientX - panRef.current.x) * s.sx, (e.clientY - panRef.current.y) * s.sy);
      panRef.current = { x: e.clientX, y: e.clientY };
      return;
    }
    const d = toData(e); if (!d) return;
    if (pointer && pointer.move) pointer.move(e, d);
    const idx = stClamp(Math.round(d.ix), 0, n - 1);
    const inside = d.ix >= geom.x0 - 0.5 && d.ix <= geom.x1 + 0.5;
    if (idx !== hi) setHi(inside ? idx : null);
    if (!hideTip && inside && window.ICTooltip) window.ICTooltip.show(e, {
      title: (labels[idx] || '') + (extraLabels && extraLabels[idx] ? ' · ' + extraLabels[idx] : ''),
      rows: series.filter(s => s.values[idx] != null).map(s => ({ dot: s.color, k: s.label, v: window.IC.fmt(s.values[idx], yKind || 'short'), color: s.color })),
      foot: splitAt != null && idx >= splitAt ? 'Projected' : '',
    });
  }
  if (!n) return <window.IC.Empty h={H} />;
  const curCss = panning ? 'grabbing' : (cursor || 'crosshair');
  return (
    <div ref={wrap} className="ic-chart" style={{ position: 'relative' }}>
      <svg ref={svgRef} className={'st-canvas' + (cursor === 'default' ? ' sel' : '')} width="100%" height={H} viewBox={'0 0 ' + w + ' ' + H}
        style={{ cursor: curCss, display: 'block', height: H + 'px', overflow: 'hidden' }}
        onPointerDown={onDown}
        onPointerMove={onMove}
        onPointerUp={e => { if (endPan(e)) return; const d = toData(e); if (pointer && pointer.up && d) pointer.up(e, d); }}
        onDoubleClick={e => { if (onView) { e.preventDefault(); onView(null, true); } }}
        onAuxClick={e => { if (e.button === 1) e.preventDefault(); }}
        onPointerLeave={e => { endPan(e); setHi(null); if (window.ICTooltip) window.ICTooltip.hide(); if (pointer && pointer.leave) pointer.leave(e); }}>
        <defs>
          <clipPath id={clipId}><rect x={padL} y={padT} width={iw} height={ih} /></clipPath>
          {series.map((s, i) => (
            <linearGradient key={i} id={'stg' + i + '-' + (s.key || i)} x1="0" y1="0" x2="0" y2="1">
              <stop offset="0%" stopColor={s.color} stopOpacity="0.24" /><stop offset="100%" stopColor={s.color} stopOpacity="0" />
            </linearGradient>
          ))}
        </defs>
        <rect x={padL} y={padT} width={iw} height={ih} fill="#fff" />
        {ticks.map((v, i) => {
          const y = geom.Y(v);
          if (y < padT - 0.5 || y > padT + ih + 0.5) return null;
          return (
            <g key={'t' + i}>
              <line x1={padL} x2={padL + iw} y1={y} y2={y} stroke={Math.abs(v) < 1e-9 ? '#E5E3E0' : '#F1EFEC'} strokeWidth="1" />
              <text x={padL - 8} y={y + 3.5} textAnchor="end" fontSize="9.5" fill="#A8A4A0" fontWeight="500">{window.IC.short(v)}</text>
            </g>
          );
        })}
        {/* ── everything data-driven lives inside the clip: nothing can escape ── */}
        <g clipPath={'url(#' + clipId + ')'}>
          {band && <path d={bandPath()} fill={band.color} fillOpacity="0.11" stroke="none" />}
          {splitAt != null && splitAt > 0 && splitAt < n && (
            <g>
              <line x1={geom.X(splitAt - 0.5)} x2={geom.X(splitAt - 0.5)} y1={padT} y2={padT + ih} stroke="#1A1917" strokeWidth="1" strokeDasharray="2 4" opacity=".22" />
              <text x={geom.X(splitAt - 0.5) + 5} y={padT + 10} fontSize="8.5" fill="#A8A4A0" fontWeight="800" letterSpacing=".08em">FORECAST →</text>
            </g>
          )}
          {series.map((s, i) => (
            <g key={'s' + i}>
              {s.area && (() => {
                /* the fill closes on the ZERO line, not the bottom of the box —
                   so zooming out leaves a shrinking block of data, not a column
                   running to the frame edge */
                const b0 = stClamp(geom.Y(0), padT, padT + ih).toFixed(1);
                return <path d={seg(s.values) + ' L' + geom.X(n - 1).toFixed(1) + ' ' + b0 + ' L' + geom.X(0).toFixed(1) + ' ' + b0 + ' Z'} fill={'url(#stg' + i + '-' + (s.key || i) + ')'} stroke="none" />;
              })()}
              <path d={seg(s.values)} fill="none" stroke={s.color} strokeWidth={s.width || (s.dashed ? 1.9 : 2.4)}
                strokeDasharray={s.dashed ? '5 4' : null} strokeLinecap="round" strokeLinejoin="round"
                style={{ filter: s.dashed ? null : 'drop-shadow(0 4px 10px ' + s.color + '2e)' }} />
            </g>
          ))}
          {hi != null && !hideTip && (
            <g pointerEvents="none">
              <line x1={geom.X(hi)} x2={geom.X(hi)} y1={padT} y2={padT + ih} stroke="#1A1917" strokeWidth="1" strokeDasharray="3 3" opacity=".26" />
              {series.filter(s => s.values[hi] != null).map((s, i) => (
                <circle key={i} cx={geom.X(hi)} cy={geom.Y(s.values[hi])} r="4.5" fill="#fff" stroke={s.color} strokeWidth="2.4" />
              ))}
            </g>
          )}
          {overlay && overlay(geom)}
        </g>
        {(() => {
          const first = Math.max(0, Math.ceil(geom.x0 - 1e-6)), last = Math.min(n - 1, Math.floor(geom.x1 + 1e-6));
          if (last < first) return null;
          /* thinning by pixel pitch keeps labels legible at every scale — dense
             when zoomed in, sparse when the whole period is squeezed small */
          const pxPer = iw / Math.max(1e-9, geom.x1 - geom.x0);
          const step = Math.max(1, Math.ceil(74 / Math.max(1e-6, pxPer)));
          const out = [];
          for (let i = first; i <= last; i += step) out.push(i);
          if (out[out.length - 1] !== last) {
            if (out.length > 1 && geom.X(last) - geom.X(out[out.length - 1]) < 46) out[out.length - 1] = last; else out.push(last);
          }
          return out.map(i => {
            const x = geom.X(i);
            if (x < padL - 2 || x > padL + iw + 2) return null;
            return <text key={'l' + i} x={x} y={H - 9} textAnchor="middle" fontSize="9.5" fill={hi === i ? '#1A1917' : '#A8A4A0'} fontWeight={hi === i ? 700 : 500}>{labels[i]}</text>;
          });
        })()}
      </svg>
    </div>
  );
}

Object.assign(window, { StSec, StTag, StMeter, StEmpty, StLever, StRes, StPlot, stClamp, stClampView, stNiceStep });
