// ─────────────────────────────────────────────────────────────────────────
// Meridian — shared library
// Icon shim, layout primitives, finance widgets, theme-aware Chart.js
// wrappers, and the async-data hook. Exported to window for Babel scopes.
// ─────────────────────────────────────────────────────────────────────────
const { useState, useEffect, useRef, useMemo, useCallback } = React;

/* ── Lucide icon shim ─────────────────────────────────────────────────────── */
function Icon({ name, size = 16, className = "", style, strokeWidth = 1.75 }) {
  const ref = useRef(null);
  useEffect(() => {
    if (!ref.current || !window.lucide) return;
    ref.current.innerHTML = "";
    const node = window.lucide[toPascal(name)] || window.lucide.Circle;
    try {
      const el = window.lucide.createElement(node);
      el.setAttribute("width", size);
      el.setAttribute("height", size);
      el.setAttribute("stroke-width", strokeWidth);
      el.setAttribute("class", "lucide" + (className ? " " + className : ""));
      ref.current.appendChild(el);
    } catch (e) { /* unknown icon — leave empty */ }
  }, [name, size, className, strokeWidth]);
  return <span ref={ref} style={{ display: "inline-flex", flexShrink: 0, lineHeight: 0, ...style }} />;
}
function toPascal(s) { return String(s).replace(/(^|-)([a-z])/g, (_, __, c) => c.toUpperCase()); }

/* ── Layout primitives ────────────────────────────────────────────────────── */
function Row({ children, gap = 8, wrap = false, align = "center", justify = "flex-start", style, className = "", ...rest }) {
  return <div className={className} style={{ display: "flex", flexDirection: "row", gap, alignItems: align, justifyContent: justify, flexWrap: wrap ? "wrap" : "nowrap", ...style }} {...rest}>{children}</div>;
}
function Col({ children, gap = 8, style, className = "", ...rest }) {
  return <div className={className} style={{ display: "flex", flexDirection: "column", gap, ...style }} {...rest}>{children}</div>;
}
function Card({ children, hover = false, className = "", style, ...rest }) {
  return <div className={"card" + (hover ? " card-hover" : "") + (className ? " " + className : "")} style={style} {...rest}>{children}</div>;
}
function SectionTitle({ children, action, icon }) {
  return (
    <Row justify="space-between" align="center" style={{ marginBottom: 14 }}>
      <Row gap={8}>
        {icon && <Icon name={icon} size={16} style={{ color: "var(--fg-3)" }} />}
        <h2 style={{ fontSize: "var(--fs-lg)", fontWeight: 600, letterSpacing: "var(--tracking-tight)", margin: 0 }}>{children}</h2>
      </Row>
      {action}
    </Row>
  );
}
function PageHeader({ title, subtitle, actions }) {
  return (
    <Row justify="space-between" align="flex-end" wrap style={{ gap: 16, marginBottom: 24 }}>
      <div>
        <h1 style={{ fontSize: "var(--fs-2xl)", fontWeight: 600, letterSpacing: "var(--tracking-tighter)", margin: 0, lineHeight: 1.1 }}>{title}</h1>
        {subtitle && <p className="muted" style={{ fontSize: 14, margin: "5px 0 0", lineHeight: 1.5 }}>{subtitle}</p>}
      </div>
      {actions && <Row gap={8} wrap>{actions}</Row>}
    </Row>
  );
}

/* ── Brand mark ───────────────────────────────────────────────────────────── */
function Logo({ size = 30, withText = false }) {
  const mark = (
    <div aria-hidden style={{ width: size, height: size, borderRadius: size * 0.32, background: "linear-gradient(140deg, var(--sage-500), var(--sage-800))", boxShadow: "var(--elev-1)", position: "relative", flexShrink: 0, display: "grid", placeItems: "center" }}>
      <svg width={size * 0.6} height={size * 0.6} viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.95)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
        <path d="M3 17 L9 10 L13 14 L21 5" />
        <path d="M21 5 L21 10 M21 5 L16 5" />
      </svg>
    </div>
  );
  if (!withText) return mark;
  return (
    <Row gap={10}>
      {mark}
      <div style={{ lineHeight: 1.1 }}>
        <div style={{ fontWeight: 600, fontSize: 16, letterSpacing: "-0.02em" }}>Meridian</div>
        <div style={{ fontSize: 10, color: "var(--fg-3)", marginTop: 1 }}>Personal investing</div>
      </div>
    </Row>
  );
}

