// Shared utilities + small primitives
// Exported via window at bottom

const fmt = {
  g: (n) => (n == null ? "—" : `${(+n).toFixed(2).replace(/\.?0+$/, "") || "0"} g`),
  money: (n) => (n == null ? "—" : `$${(+n).toFixed(2)}`),
  moneyShort: (n) => (n == null ? "—" : n >= 100 ? `$${Math.round(n)}` : `$${(+n).toFixed(2)}`),
  pct: (n) => (n == null ? "—" : `${(+n).toFixed(1)}%`),
  date: (s) => {
    if (!s) return "—";
    const d = new Date(s);
    return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
  },
  dateShort: (s) => {
    if (!s) return "—";
    const d = new Date(s);
    return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
  },
  daysAgo: (s) => {
    if (!s) return "—";
    const ms = Date.now() - new Date(s).getTime();
    const d = Math.floor(ms / 86400000);
    if (d === 0) return "today";
    if (d === 1) return "yesterday";
    if (d < 30) return `${d}d ago`;
    if (d < 365) return `${Math.floor(d/30)}mo ago`;
    return `${Math.floor(d/365)}y ago`;
  }
};

const TYPE_GLYPHS = {
  "Flower": "✿",
  "Concentrate": "◆",
  "Edible": "◐",
  "Vaporizer": "▢",
  "Pre-roll": "│",
  "Tincture": "◯"
};

