/* global React, Pin */
// Timeline - hero canvas widget. Signal volume over a 6-day window with
// anomaly markers and brush-select region. Multiple visual modes:
//   mode="area"  default streamgraph-ish area
//   mode="bars"  agentic-command applied (after "switch timeline to bars")
//   mode="lanes" agentic-command applied ("split into two lanes")
//   mode="diff"  ("compare to last week") shadow of prior week behind
//
// Incident: Marcus Chen, Senior DBA, MBP-MCHEN-01.
// 4-day window: 5 Mar 2026 (day 0) through 8 Mar 2026 (day 3).
// All 8 events from event-log.md are plotted as ticks.
// Day 0 = 5 Mar 2026. Fractional day = hour/24 offset within that day.
//   EVT-0305: day 0 (5 Mar). Login 02:47 = day 0.116, MFA 03:41 = day 0.154
//   PII query 04:08 = day 0.172, PII label 04:23 = day 0.183
//   Dropbox 04:51 = day 0.202, IAM key 05:17 = day 0.221
//   EVT-0306: day 1 (6 Mar). Device fingerprint 14:23 = day 1.599, Geo anomaly 16:05 = day 1.670

const { useMemo: useM, useState: uS, useRef: uR, useEffect: uE, useLayoutEffect: uLE, useCallback: uCB } = React;

// 4 days (96h), 96 ticks (one per hour). Anomaly on day 0 02:00-06:00.
// Day 1 shows a secondary device/geo anomaly spike in the afternoon.
function buildSignal(seed = 7) {
  const N = 96; // 4 days x 24h
  const out = [];
  let s = seed;
  const rnd = () => (s = (s * 9301 + 49297) % 233280) / 233280;
  for (let i = 0; i < N; i++) {
    const day = Math.floor(i / 24);
    const hour = i % 24;
    const dayFrac = i / 24;
    // diurnal cycle: peaks around 10:00 business hours
    const diurnal = 0.45 + 0.40 * Math.sin(((hour - 9) / 24) * Math.PI * 2);
    let v = diurnal * (0.85 + 0.3 * rnd());
    // day 0 night (02:00-06:00): Marcus Chen attack window - major spike
    if (dayFrac >= 0.11 && dayFrac < 0.23 && hour >= 2 && hour <= 6) {
      v += 0.9 + 0.35 * rnd(); // +840% PII query burst
    }
    // day 1 afternoon (14:00-17:00): device fingerprint + Bucharest geo anomaly
    if (dayFrac >= 1.58 && dayFrac < 1.72 && hour >= 14 && hour <= 17) {
      v += 0.5 + 0.2 * rnd();
    }
    // days 2-3: returned to baseline, minor elevated (ongoing investigation)
    if (day >= 2) v *= 0.75;
    out.push(Math.max(0.05, Math.min(1.8, v)));
  }
  return out;
}

// days = 4 (the window)
const DAYS = 4;

const ANOMALIES = [
  // EVT-0305: attack window
  { id: 'A1', day: 0.172, kind: 'critical', label: 'DB query spike +840% - customers_pii' },
  { id: 'A2', day: 0.202, kind: 'critical', label: 'Personal Dropbox upload - 2.3 GB' },
  { id: 'A3', day: 0.116, kind: 'warning',  label: 'Odd-hours login - 02:47 UTC' },
  // EVT-0306: twist markers
  { id: 'A4', day: 1.599, kind: 'warning',  label: 'New device fingerprint - Windows/Chrome' },
  { id: 'A5', day: 1.670, kind: 'warning',  label: 'Geo anomaly - Bucharest RO parallel session' },
];