/* ── Finance widgets ──────────────────────────────────────────────────────── */
function Delta({ value, kind = "eur", showArrow = true, size, weight = 500, style }) {
  const v = (typeof value === "number" && isFinite(value)) ? value : 0;
  const pos = v >= 0;
  const text = kind === "pct" ? fmt.sPct(v) : kind === "eur" ? fmt.sEur(v) : (pos ? "+" : "−") + fmt.num(Math.abs(v));
  return (
    <span className={"delta " + (pos ? "pos" : "neg")} style={{ fontSize: size, fontWeight: weight, ...style }}>
      {showArrow && <Icon name={pos ? "arrow-up-right" : "arrow-down-right"} size={size ? size * 0.85 : 13} />}
      {text}
    </span>
  );
}

function SignalBadge({ signal, size = "" }) {
  const map = { buy: "Buy", hold: "Hold", sell: "Sell" };
  const s = map[signal] ? signal : "hold";
  return <span className={"badge badge-" + s} style={size === "lg" ? { fontSize: 13, padding: "4px 12px" } : null}>{map[s]}</span>;
}

function ClassBadge({ kind }) {
  const map = { signal: "Signal", mixed: "Mixed", hype: "Hype", uncertain: "Uncertain" };
  return <span className={"badge badge-" + kind}>{map[kind] || kind}</span>;
}

function aaqsColor(s) {
  if (s == null) return "var(--fg-3)";
  if (s >= 8) return "var(--sage-700)";
  if (s >= 6) return "var(--mixed-accent)";
  return "var(--hype-accent)";
}
function AaqsMeter({ score, showVal = true, width = 56 }) {
  if (score == null) return <span className="muted-2 mono" style={{ fontSize: 12 }}>—</span>;
  const c = aaqsColor(score);
  return (
    <Row gap={8} style={{ display: "inline-flex" }}>
      {showVal && <span className="mono" style={{ fontSize: 13, fontWeight: 500, color: c, minWidth: 26, textAlign: "right" }}>{fmt.num(score)}</span>}
      <span className="meter" style={{ width }}><span style={{ width: (Math.max(0, Math.min(10, score)) / 10 * 100) + "%", background: c }} /></span>
    </Row>
  );
}

// fair-value gap. positive = undervalued (good, to the right)
function FvBar({ gap, width = 64, showVal = true }) {
  if (gap == null) return <span className="muted-2 mono" style={{ fontSize: 12 }}>—</span>;
  const under = gap >= 0;
  const mag = Math.min(Math.abs(gap), 40) / 40;
  const color = under ? "var(--pos-fg)" : "var(--neg-fg)";
  return (
    <Row gap={8} style={{ display: "inline-flex" }} align="center">
      {showVal && <span className="mono" style={{ fontSize: 13, color, minWidth: 44, textAlign: "right", fontWeight: 500 }}>{fmt.sPct(gap)}</span>}
      <span className="fvbar"><span className="track" style={{ width }}>
        <span className="mid" />
        <span className="fill" style={under
          ? { left: "50%", width: (mag * 50) + "%", background: color }
          : { right: "50%", width: (mag * 50) + "%", background: color }} />
      </span></span>
    </Row>
  );
}

function Avatar({ initials, size = "", gradient }) {
  return <div className={"avatar" + (size ? " avatar-" + size : "")} style={gradient ? { background: gradient, color: "#fff", border: 0 } : null}>{initials}</div>;
}