// Compute aggregate stats — derived from purchases + consumed/gone records + audits
function computeStats(data) {
  const today = new Date(data.today || "2026-04-25");
  const todayStr = today.toISOString().slice(0, 10);
  const products = data.products;
  const H = window.DATA_HELPERS;
  const dayKey = (d) => d.toISOString().slice(0, 10);

  const active = products.filter(p => p.status === "active");
  const consumed = products.filter(p => p.status === "consumed" && p.consumedDate);
  const gone = products.filter(p => p.status === "gone");

  // Window helper for purchases
  const purchasesIn = (days) => {
    const cutoff = new Date(today); cutoff.setDate(cutoff.getDate() - days);
    return products.filter(p => new Date(p.purchaseDate) >= cutoff);
  };
  const last7p = purchasesIn(7);
  const last30p = purchasesIn(30);
  const last90p = purchasesIn(90);

  // Bulk-grams equivalent: bulk uses weight (or ml for tincture, ignored from grams);
  // discrete uses unitWeight × units consumed
  const bulkGrams = (p) => {
    if (p.type === "Tincture" || p.type === "Edible") return 0; // not "weed grams"
    if (p.kind === "bulk") return p.weight;
    return (p.countOriginal || 0) * (p.unitWeight || 0);
  };
  const bulkGramsConsumed = (p) => {
    // For consumed products: full weight equivalent
    if (p.type === "Tincture" || p.type === "Edible") return 0;
    if (p.kind === "bulk") return p.weight;
    return (p.countOriginal || 0) * (p.unitWeight || 0);
  };
  const bulkGramsUsedSoFar = (p) => {
    // For active products: estimated grams used to date
    if (p.type === "Tincture" || p.type === "Edible") return 0;
    if (p.kind === "bulk") {
      const est = H.estimatedRemaining(p, todayStr);
      return Math.max(0, p.weight - est);
    }
    // discrete: (original - current) × unitWeight
    const cur = p.countLastAudit != null ? p.countLastAudit : p.countOriginal;
    return Math.max(0, (p.countOriginal - cur)) * (p.unitWeight || 0);
  };

  // Daily attribution: spread used grams over (purchase → consumed/today) for consumed/active.
  // GONE items contribute $0 NOT grams (they were lost, not used).
  const dailyGramsAttribution = {};
  consumed.forEach(p => {
    const g = bulkGramsConsumed(p);
    if (g <= 0) return;
    const start = new Date(p.purchaseDate);
    const end = new Date(p.consumedDate);
    const days = Math.max(1, Math.round((end - start) / 86400000));
    const perDay = g / days;
    for (let i = 0; i < days; i++) {
      const d = new Date(start); d.setDate(d.getDate() + i);
      dailyGramsAttribution[dayKey(d)] = (dailyGramsAttribution[dayKey(d)] || 0) + perDay;
    }
  });
  active.forEach(p => {
    const used = bulkGramsUsedSoFar(p);
    if (used <= 0) return;
    const start = new Date(p.purchaseDate);
    const days = Math.max(1, Math.round((today - start) / 86400000));
    const perDay = used / days;
    for (let i = 0; i < days; i++) {
      const d = new Date(start); d.setDate(d.getDate() + i);
      dailyGramsAttribution[dayKey(d)] = (dailyGramsAttribution[dayKey(d)] || 0) + perDay;
    }
  });

  const seriesFor = (days) => {
    const out = [];
    for (let i = days - 1; i >= 0; i--) {
      const d = new Date(today); d.setDate(d.getDate() - i);
      const k = dayKey(d);
      out.push({ date: k, grams: dailyGramsAttribution[k] || 0 });
    }
    return out;
  };
  const series7 = seriesFor(7);
  const series30 = seriesFor(30);
  const series90 = seriesFor(90);

  const sumG = (xs) => xs.reduce((s, x) => s + x.grams, 0);
  const dailyAvg = sumG(series30) / 30;
  const weeklyAvg = sumG(series30) / (30/7);
  const monthlyAvg = sumG(series90) / 3;

  // Spend
  const totalSpend = products.reduce((s, p) => s + p.price, 0);
  const goneSpend = gone.reduce((s, p) => s + p.price, 0);
  const totalGrams = products.reduce((s, p) => s + bulkGrams(p), 0);
  const avgPerGram = totalGrams ? totalSpend / totalGrams : 0;
  const spend30 = last30p.reduce((s, p) => s + p.price, 0);
  const spend7 = last7p.reduce((s, p) => s + p.price, 0);
  const spend90 = last90p.reduce((s, p) => s + p.price, 0);

  // Inventory value (active, prorated by est. remaining %)
  const inventoryValue = active.reduce((s, p) => {
    return s + p.price * H.pctRemaining(p, todayStr);
  }, 0);

  // THC mg using avg THC of products
  const avgThc = products.length ? products.reduce((s,p)=>s+p.thc,0) / products.length : 20;
  const thcLast7 = Math.round(sumG(series7) * avgThc * 10);
  const thcLast30 = Math.round(sumG(series30) * avgThc * 10);

  // Avg lifespan of consumed
  const lifespans = consumed.map(p => Math.max(1, Math.round((new Date(p.consumedDate) - new Date(p.purchaseDate))/86400000)));
  const avgLifespan = lifespans.length ? lifespans.reduce((a,b)=>a+b,0) / lifespans.length : 0;

  // Favorite shop / brand — keyed by id, look up name
  const shopCount = {};
  const brandCount = {};
  products.forEach(p => {
    if (p.shopId) shopCount[p.shopId] = (shopCount[p.shopId] || 0) + 1;
    if (p.brandId) brandCount[p.brandId] = (brandCount[p.brandId] || 0) + 1;
  });
  const topShopEntry = Object.entries(shopCount).sort((a,b)=>b[1]-a[1])[0];
  const topBrandEntry = Object.entries(brandCount).sort((a,b)=>b[1]-a[1])[0];
  const favShop = topShopEntry ? [H.shopName(data, topShopEntry[0]), topShopEntry[1]] : ["—", 0];
  const favBrand = topBrandEntry ? [H.brandName(data, topBrandEntry[0]), topBrandEntry[1]] : ["—", 0];

  // Type breakdown by est. grams on hand (active only)
  const typeBreakdown = {};
  active.forEach(p => {
    let g;
    if (p.type === "Tincture") g = H.estimatedRemaining(p, todayStr) * 0.5; // ml → display weight rough
    else if (p.type === "Edible") g = (p.countLastAudit != null ? p.countLastAudit : p.countOriginal) * 0.3; // each gummy ~0.3g for chart only
    else if (p.kind === "bulk") g = H.estimatedRemaining(p, todayStr);
    else g = (p.countLastAudit != null ? p.countLastAudit : p.countOriginal) * (p.unitWeight || 0);
    if (g > 0) typeBreakdown[p.type] = (typeBreakdown[p.type] || 0) + g;
  });

  // Flower-equivalent supply
  const flowerEquivalent = active
    .filter(p => p.type === "Flower" || p.type === "Pre-roll")
    .reduce((s, p) => {
      if (p.kind === "bulk") return s + H.estimatedRemaining(p, todayStr);
      return s + (p.countLastAudit != null ? p.countLastAudit : p.countOriginal) * (p.unitWeight || 0);
    }, 0);
  const daysOfSupply = dailyAvg > 0 ? flowerEquivalent / dailyAvg : 0;

  // Avg days between buys
  const sortedDates = [...products].sort((a,b)=>new Date(a.purchaseDate)-new Date(b.purchaseDate)).map(p => new Date(p.purchaseDate));
  const gaps = [];
  for (let i = 1; i < sortedDates.length; i++) {
    gaps.push((sortedDates[i] - sortedDates[i-1]) / 86400000);
  }
  const avgGap = gaps.length ? gaps.reduce((a,b)=>a+b,0)/gaps.length : 0;

  // ─── Audits & low stock ───────────────────────────────────────
  const overdueAudits = active.filter(p => H.auditOverdue(data, p, todayStr));

  // Low stock:
  // - Bulk: pctRemaining < 0.25
  // - Discrete: GROUPED BY (brand + type) — total count ≤ 2
  const lowStockBulk = active.filter(p => p.kind === "bulk" && H.pctRemaining(p, todayStr) < 0.25);

  const discreteBrandGroups = {};
  active.filter(p => p.kind === "discrete").forEach(p => {
    const k = `${p.brandId}|${p.type}|${p.name}`;
    if (!discreteBrandGroups[k]) {
      discreteBrandGroups[k] = {
        key: k, name: p.name, type: p.type, brandId: p.brandId,
        items: [], totalCount: 0
      };
    }
    discreteBrandGroups[k].items.push(p);
    discreteBrandGroups[k].totalCount += (p.countLastAudit != null ? p.countLastAudit : p.countOriginal);
  });
  const lowStockDiscreteGroups = Object.values(discreteBrandGroups).filter(g => g.totalCount <= 2);

  return {
    dailyAvg, weeklyAvg, monthlyAvg,
    totalSpend, avgPerGram, spend7, spend30, spend90, goneSpend,
    inventoryValue,
    thcLast7, thcLast30,
    avgLifespan,
    favShop, favBrand,
    typeBreakdown,
    daysOfSupply,
    avgGap,
    series7, series30, series90,
    activeCount: active.length,
    consumedCount: consumed.length,
    goneCount: gone.length,
    archivedCount: consumed.length + gone.length,
    overdueAudits,
    lowStockBulk,
    lowStockDiscreteGroups
  };
}

