// forecast-chart.jsx — ForecastChart
// Chunk 5: custom inline SVG chart, no charting library

const _CM = { top: 16, right: 16, bottom: 60, left: 48 };

function ForecastChart({
  series: seriesIn   = [],
  labels             = [],
  sublabels          = [],
  tooltipLabels      = [],
  grouplabels        = [],
  grades             = [],
  height             = 280,
  highlightIndex     = null,
  onHover,
  chartStyle         = 'line',
  nonOperating       = [],
  nonOperatingLabels = {},
  holidays           = {},
  quickEditMode      = false,
  qeHint             = false,
  clickableIndices   = null,
  within7Indices     = null,
  currentWeekIndices = null,
  lockedIndices      = null,
  draftIndices       = null,
  selectedEditIndex  = null,
  topSeriesKey       = null,
  tetherYours        = false,
  onColumnClick,
}) {
  const wrapRef = React.useRef(null);
  const [svgW, setSvgW]         = React.useState(600);
  const [hoverIdx, setHoverIdx] = React.useState(null);
  const [mouseY, setMouseY]     = React.useState(0);

  // Responsive width
  React.useEffect(() => {
    const el = wrapRef.current;
    if (!el) return;
    const ro = new ResizeObserver(([e]) => {
      const w = e.contentRect.width;
      if (w > 0) setSvgW(w);
    });
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  const activeIdx = highlightIndex ?? hoverIdx;

  // Holidays — closed days, marked on the x-axis rather than as a column wash.
  // The plot already spends four full-column tints (quick-edit violet, within-7
  // amber, locked grey, grade bands), and at trend density (~40 columns, ~20px
  // each) a fifth one plus centred text is unreadable. So the marker lives in
  // the axis label stack, backed by a thin dashed rule; the series are cut at
  // the column so a closed day never drags a line down to nothing.
  const holidayIdxs = Object.keys(holidays).map(Number);
  const isHoliday   = i => holidays[i] != null;
  const series      = holidayIdxs.length
    ? seriesIn.map(s => ({ ...s, values: s.values.map((v, i) => (isHoliday(i) ? null : v)) }))
    : seriesIn;

  // Geometry — widen the right gutter when a grade axis is present so its
  // 0–10 tick labels have room without shrinking charts that have no grades.
  const hasGrades = grades.some(g => g != null);
  const M    = hasGrades ? { ..._CM, right: 40 } : _CM;
  const cw   = svgW - M.left - M.right;
  const ch   = height - M.top - M.bottom;
  const n    = Math.max(labels.length, 1);
  const colW = cw / n;
  const xC   = i => M.left + (i + 0.5) * colW;

  // Secondary grade scale (right axis): fixed 0–10, even ticks — one slim bar
  // per graded week. Weeks are the contiguous column runs delimited by the
  // group labels (each non-empty grouplabel starts a new week).
  const GRADE_MAX  = 10;
  const gradeTicks = [0, 2, 4, 6, 8, 10];
  // Smallest band a graded week may render at, so grade 0 never disappears.
  const GRADE_BAND_MIN = 12;
  const gY = g => M.top + ch - (g / GRADE_MAX) * ch;
  const weekGroups = [];
  if (hasGrades) {
    labels.forEach((_, i) => {
      if (i === 0 || grouplabels[i]) weekGroups.push({ start: i, end: i });
      else weekGroups[weekGroups.length - 1].end = i;
    });
  }

  // Y scale: nice round step so every tick is a clean number.
  // Normal data (max ≥ 200) uses the hundreds ladder; small data (max < 200)
  // uses a fine ladder so low weeks aren't stranded on a giant axis. Pick the
  // smallest step that fits; always 5 steps + ~20% headroom.
  const FINE_STEPS = [10, 25, 50];
  const BIG_STEPS  = [100, 200, 300, 500, 750, 1000, 2000, 5000];
  const flat   = series.flatMap(s => s.values.filter(v => v != null));
  const rawMax = flat.length ? Math.max(...flat) : 100;
  const rawStep = (rawMax * 1.2) / 5 || 10;      // per-step target incl. headroom
  const niceStep = (() => {
    const ladder = rawMax < 200 ? FINE_STEPS : BIG_STEPS;
    const hit = ladder.find(s => s >= rawStep);
    if (hit) return hit;
    // Fallback for data beyond the ladder: nearest nice 1/2/5 × 10^k
    const pow  = Math.pow(10, Math.floor(Math.log10(rawStep)));
    const frac = rawStep / pow;
    const nice = frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 5 ? 5 : 10;
    return nice * pow;
  })();
  const yMax = niceStep * 5;
  const yC   = v => M.top + ch - (v / yMax) * ch;

  // 6 gridline ticks (0 → yMax in 5 nice steps)
  const ticks = Array.from({ length: 6 }, (_, i) => ({
    v: niceStep * i,
    y: yC(niceStep * i),
  }));

  // Series render order — topSeriesKey is drawn last (on top)
  const renderSeries = (() => {
    const withIdx = series.map((s, si) => ({ ...s, si }));
    if (!topSeriesKey) return withIdx;
    const idx = withIdx.findIndex(s => s.key === topSeriesKey);
    if (idx === -1 || idx === withIdx.length - 1) return withIdx;
    return [...withIdx.slice(0, idx), ...withIdx.slice(idx + 1), withIdx[idx]];
  })();

  // SVG path builders
  // A null lifts the pen rather than bridging the gap — otherwise a closed day
  // would be papered over by a straight line drawn across it.
  const linePath = values => {
    let d = '', pen = false;
    values.forEach((v, i) => {
      if (v == null) { pen = false; return; }
      d += `${pen ? ' L' : ' M'} ${xC(i)} ${yC(v)}`;
      pen = true;
    });
    return d.trim();
  };

  const areaPath = values => {
    const pts = values.reduce((acc, v, i) => {
      if (v != null) acc.push([xC(i), yC(v)]);
      return acc;
    }, []);
    if (!pts.length) return '';
    const line = pts.map(([x, y], i) => `${i ? 'L' : 'M'} ${x} ${y}`).join(' ');
    return `${line} L ${pts[pts.length - 1][0]} ${yC(0)} L ${pts[0][0]} ${yC(0)} Z`;
  };

  // Baseline for the "yours" series. Your forecast is a sparse set of overrides
  // (null on days you didn't touch), so rather than draw a line across the gaps
  // we tether each edited point to the Trunkrs value for that day.
  const baselineValues = series.find(s => s.key === 'trk')?.values || [];

  // Interaction
  const handleMove = e => {
    const rect = wrapRef.current.getBoundingClientRect();
    const mx   = e.clientX - rect.left - M.left;
    const i    = Math.max(0, Math.min(n - 1, Math.floor(mx / colW)));
    setHoverIdx(i);
    setMouseY(e.clientY - rect.top);
    onHover?.(i);
  };

  const handleLeave = () => {
    setHoverIdx(null);
    onHover?.(null);
  };

  const handleClick = e => {
    if (!quickEditMode || hoverIdx === null) return;
    if (clickableIndices && !clickableIndices.has(hoverIdx)) return;
    // Report the column's on-screen anchor so the inline editor can attach to
    // the column itself rather than the cursor.
    const rect = wrapRef.current.getBoundingClientRect();
    onColumnClick?.(hoverIdx, {
      cx:      rect.left + xC(hoverIdx),
      plotTop: rect.top + M.top,
      colW,
    });
  };

  function getTipLabel(i) {
    if (tooltipLabels.length && tooltipLabels[i]) return tooltipLabels[i];
    var day = sublabels[i] ? ' ' + sublabels[i] : '';
    return (labels[i] || '') + day;
  }

  const isClickable = quickEditMode && activeIdx !== null && clickableIndices?.has(activeIdx);

  return (
    <div
      ref={wrapRef}
      className="chart-wrap"
      style={{ height, cursor: isClickable ? 'pointer' : 'default' }}
      onMouseMove={handleMove}
      onMouseLeave={handleLeave}
      onClick={handleClick}
    >
      <svg width={svgW} height={height} className="chart-svg">

        {/* Non-operating column fills */}
        {nonOperating.map(i => (
          <g key={i}>
            <rect
              x={M.left + i * colW} y={M.top}
              width={colW} height={ch}
              fill="#E0E0E0" fillOpacity={0.4}
            />
            {nonOperatingLabels[i] && (
              <text
                x={xC(i)} y={M.top + 9}
                textAnchor="middle"
                fontSize={12} fill="#B8B8B8"
                fontFamily="inherit"
                style={{ letterSpacing: '0.06em' }}
              >
                {nonOperatingLabels[i].toUpperCase()}
              </text>
            )}
          </g>
        ))}

        {/* Quick-edit: persistent affordance on every editable column */}
        {quickEditMode && clickableIndices && [...clickableIndices].map(i => (
          <g key={`qe-col-${i}`} pointerEvents="none" className={`qe-col${qeHint ? ' pulse' : ''}`}>
            <rect
              x={M.left + i * colW + 1} y={M.top}
              width={colW - 2} height={ch}
              fill="rgba(134,100,255,0.06)" rx={3}
            />
            <rect
              x={xC(i) - 9} y={M.top + 4}
              width={18} height={4} rx={2}
              fill="#8664FF" fillOpacity={0.55}
            />
          </g>
        ))}

        {/* Quick-edit: the column currently being edited inline */}
        {quickEditMode && selectedEditIndex != null && (
          <rect
            x={M.left + selectedEditIndex * colW + 1} y={M.top}
            width={colW - 2} height={ch}
            fill="rgba(134,100,255,0.12)" stroke="#8664FF" strokeWidth={1.5} rx={4}
            pointerEvents="none"
          />
        )}

        {/* Quick-edit: within-7-days amber column tints */}
        {quickEditMode && within7Indices && [...within7Indices].map(i => (
          <rect key={`w7-${i}`}
            x={M.left + i * colW} y={M.top}
            width={colW} height={ch}
            fill="#FFD65A" fillOpacity={0.14}
            pointerEvents="none"
          />
        ))}

        {/* Quick-edit: locked near-term days (committed to trucks/drivers) —
            not editable, mirroring the review form's near-term lock. */}
        {quickEditMode && lockedIndices && [...lockedIndices].map(i => (
          <g key={`lock-${i}`} pointerEvents="none">
            <rect
              x={M.left + i * colW + 1} y={M.top}
              width={colW - 2} height={ch}
              fill="rgba(34,12,74,0.04)" rx={3}
            />
            <svg x={xC(i) - 7} y={M.top + 3} width={14} height={14} viewBox="0 0 24 24">
              <path
                fillRule="evenodd" clipRule="evenodd"
                d="M15 8V11H9V8C9 6.34315 10.3431 5 12 5C13.6569 5 15 6.34315 15 8ZM7 11V8C7 5.23858 9.23858 3 12 3C14.7614 3 17 5.23858 17 8V11H19V22H5V11H7ZM17 13H7V20H17V13Z"
                fill="#B8B8B8"
              />
            </svg>
          </g>
        ))}

        {/* Gridlines + Y labels */}
        {ticks.map(({ v, y }) => (
          <g key={v}>
            <line
              x1={M.left} y1={y} x2={svgW - M.right} y2={y}
              stroke="#E0E0E0" strokeWidth={1}
            />
            <text
              x={M.left - 6} y={y + 4}
              textAnchor="end"
              fontSize={12} fill="#999" fontFamily="inherit"
            >
              {v}
            </text>
          </g>
        ))}

        {/* Weekly grade bands + right axis (secondary 0–10 scale). One faint,
            band-coloured region per week (green ≥8 / amber 6 / red below),
            height on the grade scale; faint at rest, hovering a day lifts that
            week's band. Bands are floored at GRADE_BAND_MIN so a 0 still draws.
            The number itself sits in a pill on the week label row (outside the
            plot, so it obscures nothing) and in the tooltip. Behind the lines. */}
        {hasGrades && (
          <g className="grade-layer" pointerEvents="none">
            {gradeTicks.map(g => (
              <text key={`gt-${g}`}
                x={svgW - M.right + 6} y={gY(g) + 4}
                textAnchor="start"
                fontSize={11} fill="#B8B8B8" fontFamily="inherit"
              >
                {g}
              </text>
            ))}
            {weekGroups.map((grp, gi) => {
              const g = grades[grp.start];
              if (g == null) return null;
              // Near-full-width band with a 1px breather each side so adjacent
              // same-grade weeks still read as separate.
              const x0     = M.left + grp.start * colW + 1;
              const w      = (grp.end - grp.start + 1) * colW - 2;
              // Floor the band: a grade of 0 is scale-correct at zero height,
              // but drawing nothing makes the worst possible week identical to
              // a week that simply hasn't been graded yet. Every graded week
              // keeps a visible sill, and 0 is saturated so it reads as
              // bottomed-out rather than as a small score.
              const yTop   = Math.min(gY(g), M.top + ch - GRADE_BAND_MIN);
              const active = activeIdx != null && activeIdx >= grp.start && activeIdx <= grp.end;
              const band   = g >= 8 ? '#1ED771' : g === 6 ? '#FFD040' : '#FF3A37';
              const op     = g === 0 ? (active ? 0.50 : 0.35) : (active ? 0.22 : 0.09);
              return (
                <rect key={`gb-${gi}`}
                  x={x0} y={yTop}
                  width={w} height={(M.top + ch) - yTop}
                  rx={3}
                  fill={band}
                  fillOpacity={op}
                />
              );
            })}
          </g>
        )}

        {/* Holiday rules — one thin dashed line per closed day. Drawn after the
            grade layer so it stays legible over a coloured band, and kept to a
            hairline so it reads as an annotation, not another column mode. */}
        {holidayIdxs.map(i => (
          <line key={`hol-${i}`}
            x1={xC(i)} y1={M.top} x2={xC(i)} y2={M.top + ch}
            stroke="#FFC927" strokeWidth={1} strokeDasharray="3 3"
            pointerEvents="none"
          />
        ))}

        {/* Bars (bars-line: first series rendered as bars) */}
        {chartStyle === 'bars-line' && series[0]?.values.map((v, i) => {
          if (v == null) return null;
          const bw = colW * 0.55;
          return (
            <rect
              key={i}
              x={xC(i) - bw / 2} y={yC(v)}
              width={bw} height={(v / yMax) * ch}
              fill={series[0].color}
              fillOpacity={activeIdx === i ? 0.85 : 0.55}
              rx={2}
            />
          );
        })}

        {/* Lines */}
        {renderSeries.map((s) => {
          if (chartStyle === 'bars-line' && s.si === 0) return null;
          const opacity = (quickEditMode && s.key !== 'yours') ? 0.2
            : (topSeriesKey && s.key !== topSeriesKey) ? 0.3
            : 1;

          if (s.key === 'yours' && (tetherYours || quickEditMode || draftIndices?.size > 0)) {
            // "Your forecast" is a sparse set of overrides (null on untouched
            // days, which plot nothing). Adjacent edited days are joined with a
            // line (a run reads as a trend); an isolated edited day is instead
            // tethered to the Trunkrs baseline so a lone point reads as a
            // deviation rather than an orphan — never a line across a gap. This
            // single path covers both the static view and active quick-edit;
            // during quick-edit the pending (draft) days render thin/dashed.
            const has = i => i >= 0 && i < s.values.length && s.values[i] != null;
            const isDraftDay = i => quickEditMode && !!(draftIndices?.has(i));
            const links = [];   // i → i+1 line segments within a run
            s.values.forEach((v, i) => { if (v != null && has(i + 1)) links.push(i); });
            return (
              <g key={s.si} opacity={opacity}>
                {links.map(i => {
                  const draft = isDraftDay(i) || isDraftDay(i + 1);
                  return (
                    <line
                      key={`link-${i}`}
                      x1={xC(i)} y1={yC(s.values[i])}
                      x2={xC(i + 1)} y2={yC(s.values[i + 1])}
                      stroke={s.color}
                      strokeWidth={draft ? 1.5 : 2.5}
                      strokeDasharray={draft ? '1.5 3' : undefined}
                      strokeLinecap="round" strokeLinejoin="round"
                    />
                  );
                })}
                {s.values.map((v, i) => {
                  if (v == null || has(i - 1) || has(i + 1)) return null;   // in a run → no tether
                  const base = baselineValues[i];
                  if (base == null || base === v) return null;
                  return (
                    <line
                      key={`tether-${i}`}
                      x1={xC(i)} y1={yC(base)}
                      x2={xC(i)} y2={yC(v)}
                      stroke={s.color} strokeOpacity={0.5}
                      strokeWidth={2} strokeDasharray="2 3"
                      strokeLinecap="round"
                    />
                  );
                })}
              </g>
            );
          }

          const d = linePath(s.values);
          if (!d) return null;
          return (
            <path
              key={s.si} d={d} fill="none"
              stroke={s.color} strokeWidth={2.5}
              strokeLinecap="round" strokeLinejoin="round"
              strokeDasharray={s.style === 'dashed' ? '5 5' : undefined}
              opacity={opacity}
            />
          );
        })}

        {/* Quick-edit: highlight rect for hovered clickable columns */}
        {quickEditMode && activeIdx !== null && clickableIndices?.has(activeIdx) && (
          <rect
            x={M.left + activeIdx * colW} y={M.top}
            width={colW} height={ch}
            fill="rgba(134,100,255,0.08)" rx={2}
            pointerEvents="none"
          />
        )}

        {/* Hover crosshair */}
        {activeIdx !== null && (
          <line
            x1={xC(activeIdx)} y1={M.top}
            x2={xC(activeIdx)} y2={M.top + ch}
            stroke="rgba(34,12,74,0.1)" strokeWidth={1}
          />
        )}

        {/* Dots */}
        {renderSeries.map((s) =>
          (chartStyle === 'bars-line' && s.si === 0) ? null :
          s.values.map((v, i) => {
            if (v == null) return null;
            const isDraft = s.key === 'yours' && quickEditMode && !!(draftIndices?.has(i));
            // An isolated edited day (no edited neighbour) is tethered to the
            // baseline, so give it a slightly larger white-ringed dot to read as a
            // deliberate marker. Run members sit on their connecting line and keep
            // the regular dot.
            const isTetherMarker = s.key === 'yours' && !isDraft && tetherYours
              && s.values[i - 1] == null && s.values[i + 1] == null;
            const r       = isTetherMarker ? (activeIdx === i ? 6 : 5) : (activeIdx === i ? 5 : 3.5);
            const opacity = (quickEditMode && s.key !== 'yours') ? 0.2
            : (topSeriesKey && s.key !== topSeriesKey) ? 0.3
            : 1;
            return isDraft ? (
              <circle key={`${s.si}-${i}`} cx={xC(i)} cy={yC(v)} r={r}
                fill="#fff" stroke={s.color} strokeWidth={2}
                opacity={opacity}
                style={{ transition: 'r 120ms ease' }}
              />
            ) : isTetherMarker ? (
              <circle key={`${s.si}-${i}`} cx={xC(i)} cy={yC(v)} r={r}
                fill={s.color} stroke="#fff" strokeWidth={2}
                opacity={opacity}
                style={{ transition: 'r 120ms ease' }}
              />
            ) : (
              <circle key={`${s.si}-${i}`} cx={xC(i)} cy={yC(v)} r={r}
                fill={s.color} opacity={opacity}
                style={{ transition: 'r 120ms ease' }}
              />
            );
          })
        )}

        {/* X-axis labels */}
        {labels.map((lbl, i) => (
          <g key={i}>
            <text
              x={xC(i)} y={M.top + ch + 18}
              textAnchor="middle"
              fontSize={12} fontWeight={700}
              fill={isHoliday(i) ? '#B8B8B8' : '#220C4A'} fontFamily="inherit"
            >
              {lbl}
            </text>
            {isHoliday(i) ? (
              // The weekday name gives way to a calendar chip, so the row you
              // already scan for "which day is this" also says it's closed. The
              // holiday's name is in the tooltip — it never fits in a column.
              <g pointerEvents="none">
                <rect
                  x={xC(i) - 9} y={M.top + ch + 22}
                  width={18} height={15} rx={4}
                  fill="#FFF5D6" stroke="#FFE38D" strokeWidth={1}
                />
                <svg x={xC(i) - 5.5} y={M.top + ch + 24} width={11} height={11} viewBox="0 0 24 24">
                  <path
                    fillRule="evenodd" clipRule="evenodd"
                    d="M7 8V6H4V9L20 9V6H17V8H15V6H9V8H7ZM15 4H9V2H7V4H2V9V11V21H22V11V9V4H17V2H15V4ZM20 11V19H4V11L20 11Z"
                    fill="#403516"
                  />
                </svg>
              </g>
            ) : sublabels[i] && (
              <text
                x={xC(i)} y={M.top + ch + 32}
                textAnchor="middle"
                fontSize={12} fill="#999" fontFamily="inherit"
              >
                {sublabels[i]}
              </text>
            )}
            {grouplabels[i] && (() => {
              // The current week's group starts on one of its own columns, so a
              // group-start index that's in currentWeekIndices marks "this week".
              const isCurrent = currentWeekIndices && currentWeekIndices.has(i);
              const gy  = M.top + ch + 54;
              // The week's grade is repeated across its days, so the value on
              // the group-start column is the week's. Showing the number here
              // is what actually separates a 0 from an ungraded week — a band
              // alone can only ever say "low", never "zero".
              const wg  = hasGrades ? grades[i] : null;
              const txt = isCurrent ? `${grouplabels[i]} · this week` : grouplabels[i];
              // Label and grade pill are laid out as one unit and centred
              // together, so adding the pill doesn't shift the week label off
              // its column.
              const textW = txt.length * 6.1;
              const pillW = wg == null ? 0 : (wg === 10 ? 22 : 17);
              const gap   = wg == null ? 0 : 5;
              const left  = xC(i) - (textW + gap + pillW) / 2;
              const tx    = left + textW / 2;
              const px    = left + textW + gap;
              // Grade 0 is the one case that gets a solid fill and white text —
              // every other grade is a quiet tinted pill.
              const zero  = wg === 0;
              const pill  = wg == null ? null
                : zero          ? { bg: '#FF3A37', fg: '#FFFFFF' }
                : wg >= 8       ? { bg: '#CAF8DF', fg: '#0B3820' }
                : wg === 6      ? { bg: '#FFF5D6', fg: '#403516' }
                :                 { bg: '#FFD4D3', fg: '#401514' };
              return (
                <g>
                  {isCurrent && (
                    <rect
                      x={left - 8} y={gy - 12}
                      width={textW + 16} height={17} rx={8.5}
                      fill="rgba(34,12,74,0.07)"
                    />
                  )}
                  <text
                    x={tx} y={gy}
                    textAnchor="middle"
                    fontSize={11} fontWeight={isCurrent ? 700 : 400}
                    fill={isCurrent ? '#220C4A' : '#B8B8B8'} fontFamily="inherit"
                    style={{ letterSpacing: isCurrent ? '0.03em' : '0.04em' }}
                  >
                    {txt}
                  </text>
                  {pill && (
                    <g>
                      <rect
                        x={px} y={gy - 12}
                        width={pillW} height={17} rx={8.5}
                        fill={pill.bg}
                      />
                      <text
                        x={px + pillW / 2} y={gy}
                        textAnchor="middle"
                        fontSize={11} fontWeight={700} fill={pill.fg} fontFamily="inherit"
                      >
                        {wg}
                      </text>
                    </g>
                  )}
                </g>
              );
            })()}
          </g>
        ))}
      </svg>

      {/* Tooltip — suppressed in quick-edit mode (popover takes over) */}
      {hoverIdx !== null && !quickEditMode && (
        <div
          className="chart-tooltip is-visible"
          style={{ left: xC(hoverIdx), top: mouseY }}
        >
          <div className="tip-date">{getTipLabel(hoverIdx)}</div>
          {isHoliday(hoverIdx) ? (
            <div className="tip-holiday">
              <span className="tip-holiday-name">{holidays[hoverIdx]}</span>
              <span>No delivery — the volume moves to the surrounding days.</span>
            </div>
          ) : (
            <React.Fragment>
              {series.map((s, i) => {
                const v = s.values[hoverIdx];
                if (v == null) return null;
                return (
                  <div key={i} className="tip-row">
                    <span className="lab">{s.label}</span>
                    <span className="val">{v.toLocaleString()}</span>
                  </div>
                );
              })}
              {hasGrades && grades[hoverIdx] != null && (
                <div className="tip-row tip-grade-row">
                  <span className="lab">Week grade</span>
                  <span className={`val tip-grade tip-grade-${grades[hoverIdx] >= 8 ? 'ok' : grades[hoverIdx] === 6 ? 'warn' : 'bad'}`}>
                    {grades[hoverIdx]}
                  </span>
                </div>
              )}
            </React.Fragment>
          )}
        </div>
      )}
    </div>
  );
}