// Company logo for a stock. Pulls a free SVG from Parqet by ISIN (preferred)
// or ticker symbol — no API key. On any load error, or when neither ISIN nor
// ticker is known, it falls back to the original 2-letter initials box in the
// exact prior optic (var(--bg-tint), mono, var(--fg-2)).
function StockLogo({ isin, ticker, name, size = 34, radius, initialsFontSize, style }) {
  const [failed, setFailed] = useState(false);
  const r = radius != null ? radius : Math.round(size * 0.25);
  const src = isin
    ? "https://assets.parqet.com/logos/isin/" + encodeURIComponent(isin)
    : ticker ? "https://assets.parqet.com/logos/symbol/" + encodeURIComponent(ticker) : null;
  // Reset the error state when we point at a different stock (the detail header
  // reuses one instance across navigations).
  useEffect(() => { setFailed(false); }, [src]);

  if (!src || failed) {
    return (
      <div style={{ width: size, height: size, borderRadius: r, background: "var(--bg-tint)", display: "grid", placeItems: "center", flexShrink: 0, fontWeight: 600, fontSize: initialsFontSize != null ? initialsFontSize : Math.round(size * 0.34), color: "var(--fg-2)", fontFamily: "var(--font-mono)", ...style }}>
        {String(name || ticker || "").slice(0, 2).toUpperCase()}
      </div>
    );
  }
  return (
    <img
      src={src}
      alt={name || ticker || isin || "logo"}
      loading="lazy"
      onError={() => setFailed(true)}
      style={{ width: size, height: size, borderRadius: r, objectFit: "contain", background: "var(--bg-tint)", padding: 3, flexShrink: 0, ...style }}
    />
  );
}

function Sparkline({ points, width = 88, height = 28, up }) {
  const vals = (points || []).map((p) => (typeof p === "number" ? p : p.v));
  if (vals.length < 2) return <svg width={width} height={height} />;
  const min = Math.min(...vals), max = Math.max(...vals);
  const rng = max - min || 1;
  const step = width / (vals.length - 1);
  const d = vals.map((v, i) => `${i === 0 ? "M" : "L"} ${(i * step).toFixed(1)} ${(height - ((v - min) / rng) * (height - 4) - 2).toFixed(1)}`).join(" ");
  const rising = up != null ? up : vals[vals.length - 1] >= vals[0];
  const c = rising ? "var(--pos-strong)" : "var(--neg-strong)";
  return (
    <svg width={width} height={height} style={{ display: "block", overflow: "visible" }}>
      <path d={d} fill="none" stroke={c} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

/* ── Chart.js helpers ─────────────────────────────────────────────────────── */
function cssVar(name) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); }
function resolveColor(c) {
  if (!c) return c;
  c = String(c).trim();
  if (c.startsWith("var(")) { const n = c.slice(4, -1).trim(); return cssVar(n) || c; }
  return c;
}