// Subtle inline icons (1px stroke)
const Icon = ({name, size = 18, color = "currentColor"}) => {
  const paths = {
    home: "M3 11l9-8 9 8M5 9v12h5v-7h4v7h5V9",
    box: "M3 7l9-4 9 4v10l-9 4-9-4V7zM3 7l9 4 9-4M12 11v10",
    chart: "M3 21V3M3 21h18M7 17v-7M11 17v-4M15 17v-9M19 17v-2",
    plus: "M12 5v14M5 12h14",
    check: "M5 13l4 4L19 7",
    settings: "M12 3v3M12 18v3M5 5l2 2M17 17l2 2M3 12h3M18 12h3M5 19l2-2M17 7l2-2M12 8a4 4 0 100 8 4 4 0 000-8z",
    search: "M11 19a8 8 0 100-16 8 8 0 000 16zM21 21l-4-4",
    filter: "M3 5h18M6 12h12M10 19h4",
    bin: "M4 7h16M9 7V4h6v3M6 7v13h12V7",
    leaf: "M5 19c0-7 5-14 14-14 0 9-5 14-14 14zM5 19l7-7",
    flame: "M12 3c1 4 4 5 4 9a4 4 0 11-8 0c0-2 2-3 2-6 0-1 1-2 2-3z",
    droplet: "M12 3l5 7a5 5 0 11-10 0l5-7z",
    arrow: "M5 12h14M13 5l7 7-7 7",
    arrowDown: "M12 5v14M5 13l7 7 7-7",
    close: "M6 6l12 12M18 6L6 18",
    edit: "M4 20h4l10-10-4-4L4 16v4zM14 6l4 4",
    star: "M12 3l3 6 7 1-5 5 1 7-6-3-6 3 1-7-5-5 7-1z",
    calendar: "M5 5h14v15H5zM3 10h18M9 3v4M15 3v4",
    tag: "M3 12V3h9l9 9-9 9-9-9zM7 7h.01"
  };
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{flexShrink: 0}}>
      <path d={paths[name] || ""} />
    </svg>
  );
};