// All 8 events from event-log.md
const EVENTS = [
  { day: 0.116, t: '02:47', label: 'Odd-hours login - 10.14.22.88',          tone: 'mustard', kind: 'auth',   mitre: 'T1078',     date: '05 Mar' },
  { day: 0.154, t: '03:41', label: 'MFA fatigue accept - 6 push prompts',    tone: 'mustard', kind: 'auth',   mitre: 'T1621',     date: '05 Mar' },
  { day: 0.172, t: '04:08', label: 'DB query spike - customers_pii 412k',    tone: 'brick',   kind: 'data',   mitre: 'T1213',     date: '05 Mar' },
  { day: 0.183, t: '04:23', label: 'Sensitive-label access - PII data class', tone: 'brick',  kind: 'data',   mitre: 'T1530',     date: '05 Mar' },
  { day: 0.202, t: '04:51', label: 'Personal Dropbox upload - 2.3 GB',       tone: 'brick',   kind: 'exfil',  mitre: 'T1567.002', date: '05 Mar' },
  { day: 0.221, t: '05:17', label: 'AWS IAM key created - personal acct',    tone: 'brick',   kind: 'iam',    mitre: 'T1098.001', date: '05 Mar' },
  { day: 1.599, t: '14:23', label: 'New device fingerprint - Win/Chrome',    tone: 'mustard', kind: 'device', mitre: 'T1078.004', date: '06 Mar' },
  { day: 1.670, t: '16:05', label: 'Geo anomaly - Bucharest RO session',     tone: 'mustard', kind: 'geo',    mitre: 'T1078',     date: '06 Mar' },
];

// Map a fractional day index to x within plot area
function dayToX(day, padL, plotW, days = 4) {
  return padL + (day / days) * plotW;
}

// Convert svg x position to day fraction
function xToDay(x, padL, plotW, days = 4) {
  return Math.max(0, Math.min(days, ((x - padL) / plotW) * days));
}