function LineChart({ data, height = 260, theme, color, fill = true, euro = true, valueFmt }) {
  const ref = useRef(null);
  const chart = useRef(null);
  useEffect(() => {
    if (!ref.current || !window.Chart || !data || data.length === 0) return;
    const ctx = ref.current.getContext("2d");
    const line = resolveColor(color) || cssVar("--sage-600") || "#557D65";
    const grid = cssVar("--border-1");
    const tick = cssVar("--fg-3");
    const surf = cssVar("--bg-surface");
    const ink = cssVar("--fg-1");
    const muted = cssVar("--fg-2");
    const g = ctx.createLinearGradient(0, 0, 0, height);
    g.addColorStop(0, hexA(line, 0.18));
    g.addColorStop(1, hexA(line, 0.0));
    if (chart.current) chart.current.destroy();
    chart.current = new window.Chart(ctx, {
      type: "line",
      data: {
        labels: data.map((d) => d.t),
        datasets: [{
          data: data.map((d) => d.v),
          borderColor: line, borderWidth: 2,
          backgroundColor: fill ? g : "transparent",
          fill, tension: 0.28, pointRadius: 0, pointHoverRadius: 4,
          pointHoverBackgroundColor: line, pointHoverBorderColor: surf, pointHoverBorderWidth: 2,
        }],
      },
      options: {
        responsive: true, maintainAspectRatio: false,
        interaction: { mode: "index", intersect: false },
        plugins: {
          legend: { display: false },
          tooltip: {
            backgroundColor: surf, titleColor: muted, bodyColor: ink,
            borderColor: grid, borderWidth: 1, padding: 10, cornerRadius: 10,
            displayColors: false, titleFont: { family: "Geist Mono", size: 11 },
            bodyFont: { family: "Geist Mono", size: 13, weight: "600" },
            callbacks: {
              title: (it) => fmt.date(it[0].label),
              label: (it) => valueFmt ? valueFmt(it.parsed.y) : (euro ? fmt.eur0(it.parsed.y) : fmt.num(it.parsed.y)),
            },
          },
        },
        scales: {
          x: { display: false, grid: { display: false } },
          y: {
            position: "right",
            grid: { color: grid, drawTicks: false },
            border: { display: false },
            ticks: { color: tick, font: { family: "Geist Mono", size: 10 }, maxTicksLimit: 5, padding: 8,
              callback: (v) => euro ? "€" + (v >= 1000 ? (v / 1000).toFixed(0) + "k" : v) : fmt.num0(v) },
          },
        },
      },
    });
    return () => { if (chart.current) chart.current.destroy(); };
  }, [data, theme, color]);
  if (!data || data.length === 0) return <div style={{ height, display: "grid", placeItems: "center" }} className="muted-2">No data</div>;
  return <div className="chart-wrap" style={{ height }}><canvas ref={ref} /></div>;
}

function Donut({ slices, height = 220, theme, centerLabel, centerValue }) {
  const ref = useRef(null);
  const chart = useRef(null);
  useEffect(() => {
    if (!ref.current || !window.Chart || !slices || slices.length === 0) return;
    const ctx = ref.current.getContext("2d");
    const surf = cssVar("--bg-surface");
    const ink = cssVar("--fg-1");
    const muted = cssVar("--fg-2");
    const grid = cssVar("--border-1");
    if (chart.current) chart.current.destroy();
    chart.current = new window.Chart(ctx, {
      type: "doughnut",
      data: {
        labels: slices.map((s) => s.label),
        datasets: [{
          data: slices.map((s) => s.value),
          backgroundColor: slices.map((s) => resolveColor(s.color)),
          borderColor: surf, borderWidth: 2.5, hoverBorderWidth: 2.5, hoverOffset: 6,
        }],
      },
      options: {
        responsive: true, maintainAspectRatio: false, cutout: "68%",
        plugins: {
          legend: { display: false },
          tooltip: {
            backgroundColor: surf, titleColor: ink, bodyColor: muted,
            borderColor: grid, borderWidth: 1, padding: 10, cornerRadius: 10,
            displayColors: true, boxWidth: 8, boxHeight: 8, usePointStyle: true,
            titleFont: { family: "Geist", size: 13, weight: "600" },
            bodyFont: { family: "Geist Mono", size: 12 },
            callbacks: { label: (it) => " " + fmt.eur0(it.parsed) + "  ·  " + fmt.pct(slices[it.dataIndex].pct) },
          },
        },
      },
    });
    return () => { if (chart.current) chart.current.destroy(); };
  }, [slices, theme]);
  return (
    <div className="chart-wrap" style={{ height, position: "relative" }}>
      {slices && slices.length > 0 ? <canvas ref={ref} /> : <div style={{ height, display: "grid", placeItems: "center" }} className="muted-2">No data</div>}
      {centerValue && (
        <div style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center", pointerEvents: "none" }}>
          <div style={{ textAlign: "center" }}>
            <div className="kpi-label">{centerLabel}</div>
            <div style={{ fontSize: 20, fontWeight: 600, letterSpacing: "-0.02em", fontVariantNumeric: "tabular-nums" }}>{centerValue}</div>
          </div>
        </div>
      )}
    </div>
  );
}