// Sparkline
const Sparkline = ({values, width = 120, height = 32, color = "var(--ink)", fill = false}) => {
  if (!values || values.length === 0) return null;
  const max = Math.max(...values, 0.001);
  const min = Math.min(...values, 0);
  const span = max - min || 1;
  const step = width / (values.length - 1 || 1);
  const pts = values.map((v, i) => [i * step, height - ((v - min) / span) * (height - 4) - 2]);
  const path = pts.map((p, i) => (i === 0 ? "M" : "L") + p[0].toFixed(1) + " " + p[1].toFixed(1)).join(" ");
  const fillPath = fill ? path + ` L ${width} ${height} L 0 ${height} Z` : null;
  return (
    <svg width={width} height={height} style={{display: "block", overflow: "visible"}}>
      {fillPath && <path d={fillPath} fill={color} opacity="0.12" />}
      <path d={path} stroke={color} strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
};

// Bar chart
const BarChart = ({data, height = 160, color = "var(--sage)", labels = false}) => {
  if (!data || !data.length) return null;
  const max = Math.max(...data.map(d => d.value), 0.001);
  return (
    <div style={{display: "flex", alignItems: "flex-end", gap: 2, height, width: "100%"}}>
      {data.map((d, i) => (
        <div key={i} style={{flex: 1, display: "flex", flexDirection: "column", alignItems: "center", gap: 4, minWidth: 0}}>
          <div style={{
            width: "100%",
            height: `${(d.value / max) * 100}%`,
            background: d.value > 0 ? color : "var(--line)",
            borderRadius: "2px 2px 0 0",
            minHeight: d.value > 0 ? 2 : 1,
            opacity: d.muted ? 0.4 : 1
          }} />
          {labels && <div style={{fontSize: 9, color: "var(--ink-3)", fontFamily: "var(--mono)"}}>{d.label}</div>}
        </div>
      ))}
    </div>
  );
};

// Donut chart
const Donut = ({segments, size = 160, thickness = 22}) => {
  const total = segments.reduce((s, x) => s + x.value, 0);
  const r = size / 2 - thickness / 2;
  const c = 2 * Math.PI * r;
  let offset = 0;
  return (
    <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} style={{transform: "rotate(-90deg)"}}>
      <circle cx={size/2} cy={size/2} r={r} stroke="var(--line)" strokeWidth={thickness} fill="none" opacity="0.3" />
      {segments.map((s, i) => {
        const len = (s.value / total) * c;
        const dash = `${len} ${c - len}`;
        const el = (
          <circle key={i} cx={size/2} cy={size/2} r={r} stroke={s.color} strokeWidth={thickness} fill="none"
            strokeDasharray={dash} strokeDashoffset={-offset} strokeLinecap="butt" />
        );
        offset += len;
        return el;
      })}
    </svg>
  );
};

// Reusable card
const Card = ({children, style, padded = true, ...rest}) => {
  const { padded: _ignored, ...domProps } = rest;
  return (
    <div {...domProps} style={{
      background: "var(--surface)",
      border: "1px solid var(--line)",
      borderRadius: "var(--r-md)",
      padding: padded ? 20 : 0,
      ...style
    }}>{children}</div>
  );
};

// Stat card
const Stat = ({label, value, unit, sub, spark, accent, big}) => (
  <div style={{
    background: "var(--surface)",
    border: "1px solid var(--line)",
    borderRadius: "var(--r-md)",
    padding: 18,
    display: "flex",
    flexDirection: "column",
    gap: 8,
    minWidth: 0
  }}>
    <div className="smallcaps" style={{color: "var(--ink-3)"}}>{label}</div>
    <div style={{display: "flex", alignItems: "baseline", gap: 6}}>
      <div className="serif" style={{fontSize: big ? 44 : 32, lineHeight: 1, color: accent || "var(--ink)", fontWeight: 500, letterSpacing: "-0.01em"}}>{value}</div>
      {unit && <div style={{fontSize: 13, color: "var(--ink-3)", fontFamily: "var(--mono)"}}>{unit}</div>}
    </div>
    {sub && <div style={{fontSize: 12, color: "var(--ink-3)"}}>{sub}</div>}
    {spark && <div style={{marginTop: 4}}>{spark}</div>}
  </div>
);