function Timeline({
  mode = 'area',
  width,             // optional override; if omitted, fills container via ResizeObserver
  height = 260,
  brush,             // [day0, day1] or null - controlled from outside
  pinned,            // array of [day0, day1] regions already pinned
  showEvents = true,
  showCompare,       // diff against prior week
  hoverDay,          // marker
  caption = 'Signal volume - 4-day investigation window - Marcus Chen',
  hideAxis,
  onBrushChange,     // (brush | null) => void
  onPinRegion,       // ([start,end]) => void
  onUnpinRegion,     // (idx) => void
  onResizeRegion,    // (idx, which: 'start'|'end', day) => void
}) {
  const containerRef = uR(null);
  const [containerW, setContainerW] = uS(width || 800);

  // ResizeObserver to fill container width responsively
  uLE(() => {
    if (width) { setContainerW(width); return; }
    const el = containerRef.current;
    if (!el) return;
    setContainerW(el.offsetWidth || 800);
    const ro = new ResizeObserver(entries => {
      const w = entries[0].contentRect.width;
      if (w > 0) setContainerW(w);
    });
    ro.observe(el);
    return () => ro.disconnect();
  }, [width]);

  const W = containerW;

  const [hovered, setHovered] = uS(null);
  // Internal brush state for pointer-driven interaction
  const [localBrush, setLocalBrush] = uS(null);
  const dragging = uR(false);
  const dragStart = uR(null);
  const svgRef = uR(null);

  // Use controlled brush if no onBrushChange provided, else use localBrush
  const activeBrush = onBrushChange ? localBrush : brush;

  const data = useM(() => buildSignal(7), []);
  const dataPrev = useM(() => buildSignal(13).map(v => v * 0.58), []);
  const padL = 36, padR = 16, padT = 20, padB = hideAxis ? 12 : 28;
  const plotW = W - padL - padR;
  const plotH = height - padT - padB;
  const days = DAYS;

  const max = Math.max(...data, ...(showCompare ? dataPrev : []), 1.8);
  const xOf = (i) => padL + (i / (data.length - 1)) * plotW;
  const yOf = (v) => padT + plotH - (v / max) * plotH;

  // grid + axis ticks
  const dayTicks = Array.from({ length: days + 1 }, (_, d) => ({
    x: dayToX(d, padL, plotW, days),
    label: ['Thu 5 Mar', 'Fri 6 Mar', 'Sat 7 Mar', 'Sun 8 Mar', ''][d],
  }));

  // area path
  const areaPath = useM(() => {
    const pts = data.map((v, i) => `${xOf(i).toFixed(1)} ${yOf(v).toFixed(1)}`);
    return `M${padL} ${padT + plotH} L` + pts.join(' L') + ` L${padL + plotW} ${padT + plotH} Z`;
  }, [data, plotW, plotH]);

  const linePath = useM(() => {
    return 'M' + data.map((v, i) => `${xOf(i).toFixed(1)} ${yOf(v).toFixed(1)}`).join(' L');
  }, [data, plotW, plotH]);

  const prevPath = useM(() => {
    return 'M' + dataPrev.map((v, i) => `${xOf(i).toFixed(1)} ${yOf(v).toFixed(1)}`).join(' L');
  }, [dataPrev, plotW, plotH]);

  // lanes path is memoized without random for stability
  const lanesData = useM(() => {
    return data.map((v, i) => {
      const day = i / 24;
      const lv = (day >= 0.1 && day <= 0.23) ? v * 0.8 : v * 0.28;
      return lv;
    });
  }, [data]);

  // brush rect from active brush (controlled or local)
  const brushRect = activeBrush ? (() => {
    const x0 = dayToX(Math.min(activeBrush[0], activeBrush[1]), padL, plotW, days);
    const x1 = dayToX(Math.max(activeBrush[0], activeBrush[1]), padL, plotW, days);
    return { x: x0, w: Math.max(2, x1 - x0) };
  })() : null;

  // Pointer event helpers - get day from svg mouse event
  const getSvgDay = uCB((e) => {
    const svg = svgRef.current;
    if (!svg) return 0;
    const rect = svg.getBoundingClientRect();
    const x = e.clientX - rect.left;
    return xToDay(x, padL, plotW, days);
  }, [padL, plotW, days]);

  const handleMouseDown = uCB((e) => {
    // Only respond to primary button, ignore if clicking on events
    if (e.button !== 0) return;
    const day = getSvgDay(e);
    dragging.current = true;
    dragStart.current = day;
    const newBrush = [day, day];
    setLocalBrush(newBrush);
    if (onBrushChange) onBrushChange(newBrush);
    e.preventDefault();
  }, [getSvgDay, onBrushChange]);

  const handleMouseMove = uCB((e) => {
    if (!dragging.current) return;
    const day = getSvgDay(e);
    const newBrush = [dragStart.current, day];
    setLocalBrush(newBrush);
    if (onBrushChange) onBrushChange(newBrush);
  }, [getSvgDay, onBrushChange]);

  const handleMouseUp = uCB((e) => {
    if (!dragging.current) return;
    dragging.current = false;
    const day = getSvgDay(e);
    const finalBrush = [dragStart.current, day];
    // Clear tiny brushes (< 5px)
    const dx = Math.abs(dayToX(finalBrush[1], padL, plotW, days) - dayToX(finalBrush[0], padL, plotW, days));
    const cleaned = dx < 5 ? null : finalBrush;
    setLocalBrush(cleaned);
    if (onBrushChange) onBrushChange(cleaned);
  }, [getSvgDay, onBrushChange, padL, plotW, days]);

  // Attach mousemove/mouseup to window during drag so it works outside svg
  uE(() => {
    const onMove = (e) => handleMouseMove(e);
    const onUp   = (e) => handleMouseUp(e);
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
    return () => {
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
    };
  }, [handleMouseMove, handleMouseUp]);

  // Sync external brush prop into localBrush when onBrushChange not provided
  uE(() => {
    if (!onBrushChange) setLocalBrush(brush || null);
  }, [brush, onBrushChange]);

  const showBrush = activeBrush;

  return (
    <div ref={containerRef} style={{ position: 'relative', width: '100%', height, fontFamily: 'var(--sans)', userSelect: 'none' }}>
      <svg
        ref={svgRef}
        width={W}
        height={height}
        style={{ display: 'block', cursor: 'crosshair' }}
        onMouseDown={handleMouseDown}
      >
        <defs>
          <pattern id="hatch-brick" width="4" height="4" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
            <rect width="4" height="4" fill="var(--wax-wash)" />
            <line x1="0" y1="0" x2="0" y2="4" stroke="var(--wax)" strokeWidth="0.5" opacity="0.5" />
          </pattern>
          <linearGradient id="area-fill" x1="0" x2="0" y1="0" y2="1">
            <stop offset="0" stopColor="#1B2238" stopOpacity="0.18" />
            <stop offset="1" stopColor="#1B2238" stopOpacity="0.04" />
          </linearGradient>
          <linearGradient id="area-fill-anomaly" x1="0" x2="0" y1="0" y2="1">
            <stop offset="0" stopColor="#C56B5C" stopOpacity="0.32" />
            <stop offset="1" stopColor="#C56B5C" stopOpacity="0.06" />
          </linearGradient>
        </defs>

        {/* grid */}
        {dayTicks.map((t, i) => (
          <line key={i} x1={t.x} x2={t.x} y1={padT} y2={padT + plotH}
            stroke="var(--rule)" strokeWidth="0.5"
            strokeDasharray={i === 0 || i === days ? undefined : '1 3'} />
        ))}
        {/* horizontal baseline */}
        <line x1={padL} x2={padL + plotW} y1={padT + plotH} y2={padT + plotH} stroke="var(--rule)" strokeWidth="0.5" />

        {/* compare-to-last-week ghost */}
        {showCompare && (
          <path d={prevPath} fill="none" stroke="var(--ink-faint)" strokeWidth="1" strokeDasharray="2 3" opacity="0.7" />
        )}

        {/* main series */}
        {mode === 'area' && (
          <>
            <path d={areaPath} fill="url(#area-fill)" />
            <path d={linePath} fill="none" stroke="var(--ink)" strokeWidth="1.1" />
          </>
        )}
        {mode === 'bars' && (
          <g>
            {data.map((v, i) => {
              const x = xOf(i);
              const w = (plotW / data.length) * 0.78;
              const y = yOf(v);
              const dayFrac = i / 24;
              const inAttack = (dayFrac >= 0.10 && dayFrac <= 0.24) || (dayFrac >= 1.58 && dayFrac <= 1.69);
              return (
                <rect key={i} x={x - w / 2} y={y} width={w} height={padT + plotH - y}
                  fill={inAttack ? 'var(--brick)' : 'var(--ink-soft)'}
                  opacity={inAttack ? 0.85 : 0.62} />
              );
            })}
          </g>
        )}
        {mode === 'lanes' && (() => {
          const half = plotH / 2 - 4;
          const upper = data.map((v, i) => `${xOf(i).toFixed(1)} ${(padT + half - (v / max) * half).toFixed(1)}`);
          const lower = lanesData.map((lv, i) => `${xOf(i).toFixed(1)} ${(padT + half + 8 + (lv / max) * half).toFixed(1)}`);
          return (
            <g>
              <text x={padL + 4} y={padT + 11} fontFamily="var(--mono)" fontSize="9" fill="var(--ink-mute)">AUTH SIGNALS</text>
              <text x={padL + 4} y={padT + half + 19} fontFamily="var(--mono)" fontSize="9" fill="var(--ink-mute)">DATA SIGNALS</text>
              <path d={'M' + upper.join(' L')} fill="none" stroke="var(--ink-soft)" strokeWidth="1" />
              <path d={'M' + lower.join(' L')} fill="none" stroke="var(--brick)" strokeWidth="1.1" />
              <line x1={padL} x2={padL + plotW} y1={padT + half + 4} y2={padT + half + 4} stroke="var(--rule)" strokeWidth="0.5" strokeDasharray="2 3" />
            </g>
          );
        })()}

        {/* pinned regions - solid wax-wash bracket with resize handles + minus button */}
        {(pinned || []).map((p, i) => {
          const x0 = dayToX(Math.min(p[0], p[1]), padL, plotW, days);
          const x1 = dayToX(Math.max(p[0], p[1]), padL, plotW, days);
          const w = x1 - x0;
          const dur = brushDuration([p[0], p[1]]);
          const startResize = (which) => (ev) => {
            ev.stopPropagation();
            ev.preventDefault();
            const onMove = (mv) => {
              const svgEl = ev.currentTarget.ownerSVGElement;
              if (!svgEl) return;
              const sr = svgEl.getBoundingClientRect();
              const rawX = mv.clientX - sr.left;
              const day = ((rawX - padL) / plotW) * days;
              if (onResizeRegion) onResizeRegion(i, which, Math.max(0, Math.min(days, day)));
            };
            const onUp = () => {
              window.removeEventListener('mousemove', onMove);
              window.removeEventListener('mouseup', onUp);
            };
            window.addEventListener('mousemove', onMove);
            window.addEventListener('mouseup', onUp);
          };
          return (
            <g key={i}>
              <rect x={x0} y={padT} width={w} height={plotH}
                fill="rgba(156,58,42,0.22)" stroke="var(--wax)" strokeWidth="1" />
              <text x={x0 + 6} y={padT - 8} fontFamily="var(--sans)" fontSize="11" fontWeight="500" fill="var(--wax)">
                {dur}
              </text>
              {/* resize handles, left and right */}
              {onResizeRegion && (
                <>
                  <rect x={x0 - 3} y={padT + plotH / 2 - 12} width="6" height="24" rx="1" fill="var(--wax)"
                    style={{ cursor: 'ew-resize' }} onMouseDown={startResize('start')} />
                  <rect x={x1 - 3} y={padT + plotH / 2 - 12} width="6" height="24" rx="1" fill="var(--wax)"
                    style={{ cursor: 'ew-resize' }} onMouseDown={startResize('end')} />
                </>
              )}
              {/* minus button at top-right corner */}
              {onUnpinRegion && (
                <foreignObject x={x1 - 11} y={padT - 11} width="22" height="22" style={{ overflow: 'visible' }}>
                  <button
                    type="button"
                    aria-label="Remove pinned region"
                    onMouseDown={(e) => { e.stopPropagation(); e.preventDefault(); onUnpinRegion(i); }}
                    style={{
                      width: 22, height: 22, borderRadius: '50%',
                      border: 'none', padding: 0,
                      background: 'var(--wax)',
                      color: 'var(--paper)',
                      cursor: 'pointer',
                      display: 'flex', alignItems: 'center', justifyContent: 'center',
                      font: '600 14px/1 var(--sans)',
                      lineHeight: 1,
                    }}
                  >−</button>
                </foreignObject>
              )}
            </g>
          );
        })}

        {/* anomaly markers - small ink ticks at the top */}
        {ANOMALIES.map(a => {
          const x = dayToX(a.day, padL, plotW, days);
          const c = a.kind === 'critical' ? 'var(--brick)' : a.kind === 'warning' ? 'var(--mustard)' : 'var(--teal)';
          return (
            <g key={a.id}>
              <circle cx={x} cy={padT - 4} r="2.6" fill={c} />
              <line x1={x} x2={x} y1={padT - 1} y2={padT + plotH} stroke={c} strokeWidth="0.5" strokeDasharray="1 2" opacity="0.4" />
            </g>
          );
        })}

        {/* active brush selection - dashed bracket with + button at top-right */}
        {brushRect && (() => {
          const dur = brushDuration(showBrush);
          return (
            <g>
              <g style={{ pointerEvents: 'none' }}>
                <rect x={brushRect.x} y={padT} width={brushRect.w} height={plotH}
                  fill="var(--wax)" fillOpacity="0.08" stroke="var(--wax)" strokeWidth="1" strokeDasharray="4 3" />
                <text x={brushRect.x + 6} y={padT - 8} fontFamily="var(--sans)" fontSize="11" fontWeight="500" fill="var(--wax)">
                  {dur}
                </text>
                <rect x={brushRect.x - 3} y={padT + plotH / 2 - 10} width="6" height="20" rx="1" fill="var(--wax)" />
                <rect x={brushRect.x + brushRect.w - 3} y={padT + plotH / 2 - 10} width="6" height="20" rx="1" fill="var(--wax)" />
              </g>
              {onPinRegion && brushRect.w > 8 && (
                <foreignObject x={brushRect.x + brushRect.w - 11} y={padT - 11} width="22" height="22" style={{ overflow: 'visible' }}>
                  <button
                    type="button"
                    aria-label="Pin region as clues"
                    onMouseDown={(e) => {
                      e.stopPropagation();
                      e.preventDefault();
                      const br = activeBrush;
                      if (br) {
                        onPinRegion([Math.min(br[0], br[1]), Math.max(br[0], br[1])]);
                        setLocalBrush(null);
                        if (onBrushChange) onBrushChange(null);
                      }
                    }}
                    style={{
                      width: 22, height: 22, borderRadius: '50%',
                      border: 'none', padding: 0,
                      background: 'var(--wax)',
                      color: 'var(--paper)',
                      cursor: 'pointer',
                      display: 'flex', alignItems: 'center', justifyContent: 'center',
                      font: '600 14px/1 var(--sans)',
                      lineHeight: 1,
                    }}
                  >+</button>
                </foreignObject>
              )}
            </g>
          );
        })()}

        {/* events on the timeline */}
        {showEvents && mode !== 'lanes' && EVENTS.map((e, i) => {
          const x = dayToX(e.day, padL, plotW, days);
          const c = e.tone === 'brick' ? 'var(--brick)' : e.tone === 'mustard' ? 'var(--mustard)' : 'var(--teal)';
          const isHov = hovered === i;
          return (
            <g key={i}
              onMouseEnter={(ev) => { ev.stopPropagation(); setHovered(i); }}
              onMouseLeave={() => setHovered(h => h === i ? null : h)}
              style={{ cursor: 'pointer' }}>
              <circle cx={x} cy={padT + plotH - 4} r="10" fill="transparent" />
              <circle cx={x} cy={padT + plotH - 4} r={isHov ? 4.5 : 3} fill={c} stroke="var(--paper)" strokeWidth="1.4" />
            </g>
          );
        })}

        {/* external hoverDay marker */}
        {hoverDay != null && (
          <g style={{ pointerEvents: 'none' }}>
            <line x1={dayToX(hoverDay, padL, plotW, days)} x2={dayToX(hoverDay, padL, plotW, days)} y1={padT} y2={padT + plotH}
              stroke="var(--ink)" strokeWidth="0.6" strokeDasharray="2 2" />
          </g>
        )}

        {/* axis labels */}
        {!hideAxis && dayTicks.map((t, i) => (
          <text key={i} x={t.x} y={height - 8} fontFamily="var(--mono)" fontSize="9.5" fill="var(--ink-mute)" textAnchor="middle">
            {t.label}
          </text>
        ))}
        {!hideAxis && [0.5, 1, 1.5].map((v, i) => (
          <text key={i} x={padL - 6} y={yOf(v) + 3} fontFamily="var(--mono)" fontSize="9" fill="var(--ink-mute)" textAnchor="end">
            {Math.round(v * 1000)}
          </text>
        ))}

      </svg>

      {/* event tooltip on hover */}
      {showEvents && mode !== 'lanes' && hovered != null && (() => {
        const e = EVENTS[hovered];
        const x = dayToX(e.day, padL, plotW, days);
        const tone = e.tone === 'brick' ? 'var(--brick)' : e.tone === 'mustard' ? 'var(--mustard)' : 'var(--teal)';
        const flipLeft = x > padL + plotW * 0.7;
        return (
          <div style={{
            position: 'absolute',
            left: flipLeft ? undefined : x + 10,
            right: flipLeft ? (W - x + 10) : undefined,
            top: padT + plotH - 46,
            background: 'var(--paper)',
            border: '1px solid var(--rule)',
            padding: '6px 9px',
            font: '500 11px/1.3 var(--mono)',
            color: 'var(--ink)',
            whiteSpace: 'nowrap',
            pointerEvents: 'none',
            zIndex: 2,
          }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, marginBottom: 3 }}>
              <span style={{ color: tone, fontSize: 10, letterSpacing: '0.04em' }}>{e.t} UTC - {e.date}</span>
              <span style={{ color: 'var(--ink-mute)', fontSize: 9.5 }}>{e.mitre}</span>
            </div>
            <div style={{ color: 'var(--ink)', font: '500 12px/1.2 var(--sans)' }}>{e.label}</div>
          </div>
        );
      })()}


      {/* caption */}
      <div style={{ position: 'absolute', left: padL, top: 2, font: '400 10px/1 var(--sans)', color: 'var(--ink-mute)', pointerEvents: 'none' }}>
        {caption}
      </div>
    </div>
  );
}

function brushDuration([a, b]) {
  const lo = Math.min(a, b), hi = Math.max(a, b);
  const hours = (hi - lo) * 24;
  if (hours < 1) return `${Math.round(hours * 60)}m selected`;
  if (hours < 24) return `${hours.toFixed(1)}h selected`;
  return `${(hours / 24).toFixed(1)}d selected`;
}

Object.assign(window, { Timeline, ANOMALIES, EVENTS });
