import { useState, useMemo, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import {
  AlertTriangle, TrendingDown, ShoppingCart, Check, BellOff,
  ChevronDown, ChevronUp, Info, Edit2, X, Lock,
  ArrowUp, ArrowDown, ArrowUpDown, Package, Trash2, BarChart2,
  CreditCard, PackageCheck, Download, RotateCcw,
} from 'lucide-react';
import {
  AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip,
  ResponsiveContainer, ReferenceLine, Legend,
} from 'recharts';
import { api } from '../api/client';
import type { InventorySpool, SpoolUsageRecord, FilamentSkuSettings, ShoppingListItem } from '../api/client';
import { getSwatchStyle } from '../utils/colors';
import { useToast } from '../contexts/ToastContext';
import { useAuth } from '../contexts/AuthContext';

// ── Types ─────────────────────────────────────────────────────────────────────

interface SkuGroup {
  key: string;
  material: string;
  subtype: string | null;
  brand: string | null;
  spools: InventorySpool[];
}

interface SkuForecast {
  group: SkuGroup;
  settings: FilamentSkuSettings | null;
  totalRemainingG: number;
  totalLabelG: number;
  totalSpools: number;
  totalUsedG: number;
  dailyRateG: number | null;
  dailyRateStdDev: number | null;
  rateTier: 'history' | 'delta' | 'none';
  effectiveLeadTimeDays: number;
  safetyStockG: number;
  reorderPointG: number;
  daysRemaining: number | null;
  daysUntilROP: number | null;
  projectedEmptyDate: Date | null;
  reorderTriggerDate: Date | null;
  reorderAlert: boolean;
  stockBreakAlert: boolean;
}

type SortKey = 'material' | 'used' | 'days_left' | 'stock';
type SortDir = 'asc' | 'desc';
type ChartDays = 7 | 30 | 180;

// ── Constants ─────────────────────────────────────────────────────────────────

const Z_95 = 1.65;
const CHART_COLORS = ['#1DB954', '#3B82F6', '#F59E0B', '#EF4444', '#8B5CF6'];

// ── Pure helpers ──────────────────────────────────────────────────────────────

function skuKey(material: string, subtype: string | null, brand: string | null) {
  return `${material}||${subtype ?? ''}||${brand ?? ''}`;
}

function addDays(date: Date, days: number): Date {
  const d = new Date(date);
  d.setDate(d.getDate() + Math.round(days));
  return d;
}

function formatDate(date: Date): string {
  return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
}

function formatDateShort(date: Date): string {
  return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
}

/**
 * Compute a time-weighted daily consumption rate and standard deviation.
 *
 * Algorithm:
 *   1. Sort all usage events by timestamp (oldest → newest).
 *   2. Convert each event into a g/day intensity = weight_used / elapsed_days,
 *      where elapsed_days is the gap to the previous event (floor: 0.5d to
 *      avoid inflated rates from same-day prints).
 *   3. Apply exponential age-decay: each observation is weighted by
 *      exp(-λ * age_days) so recent prints dominate. λ = ln(2)/30 gives a
 *      30-day half-life — prints from a month ago count half as much.
 *   4. Compute the weighted mean and weighted variance → std dev.
 *
 * Returns null when there is only one event (no gap to measure) — the
 * delta-rate fallback handles that case.
 */
function computeHistoryRate(records: SpoolUsageRecord[]): { rate: number; stdDev: number } | null {
  if (records.length < 2) return null;

  // Sort ascending by time
  const sorted = [...records].sort(
    (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime(),
  );

  const now = Date.now();
  // λ for 30-day half-life: ln(2)/30
  const lambda = Math.LN2 / 30;

  const observations: { rate: number; weight: number }[] = [];

  for (let i = 1; i < sorted.length; i++) {
    const prev = new Date(sorted[i - 1].created_at).getTime();
    const curr = new Date(sorted[i].created_at).getTime();
    const elapsedDays = Math.max((curr - prev) / 86400000, 0.5); // floor at 0.5d
    const ageDays = (now - curr) / 86400000;

    // g/day for this interval
    const intervalRate = sorted[i].weight_used / elapsedDays;
    // Exponential age-decay weight
    const w = Math.exp(-lambda * ageDays);

    observations.push({ rate: intervalRate, weight: w });
  }

  const totalW = observations.reduce((s, o) => s + o.weight, 0);
  if (totalW === 0) return null;

  const mean = observations.reduce((s, o) => s + o.rate * o.weight, 0) / totalW;
  const variance = observations.reduce((s, o) => s + o.weight * (o.rate - mean) ** 2, 0) / totalW;

  return { rate: mean, stdDev: Math.sqrt(variance) };
}

function computeDeltaRate(spools: InventorySpool[]): number | null {
  // Use weight_used - baseline so "Reset usage to 0" on the Inventory page
  // makes forecast restart from zero rather than carrying stale lifetime
  // consumption across the reset (#1390).
  const totalUsed = spools.reduce((s, sp) => s + Math.max(0, sp.weight_used - (sp.weight_used_baseline ?? 0)), 0);
  if (totalUsed === 0) return null;
  const now = Date.now();
  const oldestMs = spools.reduce((min, sp) => {
    const t = new Date(sp.created_at).getTime();
    return t < min ? t : min;
  }, now);
  const daysSinceOldest = (now - oldestMs) / 86400000;
  if (daysSinceOldest < 1) return null;
  return totalUsed / daysSinceOldest;
}

function buildProjectionSeries(
  forecast: SkuForecast,
  days = 60,
): { day: number; label: string; stock: number; rop: number }[] {
  if (forecast.dailyRateG === null) return [];
  const rate = forecast.dailyRateG;
  const result = [];
  for (let d = 0; d <= days; d++) {
    const stock = Math.max(0, forecast.totalRemainingG - rate * d);
    result.push({
      day: d,
      label: formatDateShort(addDays(new Date(), d)),
      stock: Math.round(stock),
      rop: Math.round(forecast.reorderPointG),
    });
    if (stock === 0) break;
  }
  return result;
}

// ── Main component ────────────────────────────────────────────────────────────

export function ForecastPanel({ spools }: { spools: InventorySpool[] }) {
  const queryClient = useQueryClient();
  const { showToast } = useToast();
  const { t } = useTranslation();
  const { hasPermission, hasAnyPermission } = useAuth();

  const canRead = hasPermission('inventory:forecast_read');
  const canWrite = hasAnyPermission('inventory:forecast_write', 'inventory:update');

  // All hooks must run unconditionally — guard render is deferred until after hooks
  const [alertsOpen, setAlertsOpen] = useState(false);
  const [sortKey, setSortKey] = useState<SortKey>('material');
  const [sortDir, setSortDir] = useState<SortDir>('asc');
  const [cartModal, setCartModal] = useState<SkuForecast | null>(null);
  const [listOpen, setListOpen] = useState(false);
  const [chartDays, setChartDays] = useState<ChartDays>(30);

  const { data: settings } = useQuery({ queryKey: ['settings'], queryFn: api.getSettings, enabled: canRead });
  const { data: skuSettingsList = [] } = useQuery({ queryKey: ['sku-settings'], queryFn: api.getSkuSettings, staleTime: 60_000, enabled: canRead });
  const { data: usageHistory = [] } = useQuery({ queryKey: ['all-usage-history-forecast'], queryFn: () => api.getAllUsageHistory(5000), staleTime: 60_000, enabled: canRead });
  const { data: shoppingList = [] } = useQuery({ queryKey: ['shopping-list'], queryFn: api.getShoppingList, staleTime: 30_000, enabled: canRead });

  const globalLeadTime = settings?.forecast_global_lead_time_days ?? 0;

  const settingsMap = useMemo(() => {
    const m = new Map<string, FilamentSkuSettings>();
    for (const s of skuSettingsList) m.set(skuKey(s.material, s.subtype, s.brand), s);
    return m;
  }, [skuSettingsList]);

  const usageBySpoolId = useMemo(() => {
    const m = new Map<number, SpoolUsageRecord[]>();
    for (const r of usageHistory) {
      const arr = m.get(r.spool_id) ?? [];
      arr.push(r);
      m.set(r.spool_id, arr);
    }
    return m;
  }, [usageHistory]);

  const groups = useMemo((): SkuGroup[] => {
    const map = new Map<string, SkuGroup>();
    for (const spool of spools) {
      if (spool.archived_at) continue;
      const key = skuKey(spool.material, spool.subtype, spool.brand);
      const g = map.get(key) ?? { key, material: spool.material, subtype: spool.subtype, brand: spool.brand, spools: [] };
      g.spools.push(spool);
      map.set(key, g);
    }
    return [...map.values()];
  }, [spools]);

  const forecasts = useMemo((): SkuForecast[] => {
    const today = new Date(); today.setHours(0, 0, 0, 0);

    return groups.map((group): SkuForecast => {
      const skuSettings = settingsMap.get(group.key) ?? null;
      const skuLeadTime = skuSettings?.lead_time_days ?? 0;
      const effectiveLeadTimeDays = Math.max(globalLeadTime, skuLeadTime);
      const marginValue = skuSettings?.safety_margin_value ?? 14;
      const marginUnit = skuSettings?.safety_margin_unit ?? 'days';

      const totalRemainingG = group.spools.reduce((s, sp) => s + Math.max(0, sp.label_weight - sp.weight_used), 0);
      const totalLabelG = group.spools.reduce((s, sp) => s + sp.label_weight, 0);
      // Consumed since baseline (post-reset); see InventoryPage stats calc (#1390).
      const totalUsedG = group.spools.reduce((s, sp) => s + Math.max(0, sp.weight_used - (sp.weight_used_baseline ?? 0)), 0);

      const groupHistory: SpoolUsageRecord[] = [];
      for (const s of group.spools) groupHistory.push(...(usageBySpoolId.get(s.id) ?? []));

      let dailyRateG: number | null = null;
      let dailyRateStdDev: number | null = null;
      let rateTier: SkuForecast['rateTier'] = 'none';

      const histResult = computeHistoryRate(groupHistory);
      if (histResult !== null) {
        dailyRateG = histResult.rate;
        dailyRateStdDev = histResult.stdDev;
        rateTier = 'history';
      } else {
        const delta = computeDeltaRate(group.spools);
        if (delta !== null) { dailyRateG = delta; rateTier = 'delta'; }
      }

      const σ = dailyRateStdDev ?? (dailyRateG !== null ? dailyRateG * 0.2 : 0);
      const statisticalSafetyStockG = Z_95 * σ * Math.sqrt(effectiveLeadTimeDays);
      // safety margin: user-defined buffer on top of statistical safety stock
      const safetyMarginG = marginUnit === 'g'
        ? marginValue
        : (dailyRateG !== null ? dailyRateG * marginValue : marginValue * 5);
      const safetyStockG = statisticalSafetyStockG + safetyMarginG;
      const reorderPointG = dailyRateG !== null
        ? dailyRateG * effectiveLeadTimeDays + safetyStockG
        : 0;

      const daysRemaining = dailyRateG && dailyRateG > 0 ? Math.floor(totalRemainingG / dailyRateG) : null;
      const projectedEmptyDate = daysRemaining !== null ? addDays(today, daysRemaining) : null;

      const daysUntilROP = dailyRateG && dailyRateG > 0
        ? Math.floor((totalRemainingG - reorderPointG) / dailyRateG)
        : null;

      const reorderTriggerDate = daysUntilROP !== null ? addDays(today, Math.max(0, daysUntilROP)) : null;
      const stockBreakAlert = daysRemaining !== null && effectiveLeadTimeDays > 0 && daysRemaining <= effectiveLeadTimeDays;
      const reorderAlert = !stockBreakAlert && daysUntilROP !== null && daysUntilROP <= 0;

      return {
        group, settings: skuSettings,
        totalRemainingG, totalLabelG, totalSpools: group.spools.length, totalUsedG,
        dailyRateG, dailyRateStdDev,
        rateTier,
        effectiveLeadTimeDays, safetyStockG, reorderPointG,
        daysRemaining, daysUntilROP,
        projectedEmptyDate, reorderTriggerDate,
        reorderAlert, stockBreakAlert,
      };
    });
  }, [groups, settingsMap, usageBySpoolId, globalLeadTime]);

  const sortedForecasts = useMemo(() => {
    const arr = [...forecasts];
    arr.sort((a, b) => {
      let va: number | string = 0;
      let vb: number | string = 0;
      switch (sortKey) {
        case 'material':
          va = [a.group.material, a.group.subtype ?? '', a.group.brand ?? ''].join(' ').toLowerCase();
          vb = [b.group.material, b.group.subtype ?? '', b.group.brand ?? ''].join(' ').toLowerCase();
          break;
        case 'used':
          va = a.totalUsedG; vb = b.totalUsedG;
          break;
        case 'days_left':
          va = a.daysRemaining ?? 999999; vb = b.daysRemaining ?? 999999;
          break;
        case 'stock':
          va = a.totalRemainingG; vb = b.totalRemainingG;
          break;
      }
      const cmp = va < vb ? -1 : va > vb ? 1 : 0;
      return sortDir === 'asc' ? cmp : -cmp;
    });
    return arr;
  }, [forecasts, sortKey, sortDir]);

  const alerts = useMemo(() => forecasts.filter((f) => !f.settings?.alerts_snoozed && (f.stockBreakAlert || f.reorderAlert)), [forecasts]);

  const top5 = useMemo(() =>
    [...forecasts]
      .filter((f) => f.dailyRateG !== null)
      .sort((a, b) => b.totalUsedG - a.totalUsedG)
      .slice(0, 5),
    [forecasts]
  );

  // ── Read permission guard — all hooks above this point ──────────────────────
  if (!canRead) {
    return (
      <div className="flex flex-col items-center justify-center py-16 text-bambu-gray gap-3">
        <Lock className="w-8 h-8 opacity-40" />
        <p className="text-sm">{t('forecast.noReadAccess')}</p>
      </div>
    );
  }

  function handleSort(key: SortKey) {
    if (sortKey === key) setSortDir((d) => d === 'asc' ? 'desc' : 'asc');
    else { setSortKey(key); setSortDir(key === 'days_left' ? 'asc' : 'desc'); }
  }

  const shoppingListBadge = shoppingList.length > 0 ? shoppingList.length : null;

  return (
    <div className="space-y-5">

      {/* ── Toolbar ── */}
      <div className="flex flex-wrap items-center gap-3">
        {/* Alert button */}
        {alerts.length > 0 && (
          <button
            onClick={() => setAlertsOpen((o) => !o)}
            className={`flex items-center gap-2 px-3 py-1.5 rounded-lg border text-sm font-medium transition-colors ${
              alerts.some((f) => f.stockBreakAlert)
                ? 'bg-red-500/15 border-red-500/30 text-red-300 hover:bg-red-500/25'
                : 'bg-yellow-500/15 border-yellow-500/30 text-yellow-300 hover:bg-yellow-500/25'
            }`}
          >
            <AlertTriangle className="w-4 h-4" />
            {t('forecast.alertCount', { count: alerts.length })}
            {alertsOpen ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
          </button>
        )}

        {/* Global lead time */}
        {canWrite && (
          <GlobalLeadTimeSetting
            value={globalLeadTime}
            onSave={(v) => {
              api.updateSettings({ forecast_global_lead_time_days: v }).then(() => {
                queryClient.invalidateQueries({ queryKey: ['settings'] });
                showToast(t('forecast.globalLeadTimeSaved'), 'success');
              });
            }}
          />
        )}

        {/* Shopping list toggle */}
        <button
          onClick={() => setListOpen((o) => !o)}
          className="relative flex items-center gap-2 px-3 py-1.5 rounded-lg border border-bambu-dark-tertiary text-bambu-gray hover:bg-bambu-dark-tertiary text-sm transition-colors ml-auto"
        >
          <ShoppingCart className="w-4 h-4" />
          <span className="hidden sm:inline">{t('forecast.shoppingList')}</span>
          {shoppingListBadge && (
            <span className="absolute -top-1.5 -right-1.5 w-4 h-4 rounded-full bg-bambu-green text-white text-[10px] font-bold flex items-center justify-center">
              {shoppingListBadge}
            </span>
          )}
        </button>
      </div>

      {/* ── Collapsed alerts panel ── */}
      {alertsOpen && alerts.length > 0 && (
        <div className="space-y-2">
          {alerts.map((f) => (
            <AlertBanner key={f.group.key} forecast={f} onCart={() => setCartModal(f)} />
          ))}
        </div>
      )}

      {/* ── Shopping list panel ── */}
      {listOpen && (
        <ShoppingListPanel
          items={shoppingList}
          forecasts={forecasts}
          globalLeadTime={globalLeadTime}
          canWrite={canWrite}
          onClose={() => setListOpen(false)}
          onRemove={(id) => {
            api.removeFromShoppingList(id)
              .then(() => queryClient.invalidateQueries({ queryKey: ['shopping-list'] }))
              .catch(() => showToast(t('forecast.failedSaveSettings'), 'error'));
          }}
          onClear={() => {
            api.clearShoppingList()
              .then(() => queryClient.invalidateQueries({ queryKey: ['shopping-list'] }))
              .catch(() => showToast(t('forecast.failedSaveSettings'), 'error'));
          }}
        />
      )}

      {/* ── Usage + projection chart ── */}
      {top5.length > 0 && <UsageChart forecasts={top5} days={chartDays} onDaysChange={setChartDays} />}

      {/* ── Table ── */}
      {forecasts.length === 0 ? (
        <div className="flex flex-col items-center justify-center py-16 text-bambu-gray">
          <TrendingDown className="w-10 h-10 mb-3 opacity-40" />
          <p className="text-sm">{t('forecast.noSpools')}</p>
        </div>
      ) : (
        <div className="bg-bambu-dark-secondary rounded-lg overflow-hidden border border-bambu-dark-tertiary">
          <div className="overflow-x-auto">
            <table className="w-full">
              <thead>
                <tr className="border-b border-bambu-dark-tertiary bg-bambu-dark-tertiary/30">
                  {/* Color dot */}
                  <th className="w-8 px-4 py-3" />
                  <SortableTh col="material" active={sortKey} dir={sortDir} onSort={handleSort}>
                    {t('forecast.sku')}
                  </SortableTh>
                  <SortableTh col="stock" active={sortKey} dir={sortDir} onSort={handleSort}>
                    {t('forecast.stock')}
                  </SortableTh>
                  <SortableTh col="used" active={sortKey} dir={sortDir} onSort={handleSort}>
                    {t('forecast.dailyRate')}
                  </SortableTh>
                  <SortableTh col="days_left" active={sortKey} dir={sortDir} onSort={handleSort}>
                    {t('forecast.daysLeft')}
                  </SortableTh>
                  <th className="px-4 py-3 text-left text-xs font-medium text-bambu-gray uppercase tracking-wide">
                    {t('forecast.emptyBy')}
                  </th>
                  <th className="px-4 py-3 text-left text-xs font-medium text-bambu-gray uppercase tracking-wide">
                    {t('forecast.reorderBy')}
                  </th>
                  {/* Actions */}
                  <th className="w-24 px-4 py-3" />
                </tr>
              </thead>
              <tbody className="divide-y divide-bambu-dark-tertiary">
                {sortedForecasts.map((f) => (
                  <ForecastRow
                    key={f.group.key}
                    forecast={f}
                    globalLeadTime={globalLeadTime}
                    canWrite={canWrite}
                    onSaved={() => queryClient.invalidateQueries({ queryKey: ['sku-settings'] })}
                    onCart={() => setCartModal(f)}
                    showToast={showToast}
                  />
                ))}
              </tbody>
            </table>
          </div>

          {/* Legend */}
          <div className="flex flex-wrap items-center gap-4 px-4 py-3 text-xs text-bambu-gray border-t border-bambu-dark-tertiary bg-bambu-dark-tertiary/20">
            <span className="flex items-center gap-1.5">
              <span className="w-2 h-2 rounded-full bg-bambu-green inline-block" />
              {t('forecast.trendLegend')}
            </span>
            <span className="flex items-center gap-1.5">
              <span className="w-2 h-2 rounded-full bg-blue-400 inline-block" />
              {t('forecast.estimatedLegend')}
            </span>
            <span className="flex items-center gap-1.5">
              <span className="w-2 h-2 rounded-full bg-bambu-gray/40 inline-block" />
              {t('forecast.noDataLegend')}
            </span>
          </div>
        </div>
      )}

      {/* ── Add to cart modal ── */}
      {cartModal && (
        <AddToCartModal
          forecast={cartModal}
          onClose={() => setCartModal(null)}
          onAdd={(item) => {
            api.addToShoppingList(item).then(() => {
              queryClient.invalidateQueries({ queryKey: ['shopping-list'] });
              showToast(t('forecast.addedToCart'), 'success');
              setCartModal(null);
              setListOpen(true);
            }).catch(() => showToast(t('forecast.failedAddItem'), 'error'));
          }}
        />
      )}
    </div>
  );
}

// ── Sortable th ───────────────────────────────────────────────────────────────

function SortableTh({
  col, active, dir, onSort, children,
}: {
  col: SortKey;
  active: SortKey;
  dir: SortDir;
  onSort: (k: SortKey) => void;
  children: React.ReactNode;
}) {
  const isActive = active === col;
  return (
    <th
      className="px-4 py-3 text-left text-xs font-medium text-bambu-gray uppercase tracking-wide cursor-pointer select-none hover:text-white transition-colors"
      onClick={() => onSort(col)}
    >
      <span className="inline-flex items-center">
        {children}
        {isActive
          ? dir === 'asc'
            ? <ArrowUp className="w-3 h-3 ml-1 text-bambu-green" />
            : <ArrowDown className="w-3 h-3 ml-1 text-bambu-green" />
          : <ArrowUpDown className="w-3 h-3 ml-1 opacity-40" />
        }
      </span>
    </th>
  );
}

// ── Alert Banner ──────────────────────────────────────────────────────────────

function AlertBanner({ forecast: f, onCart }: { forecast: SkuForecast; onCart: () => void }) {
  const { t } = useTranslation();
  const label = [f.group.brand, f.group.material, f.group.subtype].filter(Boolean).join(' ');
  const isBreak = f.stockBreakAlert;

  return (
    <div className={`flex items-center gap-3 px-4 py-3 rounded-lg border text-sm ${
      isBreak ? 'bg-red-500/10 border-red-500/30 text-red-300' : 'bg-yellow-500/10 border-yellow-500/30 text-yellow-300'
    }`}>
      <AlertTriangle className="w-4 h-4 flex-shrink-0" />
      <div className="flex-1 min-w-0">
        <span className="font-medium">{label}</span>
        {isBreak ? (
          <span className="ml-2 text-xs opacity-80">
            {t('forecast.stockBreakRisk')} — {t('forecast.stockBreakDetail', { days: f.daysRemaining, lt: f.effectiveLeadTimeDays })}
          </span>
        ) : (
          <span className="ml-2 text-xs opacity-80">
            {t('forecast.reorderNow')} — {t('forecast.reorderTriggerPassed', { date: f.reorderTriggerDate ? formatDate(f.reorderTriggerDate) : '—' })}
          </span>
        )}
      </div>
      <button
        onClick={onCart}
        className="flex items-center gap-1.5 px-2.5 py-1 rounded border border-current text-xs opacity-70 hover:opacity-100 transition-opacity"
      >
        <ShoppingCart className="w-3 h-3" /> {t('forecast.order')}
      </button>
    </div>
  );
}

// ── Usage + Projection Chart ──────────────────────────────────────────────────

const CHART_TIMEFRAMES: { label: string; value: ChartDays }[] = [
  { label: '1W', value: 7 },
  { label: '1M', value: 30 },
  { label: '6M', value: 180 },
];

function UsageChart({ forecasts, days: maxDays, onDaysChange }: {
  forecasts: SkuForecast[];
  days: ChartDays;
  onDaysChange: (d: ChartDays) => void;
}) {
  const { t } = useTranslation();
  const days = Array.from({ length: maxDays + 1 }, (_, i) => i);

  const series = forecasts.map((f, idx) => ({
    key: f.group.key,
    label: [f.group.brand, f.group.material, f.group.subtype].filter(Boolean).join(' '),
    color: CHART_COLORS[idx % CHART_COLORS.length],
    rop: f.reorderPointG,
    points: buildProjectionSeries(f, maxDays),
  }));

  const chartData = days.map((d) => {
    const row: Record<string, number | string> = { day: d, label: formatDateShort(addDays(new Date(), d)) };
    for (const s of series) {
      const pt = s.points.find((p) => p.day === d);
      row[s.key] = pt?.stock ?? 0;
    }
    return row;
  });

  const lastNonZeroDay = (() => {
    for (let d = maxDays; d >= 0; d--) {
      if (series.some((s) => (chartData[d]?.[s.key] as number) > 0)) return d;
    }
    return maxDays;
  })();

  const trimmedData = chartData.slice(0, lastNonZeroDay + 1);
  const ropLines = series.filter((s) => s.rop > 0);

  return (
    <div className="bg-bambu-dark-secondary rounded-lg overflow-hidden border border-bambu-dark-tertiary p-4">
      <div className="flex items-center gap-2 mb-4">
        <TrendingDown className="w-4 h-4 text-bambu-green" />
        <h3 className="text-sm font-semibold text-white">{t('forecast.chartTitle')}</h3>
        <span className="text-xs text-bambu-gray ml-1 hidden sm:inline">{t('forecast.dashedLinesROP')}</span>
        <div className="ml-auto flex items-center bg-bambu-dark-tertiary rounded-lg p-0.5">
          {CHART_TIMEFRAMES.map((tf) => (
            <button
              key={tf.value}
              onClick={() => onDaysChange(tf.value)}
              className={`px-2.5 py-1 text-xs font-medium rounded-md transition-colors ${
                maxDays === tf.value
                  ? 'bg-bambu-dark-secondary text-white shadow'
                  : 'text-bambu-gray hover:text-white'
              }`}
            >
              {tf.label}
            </button>
          ))}
        </div>
      </div>
      <ResponsiveContainer width="100%" height={220}>
        <AreaChart data={trimmedData} margin={{ top: 4, right: 8, bottom: 0, left: 0 }}>
          <defs>
            {series.map((s) => (
              <linearGradient key={s.key} id={`grad-${s.key}`} x1="0" y1="0" x2="0" y2="1">
                <stop offset="5%" stopColor={s.color} stopOpacity={0.25} />
                <stop offset="95%" stopColor={s.color} stopOpacity={0.02} />
              </linearGradient>
            ))}
          </defs>
          <CartesianGrid strokeDasharray="3 3" stroke="#374151" strokeOpacity={0.5} />
          <XAxis
            dataKey="label"
            tick={{ fill: '#6B7280', fontSize: 10 }}
            interval={Math.max(0, Math.ceil(lastNonZeroDay / 8) - 1)}
            axisLine={false}
            tickLine={false}
          />
          <YAxis
            tick={{ fill: '#6B7280', fontSize: 10 }}
            axisLine={false}
            tickLine={false}
            tickFormatter={(v: number) => v >= 1000 ? `${(v / 1000).toFixed(1)}kg` : `${v}g`}
            width={48}
          />
          <Tooltip
            contentStyle={{ background: '#1a1a2e', border: '1px solid #374151', borderRadius: 8, fontSize: 12 }}
            labelStyle={{ color: '#9CA3AF' }}
            itemStyle={{ color: '#E5E7EB' }}
            formatter={(value, name) => {
              if (typeof value !== 'number') return '';
              const s = series.find((x) => x.key === String(name));
              return `${value}g — ${s?.label ?? name}`;
            }}
          />
          <Legend
            formatter={(value) => {
              const s = series.find((x) => x.key === value);
              return <span style={{ color: '#9CA3AF', fontSize: 11 }}>{s?.label ?? value}</span>;
            }}
          />
          {series.map((s) => (
            <Area
              key={s.key}
              type="monotone"
              dataKey={s.key}
              stroke={s.color}
              strokeWidth={2}
              fill={`url(#grad-${s.key})`}
              dot={false}
              activeDot={{ r: 3 }}
            />
          ))}
          {ropLines.map((s) => (
            <ReferenceLine
              key={`rop-${s.key}`}
              y={s.rop}
              stroke={s.color}
              strokeDasharray="4 3"
              strokeOpacity={0.6}
            />
          ))}
        </AreaChart>
      </ResponsiveContainer>
    </div>
  );
}

// ── Global lead time setting (compact inline) ─────────────────────────────────

function GlobalLeadTimeSetting({ value, onSave }: { value: number; onSave: (v: number) => void }) {
  const { t } = useTranslation();
  const [editing, setEditing] = useState(false);
  const [input, setInput] = useState(String(value));

  function save() {
    const v = parseInt(input, 10);
    if (isNaN(v) || v < 0) return;
    onSave(v);
    setEditing(false);
  }

  return (
    <div className="flex items-center gap-2 px-3 py-1.5 bg-bambu-dark-tertiary/40 rounded-lg border border-bambu-dark-tertiary text-xs text-bambu-gray">
      <Info className="w-3.5 h-3.5 flex-shrink-0" aria-label={t('forecast.globalLeadTimeHint')} />
      <span className="hidden sm:inline">{t('forecast.globalLeadTime')}:</span>
      {editing ? (
        <form className="flex items-center gap-1.5" onSubmit={(e) => { e.preventDefault(); save(); }}>
          <input
            type="number" min={0} max={365}
            value={input}
            onChange={(e) => setInput(e.target.value)}
            className="w-14 px-1.5 py-0.5 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded text-sm text-white focus:outline-none focus:border-bambu-green"
            autoFocus
          />
          <span className="text-bambu-gray">d</span>
          <button type="submit" className="px-2 py-0.5 bg-bambu-green text-white text-xs rounded hover:bg-bambu-green/80">{t('forecast.save')}</button>
          <button type="button" onClick={() => setEditing(false)} className="text-xs text-bambu-gray hover:text-white">✕</button>
        </form>
      ) : (
        <div className="flex items-center gap-1.5">
          <span className="font-semibold text-white">{value}d</span>
          <button onClick={() => { setInput(String(value)); setEditing(true); }} className="p-0.5 text-bambu-gray hover:text-white rounded transition-colors">
            <Edit2 className="w-3 h-3" />
          </button>
        </div>
      )}
    </div>
  );
}

// ── Forecast Row ──────────────────────────────────────────────────────────────

function ForecastRow({
  forecast: f, globalLeadTime, canWrite, onSaved, onCart, showToast,
}: {
  forecast: SkuForecast;
  globalLeadTime: number;
  canWrite: boolean;
  onSaved: () => void;
  onCart: () => void;
  showToast: (msg: string, type: 'success' | 'error') => void;
}) {
  const { t } = useTranslation();
  const [expanded, setExpanded] = useState(false);
  const [editingLead, setEditingLead] = useState(false);
  const [editingMargin, setEditingMargin] = useState(false);
  const [leadInput, setLeadInput] = useState(String(f.settings?.lead_time_days ?? 0));
  const [marginInput, setMarginInput] = useState(String(f.settings?.safety_margin_value ?? 14));
  const [marginUnit, setMarginUnit] = useState<'days' | 'g'>(f.settings?.safety_margin_unit ?? 'days');

  // Sync inputs when remote settings change and the field is not actively being edited.
  useEffect(() => {
    if (!editingLead) setLeadInput(String(f.settings?.lead_time_days ?? 0));
  }, [f.settings?.lead_time_days, editingLead]);
  useEffect(() => {
    if (!editingMargin) {
      setMarginInput(String(f.settings?.safety_margin_value ?? 14));
      setMarginUnit(f.settings?.safety_margin_unit ?? 'days');
    }
  }, [f.settings?.safety_margin_value, f.settings?.safety_margin_unit, editingMargin]);

  const upsertMutation = useMutation({
    mutationFn: api.upsertSkuSettings,
    onSuccess: () => { onSaved(); showToast(t('forecast.settingsSaved'), 'success'); },
    onError: () => showToast(t('forecast.failedSaveSettings'), 'error'),
  });

  const snoozed = f.settings?.alerts_snoozed ?? false;

  const label = [f.group.brand, f.group.material, f.group.subtype].filter(Boolean).join(' ');
  // Use getSwatchStyle so a Clear (alpha=00) lead spool renders as a
  // checkerboard rather than collapsing to solid black (#1545).
  const colorStyle = f.group.spools[0]?.rgba ? getSwatchStyle(f.group.spools[0].rgba) : { backgroundColor: '#4B5563' };
  const remainPct = f.totalLabelG > 0 ? Math.round((f.totalRemainingG / f.totalLabelG) * 100) : 0;

  const daysColor = snoozed ? 'text-bambu-gray'
    : f.daysRemaining === null ? 'text-bambu-gray'
    : f.stockBreakAlert ? 'text-red-400'
    : f.reorderAlert ? 'text-yellow-400'
    : f.daysRemaining < 30 ? 'text-yellow-400'
    : 'text-green-400';

  function upsert(lead: number, marginVal: number, marginUnitArg: 'days' | 'g', alertsSnoozed = snoozed) {
    upsertMutation.mutate({ material: f.group.material, subtype: f.group.subtype, brand: f.group.brand, lead_time_days: lead, safety_margin_value: marginVal, safety_margin_unit: marginUnitArg, alerts_snoozed: alertsSnoozed });
  }

  function toggleSnooze(e: React.MouseEvent) {
    e.stopPropagation();
    upsert(f.settings?.lead_time_days ?? 0, f.settings?.safety_margin_value ?? 14, f.settings?.safety_margin_unit ?? 'days', !snoozed);
  }

  const tierBadge = f.rateTier === 'history'
    ? <span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded bg-bambu-green/15 text-bambu-green"><span className="w-1.5 h-1.5 rounded-full bg-bambu-green" />{t('forecast.trend')}</span>
    : f.rateTier === 'delta'
    ? <span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded bg-blue-400/15 text-blue-400"><span className="w-1.5 h-1.5 rounded-full bg-blue-400" />{t('forecast.estimated')}</span>
    : <span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded bg-bambu-dark-tertiary text-bambu-gray/60"><span className="w-1.5 h-1.5 rounded-full bg-bambu-gray/40" />{t('forecast.noData')}</span>;

  const rowAlertBorder = snoozed ? '' : f.stockBreakAlert ? 'bg-red-500/5' : f.reorderAlert ? 'bg-yellow-500/5' : '';

  return (
    <>
      <tr
        className={`cursor-pointer hover:bg-bambu-dark-tertiary/40 transition-colors ${rowAlertBorder} ${snoozed ? 'opacity-50' : ''}`}
        onClick={() => setExpanded((e) => !e)}
      >
        {/* Color dot */}
        <td className="px-4 py-3">
          <span
            className="block w-3 h-3 rounded-full border border-black/20"
            style={colorStyle}
          />
        </td>

        {/* SKU */}
        <td className="px-4 py-3">
          <div className="text-sm font-medium text-white">{label}</div>
          <div className="text-xs text-bambu-gray">{t('forecast.spoolCount', { count: f.totalSpools })}</div>
        </td>

        {/* Stock */}
        <td className="px-4 py-3 min-w-[120px]">
          <div className="h-1.5 bg-bambu-dark-tertiary rounded-full overflow-hidden mb-1 w-24">
            <div
              className={`h-full rounded-full ${remainPct > 50 ? 'bg-bambu-green' : remainPct > 20 ? 'bg-yellow-500' : 'bg-red-500'}`}
              style={{ width: `${Math.min(remainPct, 100)}%` }}
            />
          </div>
          <span className="text-xs text-bambu-gray">{Math.round(f.totalRemainingG)}g</span>
        </td>

        {/* Rate */}
        <td className="px-4 py-3">
          <div className="text-sm text-white">{f.dailyRateG !== null ? `${f.dailyRateG.toFixed(1)}g/d` : '—'}</div>
          <div className="mt-0.5">{tierBadge}</div>
        </td>

        {/* Days left */}
        <td className="px-4 py-3">
          <span className={`text-sm font-semibold ${daysColor}`}>
            {f.daysRemaining !== null ? `${f.daysRemaining}d` : <span className="text-bambu-gray font-normal">—</span>}
          </span>
        </td>

        {/* Empty by */}
        <td className="px-4 py-3">
          <span className="text-sm text-bambu-gray">
            {f.projectedEmptyDate ? formatDate(f.projectedEmptyDate) : '—'}
          </span>
        </td>

        {/* Reorder by */}
        <td className="px-4 py-3">
          <span className={`text-sm font-medium ${!snoozed && f.reorderAlert ? 'text-yellow-400' : 'text-bambu-gray'}`}>
            {f.reorderTriggerDate ? formatDate(f.reorderTriggerDate) : '—'}
          </span>
        </td>

        {/* Actions */}
        <td className="px-4 py-3" onClick={(e) => e.stopPropagation()}>
          <div className="flex items-center justify-end gap-1">
            {canWrite && (
              <button
                onClick={onCart}
                className="p-1.5 text-bambu-gray hover:text-bambu-green rounded transition-colors"
                title={t('forecast.addToCart')}
              >
                <ShoppingCart className="w-4 h-4" />
              </button>
            )}
            {!snoozed && (f.stockBreakAlert ? (
              <AlertTriangle className="w-4 h-4 text-red-400" aria-label={t('forecast.stockBreakRisk')} />
            ) : f.reorderAlert ? (
              <AlertTriangle className="w-4 h-4 text-yellow-400" aria-label={t('forecast.reorderNow')} />
            ) : f.daysRemaining !== null ? (
              <Check className="w-4 h-4 text-bambu-green/50" />
            ) : null)}
            {canWrite && (
              <button
                onClick={toggleSnooze}
                className={`p-1 rounded transition-colors ${snoozed ? 'text-bambu-gray/70 hover:text-white' : 'text-bambu-dark-tertiary hover:text-bambu-gray'}`}
                title={t(snoozed ? 'forecast.alertsEnabled' : 'forecast.alertsSnoozed')}
              >
                <BellOff className="w-3.5 h-3.5" />
              </button>
            )}
            <button
              onClick={(e) => { e.stopPropagation(); setExpanded((v) => !v); }}
              className="p-1.5 text-bambu-gray hover:text-white rounded transition-colors"
            >
              {expanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
            </button>
          </div>
        </td>
      </tr>

      {/* ── Expanded detail row ── */}
      {expanded && (
        <tr className="bg-bambu-dark-tertiary/10">
          <td colSpan={8} className="px-6 py-4">
            <div className="space-y-4">

              {/* Logistics summary */}
              <div className="grid grid-cols-3 gap-3">
                <LogisticStat
                  label={t('forecast.effectiveLeadTime')}
                  value={`${f.effectiveLeadTimeDays}d`}
                  hint={t('forecast.effectiveLeadTimeHint', { global: globalLeadTime, sku: f.settings?.lead_time_days ?? 0 })}
                />
                <LogisticStat
                  label={t('forecast.safetyMarginLabel')}
                  value={`${Math.round(f.safetyStockG)}g`}
                  hint={t('forecast.safetyMarginHint')}
                />
                <LogisticStat
                  label={t('forecast.reorderPoint')}
                  value={`${Math.round(f.reorderPointG)}g`}
                  hint={t('forecast.reorderPointHint')}
                />
              </div>

              {/* Per-SKU settings — write-gated */}
              {canWrite && (
                <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
                  <SettingField
                    label={t('forecast.skuLeadTimeOverride')}
                    hint={t('forecast.skuLeadTimeHint')}
                    unit={t('forecast.leadTime')}
                    editing={editingLead}
                    value={f.settings?.lead_time_days ?? 0}
                    inputValue={leadInput}
                    onInputChange={setLeadInput}
                    onEdit={() => { setLeadInput(String(f.settings?.lead_time_days ?? 0)); setEditingLead(true); }}
                    onSave={() => {
                      const v = parseInt(leadInput, 10);
                      if (!isNaN(v) && v >= 0) { upsert(v, f.settings?.safety_margin_value ?? 14, marginUnit); setEditingLead(false); }
                    }}
                    onCancel={() => setEditingLead(false)}
                    isPending={upsertMutation.isPending}
                    saveLabel={t('forecast.save')}
                    cancelLabel={t('forecast.cancel')}
                  />
                  <SafetyMarginField
                    value={f.settings?.safety_margin_value ?? 14}
                    unit={marginUnit}
                    editing={editingMargin}
                    inputValue={marginInput}
                    dailyRateG={f.dailyRateG}
                    onInputChange={setMarginInput}
                    onUnitChange={(u) => setMarginUnit(u)}
                    onEdit={() => { setMarginInput(String(f.settings?.safety_margin_value ?? 14)); setMarginUnit(f.settings?.safety_margin_unit ?? 'days'); setEditingMargin(true); }}
                    onSave={() => {
                      const v = parseInt(marginInput, 10);
                      if (!isNaN(v) && v >= 0) { upsert(f.settings?.lead_time_days ?? 0, v, marginUnit); setEditingMargin(false); }
                    }}
                    onCancel={() => setEditingMargin(false)}
                    isPending={upsertMutation.isPending}
                    saveLabel={t('forecast.save')}
                    cancelLabel={t('forecast.cancel')}
                    safetyMarginLabel={t('forecast.safetyMarginLabel')}
                  />
                </div>
              )}

              {/* Individual spools — shown when group has >1 spool */}
              {f.group.spools.length > 1 && (
                <div className="border-t border-bambu-dark-tertiary pt-3">
                  <p className="text-xs text-bambu-gray mb-2">{t('forecast.individualSpools')}</p>
                  <div className="bg-bambu-dark-secondary rounded-lg overflow-hidden border border-bambu-dark-tertiary">
                    <table className="w-full">
                      <thead>
                        <tr className="border-b border-bambu-dark-tertiary bg-bambu-dark-tertiary/30">
                          <th className="px-4 py-2 text-left text-xs font-medium text-bambu-gray uppercase tracking-wide">#</th>
                          <th className="px-4 py-2 text-left text-xs font-medium text-bambu-gray uppercase tracking-wide">{t('inventory.remaining')}</th>
                          <th className="px-4 py-2 text-left text-xs font-medium text-bambu-gray uppercase tracking-wide">{t('inventory.used')}</th>
                          <th className="px-4 py-2 text-left text-xs font-medium text-bambu-gray uppercase tracking-wide">{t('forecast.labelWeight')}</th>
                        </tr>
                      </thead>
                      <tbody className="divide-y divide-bambu-dark-tertiary">
                        {f.group.spools.map((s) => {
                          const remaining = Math.max(0, s.label_weight - s.weight_used);
                          const pct = s.label_weight > 0 ? (remaining / s.label_weight) * 100 : 0;
                          return (
                            <tr key={s.id} className="hover:bg-bambu-dark-tertiary/30 transition-colors">
                              <td className="px-4 py-2">
                                <span className="text-xs font-mono text-bambu-gray/70">#{s.id}</span>
                              </td>
                              <td className="px-4 py-2">
                                <div className="flex items-center gap-3">
                                  <div className="w-24 h-1.5 bg-bambu-dark-tertiary rounded-full overflow-hidden flex-shrink-0">
                                    <div
                                      className={`h-full rounded-full ${pct > 50 ? 'bg-bambu-green' : pct > 20 ? 'bg-yellow-500' : 'bg-red-500'}`}
                                      style={{ width: `${Math.min(pct, 100)}%` }}
                                    />
                                  </div>
                                  <span className="text-sm text-white">{Math.round(remaining)}g</span>
                                </div>
                              </td>
                              <td className="px-4 py-2">
                                <span className="text-sm text-bambu-gray">{Math.round(Math.max(0, s.weight_used - (s.weight_used_baseline ?? 0)))}g</span>
                              </td>
                              <td className="px-4 py-2">
                                <span className="text-sm text-bambu-gray">{s.label_weight}g</span>
                              </td>
                            </tr>
                          );
                        })}
                      </tbody>
                    </table>
                  </div>
                </div>
              )}
            </div>
          </td>
        </tr>
      )}
    </>
  );
}

// ── Logistic stat chip ────────────────────────────────────────────────────────

function LogisticStat({ label, value, hint }: { label: string; value: string; hint: string }) {
  return (
    <div className="bg-bambu-dark-tertiary/40 rounded-lg p-3" title={hint}>
      <div className="text-xs text-bambu-gray mb-1">{label}</div>
      <div className="text-lg font-semibold text-white">{value}</div>
    </div>
  );
}

// ── Setting field ─────────────────────────────────────────────────────────────

function SettingField({
  label, hint, unit, editing, value, inputValue,
  onInputChange, onEdit, onSave, onCancel, isPending,
  saveLabel = 'Save', cancelLabel = 'Cancel',
}: {
  label: string; hint: string; unit: string; editing: boolean;
  value: number; inputValue: string;
  onInputChange: (v: string) => void; onEdit: () => void;
  onSave: () => void; onCancel: () => void; isPending: boolean;
  saveLabel?: string; cancelLabel?: string;
}) {
  return (
    <div className="bg-bambu-dark-tertiary/40 rounded-lg p-3 space-y-1">
      <div className="flex items-center gap-1.5">
        <span className="text-xs font-medium text-white">{label}</span>
        <span title={hint}><Info className="w-3 h-3 text-bambu-gray/50" /></span>
      </div>
      {editing ? (
        <form className="flex items-center gap-2" onSubmit={(e) => { e.preventDefault(); onSave(); }}>
          <input
            type="number" min={0} max={365}
            value={inputValue} onChange={(e) => onInputChange(e.target.value)}
            className="w-20 px-2 py-1 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded text-sm text-white focus:outline-none focus:border-bambu-green"
            autoFocus disabled={isPending}
          />
          <span className="text-xs text-bambu-gray">{unit}</span>
          <button type="submit" disabled={isPending} className="px-2 py-1 bg-bambu-green text-white text-xs rounded hover:bg-bambu-green/80 disabled:opacity-50">{saveLabel}</button>
          <button type="button" onClick={onCancel} disabled={isPending} className="px-2 py-1 text-xs text-bambu-gray hover:text-white">{cancelLabel}</button>
        </form>
      ) : (
        <div className="flex items-center gap-2">
          <span className="text-lg font-semibold text-white">{value}</span>
          <span className="text-xs text-bambu-gray">{unit}</span>
          <button onClick={onEdit} className="p-1 text-bambu-gray hover:text-white rounded transition-colors"><Edit2 className="w-3 h-3" /></button>
        </div>
      )}
    </div>
  );
}

// ── Safety margin field (dual unit: days | grams) ────────────────────────────

function SafetyMarginField({
  value, unit, editing, inputValue, dailyRateG,
  onInputChange, onUnitChange, onEdit, onSave, onCancel, isPending,
  saveLabel = 'Save', cancelLabel = 'Cancel', safetyMarginLabel = 'Safety Margin',
}: {
  value: number; unit: 'days' | 'g'; editing: boolean; inputValue: string;
  dailyRateG: number | null;
  onInputChange: (v: string) => void; onUnitChange: (u: 'days' | 'g') => void;
  onEdit: () => void; onSave: () => void; onCancel: () => void; isPending: boolean;
  saveLabel?: string; cancelLabel?: string; safetyMarginLabel?: string;
}) {
  const { t } = useTranslation();
  const displayG = unit === 'g' ? value : (dailyRateG !== null ? Math.round(dailyRateG * value) : null);
  const hint = unit === 'days'
    ? t('forecast.safetyMarginHintDays', {
        approx: displayG !== null ? t('forecast.safetyMarginHintDaysApprox', { g: displayG }) : '',
      })
    : t('forecast.safetyMarginHintG', {
        approx: dailyRateG !== null ? t('forecast.safetyMarginHintGApprox', { days: Math.round(value / dailyRateG) }) : '',
      });

  return (
    <div className="bg-bambu-dark-tertiary/40 rounded-lg p-3 space-y-1">
      <div className="flex items-center gap-1.5">
        <span className="text-xs font-medium text-white">{safetyMarginLabel}</span>
        <span title={hint}><Info className="w-3 h-3 text-bambu-gray/50" /></span>
      </div>
      {editing ? (
        <form className="flex items-center gap-2 flex-wrap" onSubmit={(e) => { e.preventDefault(); onSave(); }}>
          <input
            type="number" min={0} max={unit === 'g' ? 10000 : 365}
            value={inputValue} onChange={(e) => onInputChange(e.target.value)}
            className="w-20 px-2 py-1 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded text-sm text-white focus:outline-none focus:border-bambu-green"
            autoFocus disabled={isPending}
          />
          {/* Unit toggle */}
          <div className="flex bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded overflow-hidden text-xs">
            <button type="button" onClick={() => onUnitChange('days')} className={`px-2 py-1 transition-colors ${unit === 'days' ? 'bg-bambu-green text-white' : 'text-bambu-gray hover:text-white'}`}>days</button>
            <button type="button" onClick={() => onUnitChange('g')} className={`px-2 py-1 transition-colors ${unit === 'g' ? 'bg-bambu-green text-white' : 'text-bambu-gray hover:text-white'}`}>g</button>
          </div>
          <button type="submit" disabled={isPending} className="px-2 py-1 bg-bambu-green text-white text-xs rounded hover:bg-bambu-green/80 disabled:opacity-50">{saveLabel}</button>
          <button type="button" onClick={onCancel} disabled={isPending} className="px-2 py-1 text-xs text-bambu-gray hover:text-white">{cancelLabel}</button>
        </form>
      ) : (
        <div className="flex items-center gap-2">
          <span className="text-lg font-semibold text-white">{value}</span>
          <span className="text-xs text-bambu-gray">{unit}</span>
          {displayG !== null && unit === 'days' && (
            <span className="text-xs text-bambu-gray/60">≈ {displayG}g</span>
          )}
          {unit === 'g' && dailyRateG !== null && (
            <span className="text-xs text-bambu-gray/60">≈ {Math.round(value / dailyRateG)}d</span>
          )}
          <button onClick={onEdit} className="p-1 text-bambu-gray hover:text-white rounded transition-colors"><Edit2 className="w-3 h-3" /></button>
        </div>
      )}
    </div>
  );
}

// ── Shopping list panel ───────────────────────────────────────────────────────

function ShoppingListPanel({
  items, forecasts, globalLeadTime, canWrite, onClose, onRemove, onClear,
}: {
  items: ShoppingListItem[];
  forecasts: SkuForecast[];
  globalLeadTime: number;
  canWrite: boolean;
  onClose: () => void;
  onRemove: (id: number) => void;
  onClear: () => void;
}) {
  const { t } = useTranslation();
  const queryClient = useQueryClient();
  const [view, setView] = useState<'list' | 'logistics'>('list');

  const statusMutation = useMutation({
    mutationFn: async ({ id, status, item, avgSpoolG }: {
      id: number;
      status: 'pending' | 'purchased' | 'received';
      item?: ShoppingListItem;
      avgSpoolG?: number;
    }) => {
      await api.updateShoppingListStatus(id, status);
      if (status === 'received' && item) {
        // Add received spools to stock category
        const spoolWeight = avgSpoolG ?? 1000;
        const spoolBase: Parameters<typeof api.bulkCreateSpools>[0] = {
          material: item.material,
          subtype: item.subtype,
          brand: item.brand,
          label_weight: spoolWeight,
          core_weight: 0,
          core_weight_catalog_id: null,
          color_name: null, rgba: null, extra_colors: null, effect_type: null,
          nozzle_temp_min: null, nozzle_temp_max: null,
          note: item.note ?? null,
          tag_uid: null, tray_uuid: null,
          data_origin: 'manual', tag_type: null,
          cost_per_kg: null,
          last_scale_weight: null, last_weighed_at: null,
          weight_used: 0,
          slicer_filament: null, slicer_filament_name: null,
          added_full: null, last_used: null, encode_time: null,
          category: 'Stock',
          low_stock_threshold_pct: null,
        };
        await api.bulkCreateSpools(spoolBase, item.quantity_spools);
        await api.removeFromShoppingList(id);
      }
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['shopping-list'] });
      queryClient.invalidateQueries({ queryKey: ['spools'] });
    },
  });

  // Build a forecast lookup keyed by (material||subtype||brand)
  const forecastMap = useMemo(() => {
    const m = new Map<string, SkuForecast>();
    for (const f of forecasts) m.set(f.group.key, f);
    return m;
  }, [forecasts]);

  // Resolve a forecast for each cart item
  const cartForecasts = useMemo(() =>
    items.map((item) => ({
      item,
      forecast: forecastMap.get(skuKey(item.material, item.subtype, item.brand)) ?? null,
    })),
    [items, forecastMap]
  );

  // Items where stock break before replenishment is detected
  const breakAlerts = useMemo(() =>
    cartForecasts.filter(({ forecast: f }) => {
      if (!f || f.dailyRateG === null) return false;
      // Stock runs out before the lead time window ends
      return f.stockBreakAlert || (f.daysRemaining !== null && f.daysRemaining <= f.effectiveLeadTimeDays);
    }),
    [cartForecasts]
  );

  function downloadCsv() {
    const headers = [t('forecast.qty'), t('forecast.material'), 'Brand', 'Subtype', `${t('forecast.weight')} (g)`, `${t('forecast.leadTime')} (d)`, t('forecast.expectedRestock'), t('forecast.status'), t('forecast.note')];
    const rows = items.map((i) => {
      const f = forecastMap.get(skuKey(i.material, i.subtype, i.brand)) ?? null;
      const avgSpoolG = f && f.totalSpools > 0 ? f.totalLabelG / f.totalSpools : 1000;
      const totalWeightG = Math.round(i.quantity_spools * avgSpoolG);
      const lt = f?.effectiveLeadTimeDays ?? globalLeadTime ?? 0;
      const restock = lt > 0 ? formatDate(addDays(new Date(), lt)) : '';
      return [
        i.quantity_spools,
        i.material,
        i.brand ?? '',
        i.subtype ?? '',
        totalWeightG,
        lt || '',
        restock,
        i.status,
        i.note ?? '',
      ].map((v) => `"${String(v).replace(/"/g, '""')}"`).join(',');
    });
    const csv = [headers.join(','), ...rows].join('\n');
    const blob = new Blob([csv], { type: 'text/csv' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `shopping-list-${new Date().toISOString().slice(0, 10)}.csv`;
    a.click();
    setTimeout(() => URL.revokeObjectURL(url), 100);
  }

  return (
    <div className="bg-bambu-dark-secondary rounded-lg overflow-hidden border border-bambu-dark-tertiary">
      {/* Header */}
      <div className="flex items-center justify-between px-4 py-3 border-b border-bambu-dark-tertiary bg-bambu-dark-tertiary/30">
        <div className="flex items-center gap-3">
          <ShoppingCart className="w-4 h-4 text-bambu-green" />
          <h3 className="text-sm font-semibold text-white">{t('forecast.shoppingList')}</h3>
          <span className="text-xs text-bambu-gray">{t('forecast.shoppingListItems', { count: items.length })}</span>
          {/* View toggle */}
          {items.length > 0 && (
            <div className="flex bg-bambu-dark-tertiary rounded-md p-0.5 ml-1">
              <button
                onClick={() => setView('list')}
                className={`flex items-center gap-1.5 px-2 py-0.5 text-xs font-medium rounded transition-colors ${view === 'list' ? 'bg-bambu-dark-secondary text-white shadow' : 'text-bambu-gray hover:text-white'}`}
              >
                <Package className="w-3 h-3" />
                {t('forecast.listView')}
              </button>
              <button
                onClick={() => setView('logistics')}
                className={`flex items-center gap-1.5 px-2 py-0.5 text-xs font-medium rounded transition-colors ${view === 'logistics' ? 'bg-bambu-dark-secondary text-white shadow' : 'text-bambu-gray hover:text-white'}`}
              >
                <BarChart2 className="w-3 h-3" />
                {t('forecast.logisticsView')}
                {breakAlerts.length > 0 && (
                  <span className="w-3.5 h-3.5 rounded-full bg-red-500 text-white text-[9px] font-bold flex items-center justify-center">
                    {breakAlerts.length}
                  </span>
                )}
              </button>
            </div>
          )}
        </div>
        <div className="flex items-center gap-2">
          {items.length > 0 && (
            <>
              <button onClick={downloadCsv} className="flex items-center gap-1.5 text-xs text-bambu-gray hover:text-white transition-colors px-2 py-1 rounded border border-bambu-dark-tertiary hover:bg-bambu-dark-tertiary">
                <Download className="w-3 h-3" />
                {t('forecast.downloadCsv')}
              </button>
              {canWrite && (
                <button onClick={onClear} className="text-xs text-red-400 hover:text-red-300 transition-colors px-2 py-1 rounded border border-red-500/20 hover:bg-red-500/10">
                  {t('forecast.clearAll')}
                </button>
              )}
            </>
          )}
          <button onClick={onClose} className="p-1 text-bambu-gray hover:text-white transition-colors"><X className="w-4 h-4" /></button>
        </div>
      </div>

      {items.length === 0 ? (
        <div className="flex flex-col items-center py-8 text-bambu-gray">
          <Package className="w-8 h-8 mb-2 opacity-30" />
          <p className="text-sm">{t('forecast.shoppingListEmpty')}</p>
        </div>
      ) : view === 'list' ? (
        <div className="overflow-x-auto">
          <table className="w-full">
            <thead>
              <tr className="border-b border-bambu-dark-tertiary bg-bambu-dark-tertiary/20">
                <th className="px-4 py-2 text-left text-xs font-medium text-bambu-gray uppercase tracking-wide">{t('forecast.qty')}</th>
                <th className="px-4 py-2 text-left text-xs font-medium text-bambu-gray uppercase tracking-wide">{t('forecast.material')}</th>
                <th className="px-4 py-2 text-left text-xs font-medium text-bambu-gray uppercase tracking-wide">{t('forecast.weight')}</th>
                <th className="px-4 py-2 text-left text-xs font-medium text-bambu-gray uppercase tracking-wide">{t('forecast.leadTime')}</th>
                <th className="px-4 py-2 text-left text-xs font-medium text-bambu-gray uppercase tracking-wide">{t('forecast.expectedRestock')}</th>
                <th className="px-4 py-2 text-left text-xs font-medium text-bambu-gray uppercase tracking-wide">{t('forecast.status')}</th>
                <th className="px-4 py-2 text-left text-xs font-medium text-bambu-gray uppercase tracking-wide">{t('forecast.note')}</th>
                <th className="px-4 py-2 text-right text-xs font-medium text-bambu-gray uppercase tracking-wide">{t('forecast.actions')}</th>
              </tr>
            </thead>
            <tbody className="divide-y divide-bambu-dark-tertiary">
              {items.map((item) => {
                const lbl = [item.brand, item.material, item.subtype].filter(Boolean).join(' ');
                const hasBreak = breakAlerts.some((a) => a.item.id === item.id);
                const f = forecastMap.get(skuKey(item.material, item.subtype, item.brand)) ?? null;
                const avgSpoolG = f && f.totalSpools > 0 ? f.totalLabelG / f.totalSpools : 1000;
                const totalWeightG = Math.round(item.quantity_spools * avgSpoolG);
                const lt = f?.effectiveLeadTimeDays ?? globalLeadTime ?? 0;
                const restockDate = lt > 0 ? addDays(new Date(), lt) : null;
                const isPurchased = item.status === 'purchased' || item.status === 'received';
                const isReceived = item.status === 'received';
                const isMutating = statusMutation.isPending;

                return (
                  <tr key={item.id} className={`hover:bg-bambu-dark-tertiary/30 transition-colors ${hasBreak && !isPurchased ? 'bg-red-500/5' : ''}`}>
                    {/* Qty */}
                    <td className="px-4 py-2.5">
                      <span className="text-sm font-semibold text-bambu-green">{item.quantity_spools}×</span>
                    </td>
                    {/* Material */}
                    <td className="px-4 py-2.5">
                      <div className="flex items-center gap-2">
                        <span className="text-sm text-white">{lbl}</span>
                        {hasBreak && !isPurchased && (
                          <AlertTriangle className="w-3.5 h-3.5 text-red-400 flex-shrink-0" aria-label={t('forecast.stockBreakBefore')} />
                        )}
                      </div>
                    </td>
                    {/* Weight */}
                    <td className="px-4 py-2.5">
                      <span className="text-sm text-white">
                        {totalWeightG >= 1000 ? `${(totalWeightG / 1000).toFixed(1)}kg` : `${totalWeightG}g`}
                      </span>
                    </td>
                    {/* Lead time */}
                    <td className="px-4 py-2.5">
                      <span className="text-sm text-bambu-gray">{lt > 0 ? `${lt}d` : '—'}</span>
                    </td>
                    {/* Expected restock */}
                    <td className="px-4 py-2.5">
                      <span className="text-sm text-bambu-gray">
                        {restockDate ? formatDate(restockDate) : '—'}
                      </span>
                    </td>
                    {/* Status badge — read-only */}
                    <td className="px-4 py-2.5">
                      <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${
                        isReceived ? 'bg-bambu-green/20 text-bambu-green' :
                        isPurchased ? 'bg-blue-400/20 text-blue-400' :
                        'bg-bambu-dark-tertiary text-bambu-gray'
                      }`}>
                        {isReceived ? t('forecast.received') : isPurchased ? t('forecast.purchased') : t('forecast.pending')}
                      </span>
                    </td>
                    {/* Note */}
                    <td className="px-4 py-2.5">
                      <span className="text-xs text-bambu-gray">{item.note || '—'}</span>
                    </td>
                    {/* Actions */}
                    <td className="px-4 py-2.5">
                      <div className="flex items-center justify-end gap-1">
                        {canWrite && (
                          <>
                            {/* Purchased icon — available when pending */}
                            <button
                              onClick={() => statusMutation.mutate({ id: item.id, status: isPurchased ? 'pending' : 'purchased' })}
                              disabled={isMutating || isReceived}
                              title={isPurchased ? t('forecast.resetToPending') : t('forecast.markPurchased')}
                              className={`p-1 rounded transition-colors disabled:opacity-30 ${
                                isPurchased
                                  ? 'text-blue-400 hover:text-bambu-gray'
                                  : 'text-bambu-gray hover:text-blue-400'
                              }`}
                            >
                              {isPurchased ? <RotateCcw className="w-3.5 h-3.5" /> : <CreditCard className="w-3.5 h-3.5" />}
                            </button>
                            {/* Received icon — available only after purchasing */}
                            <button
                              onClick={() => statusMutation.mutate({ id: item.id, status: 'received', item, avgSpoolG })}
                              disabled={isMutating || !isPurchased || isReceived}
                              title={t('forecast.markReceived')}
                              className="p-1 rounded transition-colors text-bambu-gray hover:text-bambu-green disabled:opacity-30"
                            >
                              <PackageCheck className="w-3.5 h-3.5" />
                            </button>
                            {/* Delete */}
                            <button
                              onClick={() => onRemove(item.id)}
                              className="p-1 text-bambu-gray hover:text-red-400 transition-colors"
                              title={t('forecast.remove')}
                            >
                              <Trash2 className="w-3.5 h-3.5" />
                            </button>
                          </>
                        )}
                      </div>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      ) : (
        /* Logistics view — exclude received items */
        <div className="divide-y divide-bambu-dark-tertiary">
          {cartForecasts.filter(({ item }) => item.status !== 'received').map(({ item, forecast }) => (
            <CartLogisticsRow
              key={item.id}
              item={item}
              forecast={forecast}
              globalLeadTime={globalLeadTime}
              canWrite={canWrite}
              onRemove={() => onRemove(item.id)}
            />
          ))}
        </div>
      )}
    </div>
  );
}

// ── Cart logistics row ────────────────────────────────────────────────────────

function CartLogisticsRow({
  item, forecast: f, globalLeadTime, canWrite, onRemove,
}: {
  item: ShoppingListItem;
  forecast: SkuForecast | null;
  globalLeadTime: number;
  canWrite: boolean;
  onRemove: () => void;
}) {
  const { t } = useTranslation();
  const label = [item.brand, item.material, item.subtype].filter(Boolean).join(' ');

  // Build a timeline showing stock depletion, arrival bump, then post-arrival depletion.
  // Two points are inserted at day `lt` (just-before and just-after arrival) so the
  // chart shows a clean vertical step rather than a smooth interpolated slope.
  const chartData = useMemo(() => {
    if (!f || f.dailyRateG === null || f.dailyRateG <= 0) return null;
    const rate = f.dailyRateG;
    const lt = f.effectiveLeadTimeDays;
    const avgSpoolG = f.totalSpools > 0 ? f.totalLabelG / f.totalSpools : 1000;
    const arrivalG = item.quantity_spools * avgSpoolG;

    const stockAtArrival = Math.max(0, f.totalRemainingG - rate * lt);
    const peakG = stockAtArrival + arrivalG;
    const daysPostArrival = Math.ceil(peakG / rate);
    const clampedMax = Math.min(lt + daysPostArrival + 5, 365);

    type Point = { day: number; label: string; stock: number; rop: number; safetyStock: number; arrival?: boolean };
    const points: Point[] = [];

    for (let d = 0; d <= clampedMax; d++) {
      const dateLabel = formatDateShort(addDays(new Date(), d));
      if (d === lt) {
        // Just before arrival — pre-bump stock level
        points.push({ day: d, label: dateLabel, stock: Math.round(stockAtArrival), rop: Math.round(f.reorderPointG), safetyStock: Math.round(f.safetyStockG) });
        // Just after arrival — post-bump peak (same x label, creates the vertical step)
        points.push({ day: d, label: dateLabel, stock: Math.round(peakG), rop: Math.round(f.reorderPointG), safetyStock: Math.round(f.safetyStockG), arrival: true });
      } else {
        const stock = d < lt
          ? Math.max(0, f.totalRemainingG - rate * d)
          : Math.max(0, peakG - rate * (d - lt));
        points.push({ day: d, label: dateLabel, stock: Math.round(stock), rop: Math.round(f.reorderPointG), safetyStock: Math.round(f.safetyStockG) });
      }
    }

    return { points, lt, maxDays: clampedMax, arrivalG, peakG, stockAtArrival };
  }, [f, item.quantity_spools]);

  // Determine break scenario: stock hits zero before arrival
  const stockBreaksAt = useMemo(() => {
    if (!f || f.dailyRateG === null || f.dailyRateG <= 0) return null;
    const zeroDay = Math.floor(f.totalRemainingG / f.dailyRateG);
    if (zeroDay < f.effectiveLeadTimeDays) return zeroDay;
    return null;
  }, [f]);

  const hasBreak = stockBreaksAt !== null;

  return (
    <div className={`px-4 py-4 ${hasBreak ? 'bg-red-500/5' : ''}`}>
      {/* Row header */}
      <div className="flex items-center justify-between mb-3">
        <div className="flex items-center gap-2 min-w-0">
          {hasBreak
            ? <AlertTriangle className="w-4 h-4 text-red-400 flex-shrink-0" />
            : <Check className="w-4 h-4 text-bambu-green/60 flex-shrink-0" />
          }
          <span className="text-sm font-medium text-white truncate">{label}</span>
          <span className="text-xs text-bambu-gray flex-shrink-0">{t('forecast.spoolCount', { count: item.quantity_spools })} ordered</span>
        </div>
        {canWrite && (
          <button onClick={onRemove} className="p-1 text-bambu-gray hover:text-red-400 transition-colors flex-shrink-0">
            <Trash2 className="w-3.5 h-3.5" />
          </button>
        )}
      </div>

      {/* Break alert */}
      {hasBreak && (
        <div className="mb-3 px-3 py-2 rounded-lg bg-red-500/10 border border-red-500/20 text-xs text-red-300">
          <span className="font-medium">{t('forecast.stockBreakIn', { days: stockBreaksAt })}</span>
          {' '}{t('forecast.stockRunsOutBefore', { lt: f!.effectiveLeadTimeDays })}
          {f!.dailyRateG !== null && (
            <span> {t('forecast.atRate', { rate: f!.dailyRateG.toFixed(1) })}{' '}
              <span className="font-semibold">{t('forecast.moreSpools', { count: Math.ceil((f!.dailyRateG * f!.effectiveLeadTimeDays - f!.totalRemainingG) / ((f!.totalLabelG / (f!.totalSpools || 1)) || 1000)) })}</span>
              {' '}{t('forecast.bridgeGap')}
            </span>
          )}
        </div>
      )}

      {/* No forecast data */}
      {(!f || f.dailyRateG === null) ? (
        <div className="py-4 text-center text-xs text-bambu-gray">
          {t('forecast.noUsageData')}
        </div>
      ) : chartData ? (
        <>
          {/* Key stats row */}
          <div className="grid grid-cols-5 gap-2 mb-3">
            <div className="bg-bambu-dark-tertiary/40 rounded-lg px-2.5 py-2 text-center">
              <div className="text-xs text-bambu-gray mb-0.5">{t('forecast.stock')}</div>
              <div className="text-sm font-semibold text-white">{Math.round(f.totalRemainingG)}g</div>
            </div>
            <div className="bg-bambu-dark-tertiary/40 rounded-lg px-2.5 py-2 text-center">
              <div className="text-xs text-bambu-gray mb-0.5">{t('forecast.leadTime')}</div>
              <div className="text-sm font-semibold text-white">{f.effectiveLeadTimeDays}d</div>
              <div className="text-[10px] text-bambu-gray/60">max(g:{globalLeadTime}, sku:{f.settings?.lead_time_days ?? 0})</div>
            </div>
            <div className="bg-bambu-dark-tertiary/40 rounded-lg px-2.5 py-2 text-center">
              <div className="text-xs text-bambu-gray mb-0.5">{t('forecast.safetyMarginLabel')}</div>
              <div className="text-sm font-semibold text-white">{Math.round(f.safetyStockG)}g</div>
            </div>
            <div className={`rounded-lg px-2.5 py-2 text-center ${hasBreak ? 'bg-red-500/15' : 'bg-bambu-dark-tertiary/40'}`}>
              <div className="text-xs text-bambu-gray mb-0.5">{t('forecast.daysLeft')}</div>
              <div className={`text-sm font-semibold ${hasBreak ? 'text-red-400' : 'text-green-400'}`}>
                {f.daysRemaining ?? '—'}d
              </div>
            </div>
            {chartData && (
              <div className="bg-bambu-green/15 rounded-lg px-2.5 py-2 text-center">
                <div className="text-xs text-bambu-gray mb-0.5">{t('forecast.onArrival')}</div>
                <div className="text-sm font-semibold text-bambu-green">{Math.round(chartData.arrivalG)}g</div>
                <div className="text-[10px] text-bambu-gray/60">+{t('forecast.spoolCount', { count: item.quantity_spools })}</div>
              </div>
            )}
          </div>

          {/* Chart */}
          <ResponsiveContainer width="100%" height={180}>
            <AreaChart data={chartData.points} margin={{ top: 8, right: 8, bottom: 0, left: 0 }}>
              <defs>
                {/* Pre-arrival fill: red if break, amber if tight, green if ok */}
                <linearGradient id={`cart-pre-${item.id}`} x1="0" y1="0" x2="0" y2="1">
                  <stop offset="5%" stopColor={hasBreak ? '#EF4444' : '#1DB954'} stopOpacity={0.25} />
                  <stop offset="95%" stopColor={hasBreak ? '#EF4444' : '#1DB954'} stopOpacity={0.02} />
                </linearGradient>
                {/* Post-arrival fill: always green */}
                <linearGradient id={`cart-post-${item.id}`} x1="0" y1="0" x2="0" y2="1">
                  <stop offset="5%" stopColor="#1DB954" stopOpacity={0.3} />
                  <stop offset="95%" stopColor="#1DB954" stopOpacity={0.03} />
                </linearGradient>
              </defs>
              <CartesianGrid strokeDasharray="3 3" stroke="#374151" strokeOpacity={0.4} />
              <XAxis
                dataKey="label"
                tick={{ fill: '#6B7280', fontSize: 9 }}
                interval={Math.max(0, Math.ceil(chartData.maxDays / 6) - 1)}
                axisLine={false}
                tickLine={false}
              />
              <YAxis
                tick={{ fill: '#6B7280', fontSize: 9 }}
                axisLine={false}
                tickLine={false}
                tickFormatter={(v: number) => v >= 1000 ? `${(v / 1000).toFixed(1)}kg` : `${v}g`}
                width={44}
              />
              <Tooltip
                contentStyle={{ background: '#1a1a2e', border: '1px solid #374151', borderRadius: 8, fontSize: 11 }}
                labelStyle={{ color: '#9CA3AF' }}
                formatter={(value, name) => {
                  if (typeof value !== 'number') return '';
                  if (name === 'stock') return `${value}g — ${t('forecast.stock')}`;
                  if (name === 'rop') return `${value}g — ${t('forecast.reorderPoint')}`;
                  if (name === 'safetyStock') return `${value}g — ${t('forecast.safetyMarginLabel')}`;
                  return `${value}`;
                }}
              />
              {/* Single stock area — linear interpolation renders the vertical step correctly
                  because the two duplicate-label points at arrival day create an instant jump */}
              <Area
                type="linear"
                dataKey="stock"
                stroke="#1DB954"
                strokeWidth={2}
                fill={`url(#cart-post-${item.id})`}
                dot={false}
                activeDot={{ r: 3 }}
              />
              {/* Reorder point */}
              {f.reorderPointG > 0 && (
                <ReferenceLine
                  y={f.reorderPointG}
                  stroke="#F59E0B"
                  strokeDasharray="5 3"
                  strokeOpacity={0.8}
                  label={{ value: 'ROP', position: 'insideTopRight', fill: '#F59E0B', fontSize: 9 }}
                />
              )}
              {/* Safety stock floor */}
              {f.safetyStockG > 0 && (
                <ReferenceLine
                  y={f.safetyStockG}
                  stroke="#6B7280"
                  strokeDasharray="3 3"
                  strokeOpacity={0.6}
                  label={{ value: 'SS', position: 'insideTopRight', fill: '#6B7280', fontSize: 9 }}
                />
              )}
              {/* Arrival / lead-time-end vertical line */}
              <ReferenceLine
                x={formatDateShort(addDays(new Date(), chartData.lt))}
                stroke="#3B82F6"
                strokeWidth={1.5}
                strokeDasharray="4 3"
                strokeOpacity={0.9}
                label={{ value: `+${chartData.arrivalG >= 1000 ? `${(chartData.arrivalG / 1000).toFixed(1)}kg` : `${Math.round(chartData.arrivalG)}g`} arrives (d${chartData.lt})`, position: 'insideTopLeft', fill: '#3B82F6', fontSize: 9 }}
              />
              {/* Stock break — only shown when stock hits zero before arrival */}
              {stockBreaksAt !== null && (
                <ReferenceLine
                  x={formatDateShort(addDays(new Date(), stockBreaksAt))}
                  stroke="#EF4444"
                  strokeWidth={1.5}
                  strokeOpacity={0.9}
                  label={{ value: 'OUT', position: 'insideTopLeft', fill: '#EF4444', fontSize: 9 }}
                />
              )}
            </AreaChart>
          </ResponsiveContainer>

          {/* Legend */}
          <div className="flex flex-wrap items-center gap-3 mt-2 text-[10px] text-bambu-gray">
            <span className="flex items-center gap-1"><span className="inline-block w-4 border-t-2 border-yellow-400 border-dashed" /> {t('forecast.ropLabel')}</span>
            <span className="flex items-center gap-1"><span className="inline-block w-4 border-t border-bambu-gray border-dashed" /> {t('forecast.safetyStockLegend')}</span>
            <span className="flex items-center gap-1"><span className="inline-block w-4 border-t-2 border-blue-400 border-dashed" /> {t('forecast.stockArrivalLegend')}</span>
            {hasBreak && <span className="flex items-center gap-1 text-red-400"><span className="inline-block w-4 border-t-2 border-red-400" /> {t('forecast.stockoutLegend')}</span>}
          </div>
        </>
      ) : null}
    </div>
  );
}

// ── Add to Cart Modal ─────────────────────────────────────────────────────────

function AddToCartModal({
  forecast: f, onClose, onAdd,
}: {
  forecast: SkuForecast;
  onClose: () => void;
  onAdd: (item: { material: string; subtype: string | null; brand: string | null; quantity_spools: number; note: string | null }) => void;
}) {
  const { t } = useTranslation();
  const label = [f.group.brand, f.group.material, f.group.subtype].filter(Boolean).join(' ');
  const [mode, setMode] = useState<'qty' | 'duration'>('qty');
  const [qty, setQty] = useState('1');
  const [durationDays, setDurationDays] = useState('30');
  const [note, setNote] = useState('');

  const spoolsForDuration = useMemo(() => {
    if (!f.dailyRateG || f.dailyRateG <= 0) return null;
    const neededG = f.dailyRateG * Number(durationDays);
    const avgSpoolG = f.group.spools.length > 0
      ? f.group.spools.reduce((s, sp) => s + sp.label_weight, 0) / f.group.spools.length
      : 1000;
    return Math.ceil(neededG / avgSpoolG);
  }, [f, durationDays]);

  const finalQty = mode === 'qty' ? parseInt(qty, 10) || 1 : (spoolsForDuration ?? 1);

  function submit(e: React.FormEvent) {
    e.preventDefault();
    onAdd({ material: f.group.material, subtype: f.group.subtype, brand: f.group.brand, quantity_spools: finalQty, note: note || null });
  }

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
      <div className="bg-bambu-dark-secondary rounded-2xl border border-bambu-dark-tertiary w-full max-w-sm shadow-2xl">
        <div className="flex items-center justify-between px-5 pt-5 pb-4 border-b border-bambu-dark-tertiary">
          <div className="flex items-center gap-2">
            <ShoppingCart className="w-5 h-5 text-bambu-green" />
            <h2 className="text-base font-semibold text-white">{t('forecast.addToCartTitle')}</h2>
          </div>
          <button onClick={onClose} className="p-1 text-bambu-gray hover:text-white transition-colors"><X className="w-5 h-5" /></button>
        </div>

        <form onSubmit={submit} className="p-5 space-y-4">
          <div className="text-sm text-bambu-gray">{label}</div>

          <div className="flex bg-bambu-dark-tertiary rounded-lg p-0.5">
            <button
              type="button"
              onClick={() => setMode('qty')}
              className={`flex-1 py-1.5 text-xs font-medium rounded-md transition-colors ${mode === 'qty' ? 'bg-bambu-dark-secondary text-white shadow' : 'text-bambu-gray hover:text-white'}`}
            >
              {t('forecast.byQuantity')}
            </button>
            <button
              type="button"
              onClick={() => setMode('duration')}
              className={`flex-1 py-1.5 text-xs font-medium rounded-md transition-colors ${mode === 'duration' ? 'bg-bambu-dark-secondary text-white shadow' : 'text-bambu-gray hover:text-white'}`}
            >
              {t('forecast.byDuration')}
            </button>
          </div>

          {mode === 'qty' ? (
            <div className="space-y-1.5">
              <label className="text-xs text-bambu-gray">{t('forecast.numberOfSpools')}</label>
              <input
                type="number" min={1} max={99}
                value={qty} onChange={(e) => setQty(e.target.value)}
                className="w-full px-3 py-2 bg-bambu-dark-tertiary border border-bambu-dark-tertiary rounded-lg text-white text-sm focus:outline-none focus:border-bambu-green"
                autoFocus
              />
            </div>
          ) : (
            <div className="space-y-2">
              <div className="space-y-1.5">
                <label className="text-xs text-bambu-gray">{t('forecast.lastHowManyDays')}</label>
                <input
                  type="number" min={1} max={365}
                  value={durationDays} onChange={(e) => setDurationDays(e.target.value)}
                  className="w-full px-3 py-2 bg-bambu-dark-tertiary border border-bambu-dark-tertiary rounded-lg text-white text-sm focus:outline-none focus:border-bambu-green"
                  autoFocus
                />
              </div>
              {f.dailyRateG !== null ? (
                <div className="flex items-center gap-2 px-3 py-2 bg-bambu-dark-tertiary/50 rounded-lg">
                  <span className="text-xs text-bambu-gray">≈</span>
                  <span className="text-sm font-semibold text-bambu-green">{t('forecast.spoolCount', { count: spoolsForDuration ?? 0 })}</span>
                  <span className="text-xs text-bambu-gray">at {f.dailyRateG.toFixed(1)}g/day</span>
                </div>
              ) : (
                <div className="text-xs text-yellow-400 px-1">{t('forecast.noUsageQty')}</div>
              )}
            </div>
          )}

          <div className="space-y-1.5">
            <label className="text-xs text-bambu-gray">{t('forecast.noteOptional')}</label>
            <input
              type="text" maxLength={200}
              value={note} onChange={(e) => setNote(e.target.value)}
              placeholder={t('forecast.notePlaceholder')}
              className="w-full px-3 py-2 bg-bambu-dark-tertiary border border-bambu-dark-tertiary rounded-lg text-white text-sm placeholder:text-bambu-gray/40 focus:outline-none focus:border-bambu-green"
            />
          </div>

          <div className="flex items-center gap-3 pt-1">
            <button
              type="submit"
              className="flex-1 py-2 bg-bambu-green text-white text-sm font-medium rounded-lg hover:bg-bambu-green/80 transition-colors"
            >
              {t('forecast.addNSpools', { count: finalQty })}
            </button>
            <button type="button" onClick={onClose} className="px-4 py-2 text-sm text-bambu-gray hover:text-white border border-bambu-dark-tertiary rounded-lg transition-colors">
              {t('forecast.cancel')}
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}

// ── Column headers (re-exported for InventoryPage) ────────────────────────────

export function ForecastColumnHeaders() {
  return null;
}