// Pill / badge
const Pill = ({children, tone = "neutral", style}) => {
  const tones = {
    neutral: { bg: "var(--bg-3)", color: "var(--ink-2)" },
    sage: { bg: "var(--sage-soft)", color: "var(--sage)" },
    terra: { bg: "var(--terracotta-soft)", color: "var(--terracotta)" },
    amber: { bg: "var(--amber-soft)", color: "oklch(48% 0.10 75)" },
    outline: { bg: "transparent", color: "var(--ink-2)", border: "1px solid var(--line-strong)" }
  };
  const t = tones[tone] || tones.neutral;
  return (
    <span style={{
      display: "inline-flex",
      alignItems: "center",
      gap: 4,
      padding: "3px 8px",
      borderRadius: 999,
      background: t.bg,
      color: t.color,
      fontSize: 11,
      fontWeight: 500,
      letterSpacing: "0.02em",
      border: t.border || "1px solid transparent",
      ...style
    }}>{children}</span>
  );
};

// Button — high contrast across themes
const Btn = ({children, variant = "ghost", icon, onClick, style, type, disabled}) => {
  // Disabled: keep solid surface, dim the text only — never become low-contrast on bg
  const variants = {
    primary: disabled
      ? { background: "var(--bg-3)", color: "var(--ink-3)", border: "1px solid var(--line-strong)" }
      : { background: "var(--ink)", color: "var(--bg)", border: "1px solid var(--ink)" },
    secondary: { background: "var(--surface)", color: "var(--ink)", border: "1px solid var(--line-strong)" },
    ghost: { background: "transparent", color: "var(--ink-2)", border: "1px solid transparent" },
    danger: { background: "var(--terracotta)", color: "oklch(98% 0.01 40)", border: "1px solid var(--terracotta)" },
    sage: { background: "var(--sage)", color: "oklch(98% 0.01 145)", border: "1px solid var(--sage)" }
  };
  const v = variants[variant];
  return (
    <button type={type} onClick={onClick} disabled={disabled} style={{
      display: "inline-flex",
      alignItems: "center",
      gap: 6,
      padding: "8px 14px",
      borderRadius: "var(--r-md)",
      fontSize: 13,
      fontWeight: 600,
      transition: "all 120ms",
      cursor: disabled ? "not-allowed" : "pointer",
      ...v,
      ...style
    }}>
      {icon && <Icon name={icon} size={14} />}
      {children}
    </button>
  );
};

// Field
const Field = ({label, children, hint, span = 1}) => (
  <label style={{display: "flex", flexDirection: "column", gap: 6, gridColumn: `span ${span}`}}>
    <span className="smallcaps" style={{color: "var(--ink-3)"}}>{label}</span>
    {children}
    {hint && <span style={{fontSize: 11, color: "var(--ink-3)"}}>{hint}</span>}
  </label>
);

const inputStyle = {
  background: "var(--bg)",
  border: "1px solid var(--line)",
  borderRadius: "var(--r-md)",
  padding: "10px 12px",
  fontSize: 13,
  color: "var(--ink)",
  outline: "none",
  fontFamily: "var(--sans)",
  width: "100%"
};

const Input = (props) => <input style={inputStyle} {...props} />;
const Select = ({children, ...rest}) => <select style={{...inputStyle, appearance: "auto"}} {...rest}>{children}</select>;
const Textarea = (props) => <textarea style={{...inputStyle, minHeight: 80, resize: "vertical"}} {...props} />;

Object.assign(window, {
  fmt, computeStats, TYPE_GLYPHS,
  Icon, Sparkline, BarChart, Donut,
  Card, Stat, Pill, Btn, Field, Input, Select, Textarea, inputStyle
});