function StackBar({ slices, height = 10 }) {
  return (
    <div style={{ display: "flex", height, borderRadius: 999, overflow: "hidden", background: "var(--bg-tint)" }}>
      {slices.map((s, i) => <div key={i} title={s.label} style={{ width: s.pct + "%", background: s.color }} />)}
    </div>
  );
}

function hexA(c, a) {
  c = (c || "").trim();
  if (c.startsWith("#")) {
    let h = c.slice(1);
    if (h.length === 3) h = h.split("").map((x) => x + x).join("");
    const n = parseInt(h, 16);
    return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${a})`;
  }
  return c;
}

const CHART_PALETTE = ["#3D604B", "#6F9C7F", "#9FBFA9", "#D4A24A", "#5B91BC", "#E07A5F", "#557D65", "#C8DCCE", "#8A8E85", "#2F5440", "#B0935A"];

function EmptyState({ icon = "inbox", title, body, action }) {
  return (
    <Col gap={10} align="center" style={{ padding: "44px 20px", textAlign: "center" }}>
      <div style={{ width: 46, height: 46, borderRadius: 14, background: "var(--bg-tint)", display: "grid", placeItems: "center", color: "var(--fg-3)" }}>
        <Icon name={icon} size={20} />
      </div>
      <div style={{ fontWeight: 600, fontSize: 15 }}>{title}</div>
      {body && <div className="muted" style={{ fontSize: 13, maxWidth: 340 }}>{body}</div>}
      {action}
    </Col>
  );
}

function Skel({ w = "100%", h = 14, r = 6, style }) {
  return <div className="skel" style={{ width: w, height: h, borderRadius: r, ...style }} />;
}
// a card-shaped loading block
function SkelCard({ lines = 3, h = 120 }) {
  return (
    <Card>
      <Col gap={10}>
        <Skel w="40%" h={16} />
        {Array.from({ length: lines }).map((_, i) => <Skel key={i} w={i === lines - 1 ? "70%" : "100%"} h={12} />)}
      </Col>
    </Card>
  );
}

/* relative time vs real now */
function relTime(iso) {
  if (!iso) return "—";
  const d = new Date(iso), now = new Date();
  if (isNaN(d)) return "—";
  const m = Math.round((now - d) / 60000);
  if (m < 1) return "just now";
  if (m < 60) return m + " min ago";
  const h = Math.round(m / 60);
  if (h < 24) return h + " h ago";
  return fmt.date(d);
}

/* ── async data hook ─────────────────────────────────────────────────────── */
// useAsync(fn, deps) → { data, loading, error, reload }
function useAsync(fn, deps = []) {
  const [state, setState] = useState({ data: null, loading: true, error: null });
  const fnRef = useRef(fn); fnRef.current = fn;
  const run = useCallback(() => {
    let live = true;
    setState((s) => ({ ...s, loading: true, error: null }));
    Promise.resolve().then(fnRef.current).then(
      (data) => { if (live) setState({ data, loading: false, error: null }); },
      (error) => { if (live) setState({ data: null, loading: false, error }); }
    );
    return () => { live = false; };
  }, deps);
  useEffect(run, deps);
  return { ...state, reload: run };
}

Object.assign(window, {
  // React hooks — exposed so every Babel screen scope can reference them
  useState, useEffect, useRef, useMemo, useCallback,
  Icon, Row, Col, Card, SectionTitle, PageHeader, Logo,
  Delta, SignalBadge, ClassBadge, AaqsMeter, FvBar, Avatar, StockLogo, Sparkline,
  LineChart, Donut, StackBar, EmptyState, Skel, SkelCard, relTime, useAsync,
  cssVar, hexA, resolveColor, aaqsColor, CHART_PALETTE,
});
