/* ==================================================================
   Overview layer — shared primitives
   ------------------------------------------------------------------
   Every overview screen (Dashboard, Academics, Assessment, Parents,
   Finance, Staff Attendance) is built from the pieces in this
   file, so the reference design's stat card / pill / meter / row is
   defined exactly once. Screens below import them off window.CX.

   Loaded before the screens that use it — see index.html.

   Naming: everything here is prefixed Cx (or lives under window.CX) so it
   cannot collide with the two dozen pre-existing screen globals that share
   this scope. Babel-standalone gives every <script type="text/babel"> the
   same top-level scope, so a bare `const Card` here would be a redeclaration
   error the moment another screen does the same.
   ================================================================== */

/* ---------- Data fetching ---------- */

// One hook behind every cockpit screen. Returns { data, error, loading,
// reload } and re-fetches when `path` changes.
//
// `loading` is only true on the FIRST load for a given path. A refresh keeps
// the previous data on screen and flips `refreshing` instead — a dashboard
// that blanks itself to skeletons every 60 seconds is unusable, and the
// numbers rarely move enough to be worth the flash.
const useCockpitData = (path, { pollMs = 0 } = {}) => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);
  const [refreshing, setRefreshing] = useState(false);
  const seen = useRef(false);

  const load = useCallback(async (quiet) => {
    if (quiet) setRefreshing(true); else setLoading(!seen.current);
    try {
      const d = await window.AppApi.get(path);
      setData(d);
      setError(null);
      seen.current = true;
    } catch (e) {
      // Keep whatever is on screen; surface the failure alongside it rather
      // than replacing real numbers with an error page.
      setError(e?.message || "Could not load this screen.");
    } finally {
      setLoading(false);
      setRefreshing(false);
    }
  }, [path]);

  useEffect(() => { seen.current = false; load(false); }, [load]);

  useEffect(() => {
    if (!pollMs) return;
    const t = setInterval(() => load(true), pollMs);
    return () => clearInterval(t);
  }, [pollMs, load]);

  return { data, error, loading, refreshing, reload: () => load(true) };
};

/* ---------- Formatting ---------- */

const cxNum = (n) => (n == null ? "—" : Number(n).toLocaleString("en-IN"));

// Indian-format currency, abbreviated at lakh/crore because a dashboard tile
// cannot hold "₹51,97,700" at 46px without wrapping.
const cxMoney = (n, { full = false } = {}) => {
  if (n == null) return "—";
  const v = Number(n);
  if (full) return "₹" + v.toLocaleString("en-IN", { maximumFractionDigits: 0 });
  const abs = Math.abs(v);
  if (abs >= 1e7) return "₹" + (v / 1e7).toFixed(abs >= 1e8 ? 0 : 2).replace(/\.00$/, "") + " Cr";
  if (abs >= 1e5) return "₹" + (v / 1e5).toFixed(abs >= 1e6 ? 1 : 2).replace(/\.00$/, "") + " L";
  if (abs >= 1e3) return "₹" + (v / 1e3).toFixed(0) + "k";
  return "₹" + v.toLocaleString("en-IN", { maximumFractionDigits: 0 });
};

const cxPct = (n) => (n == null ? "—" : `${Number(n)}%`);

const cxInitials = (name) =>
  String(name || "?")
    .trim().split(/\s+/).map((w) => w[0]).join("").slice(0, 2).toUpperCase() || "?";

// Deterministic avatar tint, so the same person is the same colour every
// render and across screens.
const CX_AV_PALETTE = [
  ["var(--blue-bg)",   "var(--blue)"],
  ["var(--pink-bg)",   "var(--pink)"],
  ["var(--green-bg)",  "var(--green)"],
  ["var(--amber-bg)",  "var(--amber)"],
  ["var(--violet-bg)", "var(--violet)"],
];
const cxAvatarTint = (seed) => {
  const s = String(seed || "");
  let h = 0;
  for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0;
  return CX_AV_PALETTE[h % CX_AV_PALETTE.length];
};

const cxRelTime = (iso) => {
  if (!iso) return "—";
  const ms = Date.now() - new Date(iso).getTime();
  if (Number.isNaN(ms)) return "—";
  const s = Math.floor(ms / 1000);
  if (s < 60) return "just now";
  if (s < 3600) return `${Math.floor(s / 60)}m ago`;
  if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
  if (s < 7 * 86400) return `${Math.floor(s / 86400)}d ago`;
  return new Date(iso).toLocaleDateString("en-IN", { day: "numeric", month: "short" });
};

const cxDate = (iso, opts) => {
  if (!iso) return "—";
  const d = new Date(iso.length === 10 ? iso + "T00:00:00" : iso);
  if (Number.isNaN(d.getTime())) return "—";
  return d.toLocaleDateString("en-IN", opts || { day: "numeric", month: "short" });
};

const cxTime = (iso) => {
  if (!iso) return "—";
  const d = new Date(iso);
  if (Number.isNaN(d.getTime())) return "—";
  return d.toLocaleTimeString("en-IN", { hour: "numeric", minute: "2-digit" });
};

const cxTitle = (s) =>
  String(s || "").replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());


/* ---------- Icons (reference set: 24-box, 1.8 stroke) ---------- */

const CX_PATHS = {
  staff: <><path d="M17 21v-2a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v2"/><circle cx="10" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></>,
  building: <><path d="M3 21h18"/><path d="M5 21V7l7-4 7 4v14"/><path d="M9 21v-6h6v6"/></>,
  pulse: <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>,
  cap: <><path d="M22 10 12 5 2 10l10 5 10-5z"/><path d="M6 12v5c0 1.7 2.7 3 6 3s6-1.3 6-3v-5"/></>,
  clock: <><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></>,
  heart: <path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1-1.1a5.5 5.5 0 0 0-7.8 7.8l8.8 8.8 8.8-8.8a5.5 5.5 0 0 0 0-7.8z"/>,
  chat: <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>,
  alert: <><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></>,
  bolt: <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>,
  smile: <><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/></>,
  rupee: <><path d="M6 3h12"/><path d="M6 8h12"/><path d="M6 13h4a5 5 0 0 0 0-10"/><path d="M6 13l8 8"/></>,
  wallet: <><path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/><path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/><path d="M18 12a2 2 0 0 0 0 4h4v-4z"/></>,
  box: <><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></>,
  bell: <><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></>,
  arrowUpRight: <><line x1="7" y1="17" x2="17" y2="7"/><polyline points="7 7 17 7 17 17"/></>,
  arrowLeft: <><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></>,
  download: <><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></>,
  activity: <><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></>,
  plus: <><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></>,
  plug: <><path d="M12 22v-5"/><path d="M9 8V2"/><path d="M15 8V2"/><path d="M18 8v3a6 6 0 0 1-12 0V8z"/></>,
  book: <><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></>,
  shield: <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>,
  users: <><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/></>,
  file: <><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></>,
  arrowRight: <><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></>,
  chevronLeft: <polyline points="15 18 9 12 15 6"/>,
  chevronRight: <polyline points="9 18 15 12 9 6"/>,
  printer: <><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></>,
  x: <><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></>,
  calendar: <><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></>,
  filter: <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/>,
  search: <><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></>,
};

const CxIcon = ({ name, size = 18, strokeWidth = 1.8, style }) => (
  <svg viewBox="0 0 24 24" width={size} height={size} fill="none"
       stroke="currentColor" strokeWidth={strokeWidth}
       strokeLinecap="round" strokeLinejoin="round" style={style} aria-hidden="true">
    {CX_PATHS[name] || CX_PATHS.pulse}
  </svg>
);

/* ---------- Building blocks ---------- */
// These are thin wrappers over the shared primitive layer in shell.jsx
// (window.AppUI) and the ui-* / cx-* classes in styles.css, so the overview
// screens share one design language with the rest of the admin panel:
// hairline cards, compact outline buttons, segmented tabs, tinted pills.

const CxCard = ({ children, className = "", style, ...rest }) => (
  <div className={`cx-card ${className}`} style={style} {...rest}>{children}</div>
);

// KPI tile: tinted square icon + muted label, big tabular figure, then a
// muted footer line. `color` is a status token (var(--green-solid) …); the
// tile tint is derived from it, never a new colour.
const CxStatCard = ({
  icon, color, label, value, delta, deltaTone = "up", suffix,
  footNote, action, onAction, actionLabel,
}) => (
  <div className="cx-card cx-stat">
    <div className="cx-stat-head">
      <div style={{ display: "flex", alignItems: "center", gap: 10, minWidth: 0 }}>
        <div className="cx-stat-icon"
             style={{ background: `color-mix(in srgb, ${color} 14%, transparent)`, color }}>
          <CxIcon name={icon} size={16}/>
        </div>
        <div className="cx-stat-label">{label}</div>
      </div>
      {onAction && (
        <button type="button" className="ui-iconbtn ghost sm" onClick={onAction}
                title={actionLabel || "Open"} aria-label={actionLabel || "Open"}>
          <CxIcon name="arrowUpRight" size={15}/>
        </button>
      )}
    </div>
    <div className="cx-stat-row">
      <span className="cx-stat-value">{value}</span>
      {suffix && <span style={{ fontSize: 14, fontWeight: 500, color: "var(--ink-3)" }}>{suffix}</span>}
      {delta && <span className={`cx-delta ${deltaTone}`}>{delta}</span>}
    </div>
    <div className="cx-stat-foot" style={{ borderTop: "1px solid var(--line)", paddingTop: 12 }}>
      <span style={{ color: "var(--ink-3)", minWidth: 0 }}>{footNote}</span>
      {action}
    </div>
  </div>
);

// Status pill. tone → ui-badge tint; "dark" → solid primary.
const CxPill = ({ tone = "", children, className = "", ...rest }) => {
  const variant = tone === "dark" ? "solid" : "tint";
  const t = tone === "dark" ? "" : tone;
  return (
    <span className={`ui-badge ${variant} ${t} ${className}`}
          style={tone === "dark" ? { background: "var(--primary)", color: "var(--primary-ink)" } : undefined}
          {...rest}>{children}</span>
  );
};

// Neutral label chip for card headers ("Today", "Last 8 months").
const CxTag = ({ children, title }) => (
  <span className="ui-badge tint grey" title={title}>{children}</span>
);

const CxMeter = ({ pct, color = "var(--blue-solid)", className = "" }) => (
  <div className={`cx-meter ${className}`}>
    <div style={{ width: `${Math.max(0, Math.min(100, Number(pct) || 0))}%`, background: color }}/>
  </div>
);

const CxAvatar = ({ name, size = "" }) => {
  const [bg, fg] = cxAvatarTint(name);
  return <div className={`cx-av ${size}`} style={{ background: bg, color: fg }}>{cxInitials(name)}</div>;
};

// Segmented control. `size="sm"` (default) for toolbars, "" for page tabs.
const CxSeg = ({ value, options, onChange, size = "sm", ariaLabel }) => (
  <div className={`ui-tabs${size ? " " + size : ""}`} role="tablist" aria-label={ariaLabel}>
    {options.map((o) => (
      <button key={o.value} type="button" role="tab" aria-selected={value === o.value}
              className={value === o.value ? "active" : ""}
              onClick={() => onChange(o.value)}>{o.label}</button>
    ))}
  </div>
);

// Page header: optional "← Back" link, eyebrow, 26px title with an optional
// status pill, muted meta line and right-aligned actions. Renders through
// the shared PageHeader so every screen's header is the same object.
const CxPageHead = ({ eyebrow, title, actions, onBack, backLabel, status, meta, avatar, toolbar }) => {
  const PH = window.AppUI && window.AppUI.PageHeader;
  if (PH) {
    return <PH onBack={onBack} backLabel={backLabel || "Back"} eyebrow={eyebrow} avatar={avatar}
               title={title} status={status} meta={meta} actions={actions} toolbar={toolbar}/>;
  }
  return (
    <div className="cx-page-head">
      <div style={{ minWidth: 0 }}>
        {onBack && (
          <button type="button" className="ui-back" onClick={onBack}>
            <CxIcon name="arrowLeft" size={14}/>{backLabel || "Back"}
          </button>
        )}
        {eyebrow && <div className="cx-eyebrow">{eyebrow}</div>}
        <h1 className="cx-h1">{title}</h1>
      </div>
      {actions && <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>{actions}</div>}
    </div>
  );
};

// Card section header: 16px title, optional muted sub, right-side node.
const CxCardHead = ({ title, sub, right, style }) => (
  <div className="cx-card-head" style={style}>
    <div style={{ minWidth: 0 }}>
      <div className="cx-card-title">{title}</div>
      {sub && <div className="cx-card-sub">{sub}</div>}
    </div>
    {right}
  </div>
);

// Card footer: hairline, muted note left, compact outline link button right.
const CxCardFoot = ({ note, action, onAction }) => (
  <div className="cx-card-foot">
    <span style={{ fontSize: 13, color: "var(--ink-3)", minWidth: 0 }}>{note}</span>
    {action && onAction && (
      <button type="button" className="btn sm" onClick={onAction}>
        {action}<CxIcon name="arrowRight" size={14}/>
      </button>
    )}
  </div>
);

// Shown where a card has nothing to draw. Same quiet look as the shared
// EmptyState, so a quiet section reads as "nothing here yet" rather than
// as a rendering failure.
const CxEmpty = ({ icon = "box", title, hint }) => (
  <div className="ui-empty compact">
    <div className="ui-empty-mark"><CxIcon name={icon} size={18}/></div>
    {title && <div className="ui-empty-title">{title}</div>}
    {hint && <div className="ui-empty-hint">{hint}</div>}
  </div>
);

// The "system not connected" card. This exists so an unwired subsystem looks
// deliberate and tells you what to do, instead of showing a zero that reads as
// a real measurement.
const CxNotConnected = ({ title, what, why }) => (
  <div className="cx-card">
    <div className="ui-empty">
      <div className="ui-empty-mark" style={{ background: "var(--amber-bg)", color: "var(--amber)" }}>
        <CxIcon name="plug" size={18}/>
      </div>
      <div className="ui-empty-title">{title}</div>
      <div className="ui-empty-hint">{what}</div>
      {why && <div className="ui-empty-hint" style={{ fontSize: 12, color: "var(--ink-4)" }}>{why}</div>}
    </div>
  </div>
);

// Card-shaped skeletons, sized to the layout they replace so the page doesn't
// jump when the data lands.
//
// The column count is a CSS custom property rather than an inline
// grid-template-columns, so the stylesheet can collapse it to one column on a
// phone. Inline, it stayed at three columns at 390px and rendered as three
// slivers with the rest of the screen blank — which read as "the app has
// finished and there is nothing here" rather than "still loading".
const CxSkeleton = ({ rows = 3, height = 150 }) => (
  <div className="cx-skelgrid" style={{ "--cx-skel-cols": rows }}>
    {Array.from({ length: rows }).map((_, i) => (
      <div key={i} className="cx-skel" style={{ height, borderRadius: "var(--radius-card)" }}/>
    ))}
  </div>
);

// Standard wrapper: skeleton while loading, an inline banner if the fetch
// failed, then the screen. The banner sits ABOVE the content rather than
// replacing it, because a stale dashboard is more useful than an error page.
const CxScreen = ({ state, children, skeleton }) => (
  <>
    {state.error && (
      <div className="ui-alert red" style={{ marginBottom: 20, display: "flex", alignItems: "center", gap: 10 }}>
        <CxIcon name="alert" size={16} style={{ flexShrink: 0 }}/>
        <span style={{ minWidth: 0 }}>{state.error}</span>
        <button type="button" className="btn sm" style={{ marginLeft: "auto" }} onClick={state.reload}>
          Retry
        </button>
      </div>
    )}
    {state.loading && !state.data ? (skeleton || <CxSkeleton/>) : children}
  </>
);

/* ---------- Donut ---------- */

// Conic-gradient ring with a white hole, as in the reference. Segments are
// [{ value, color }]; anything left over is drawn in the track colour.
const CxDonut = ({ segments, total, centreValue, centreLabel, size = 170 }) => {
  const sum = segments.reduce((a, s) => a + (Number(s.value) || 0), 0);
  const denom = total || sum || 1;
  let acc = 0;
  const stops = segments.map((s) => {
    const from = (acc / denom) * 100;
    acc += Number(s.value) || 0;
    const to = (acc / denom) * 100;
    return `${s.color} ${from}% ${to}%`;
  });
  stops.push(`var(--bg-3) ${(acc / denom) * 100}% 100%`);
  const hole = Math.round(size * 0.74);
  return (
    <div style={{
      width: size, height: size, borderRadius: "50%",
      background: `conic-gradient(${stops.join(", ")})`,
      display: "grid", placeItems: "center", flexShrink: 0,
    }}>
      <div style={{
        width: hole, height: hole, borderRadius: "50%", background: "var(--surface)",
        display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center",
      }}>
        <span style={{ fontSize: 28, fontWeight: 600, letterSpacing: "-0.8px", lineHeight: 1, color: "var(--ink-0)", fontVariantNumeric: "tabular-nums" }}>
          {centreValue}
        </span>
        <span style={{ fontSize: 12, color: "var(--ink-3)", marginTop: 4 }}>{centreLabel}</span>
      </div>
    </div>
  );
};

/* ---------- Labelled bar list ---------- */

const CxBarList = ({ items, showValue = true }) => (
  <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
    {items.map((it, i) => (
      <div key={i} style={{ display: "flex", alignItems: "center", gap: 10, fontSize: 13 }}>
        <span style={{ width: 62, fontWeight: 500, color: "var(--ink-0)", flexShrink: 0 }}>{it.label}</span>
        <div style={{ flex: 1, minWidth: 0 }}>
          <CxMeter pct={it.pct} color={it.color || "var(--blue-solid)"}/>
        </div>
        {showValue && (
          <span style={{ width: 44, textAlign: "right", color: "var(--ink-3)", flexShrink: 0, fontVariantNumeric: "tabular-nums" }}>{it.value}</span>
        )}
      </div>
    ))}
  </div>
);

/* ---------- Column chart (the rounded bars from the reference) ---------- */

const CxColumns = ({ items, height = 170 }) => (
  <div>
    <div style={{ display: "flex", alignItems: "flex-end", gap: 12, height }}>
      {items.map((it, i) => (
        <div key={i} title={it.title || `${it.label}: ${it.value}`}
             style={{
               flex: 1, maxWidth: 86,
               height: `${Math.max(6, Number(it.heightPct) || 0)}%`,
               background: it.color, borderRadius: "var(--radius-tile)",
               position: "relative", overflow: "hidden", minWidth: 0,
             }}>
          <span style={{
            position: "absolute", top: 10, left: 10,
            background: "color-mix(in srgb, var(--surface) 92%, transparent)", borderRadius: 999,
            padding: "2px 8px", fontSize: 12, fontWeight: 600, color: "var(--ink-0)",
            fontVariantNumeric: "tabular-nums",
          }}>{it.value}</span>
        </div>
      ))}
    </div>
    <div style={{ display: "flex", gap: 12, marginTop: 8 }}>
      {items.map((it, i) => (
        <div key={i} style={{
          flex: 1, maxWidth: 86, textAlign: "center",
          fontSize: 11, fontWeight: 500, color: "var(--ink-3)",
          textTransform: "uppercase", letterSpacing: "0.06em",
        }}>{it.label}</div>
      ))}
    </div>
  </div>
);

/* ---------- Month calendar ---------- */

// Renders the month containing `anchor` (an ISO date string), dotting any day
// that appears in `events` and ringing today.
const CxCalendar = ({ anchor, today, events, onMonthChange, offset = 0 }) => {
  const base = new Date((anchor || today) + "T00:00:00");
  const view = new Date(base.getFullYear(), base.getMonth() + offset, 1);
  const year = view.getFullYear(), month = view.getMonth();
  const daysInMonth = new Date(year, month + 1, 0).getDate();
  // Monday-first grid: JS getDay() is Sunday-first, so rotate.
  const lead = (new Date(year, month, 1).getDay() + 6) % 7;

  const byDay = new Map();
  for (const e of events || []) {
    const d = new Date(e.date + "T00:00:00");
    if (d.getFullYear() === year && d.getMonth() === month) {
      if (!byDay.has(d.getDate())) byDay.set(d.getDate(), []);
      byDay.get(d.getDate()).push(e);
    }
  }

  const todayD = new Date(today + "T00:00:00");
  const isThisMonth = todayD.getFullYear() === year && todayD.getMonth() === month;

  const cells = [];
  for (let i = 0; i < lead; i++) cells.push(null);
  for (let d = 1; d <= daysInMonth; d++) cells.push(d);

  return (
    <>
      <div className="cx-card-head" style={{ marginBottom: 14 }}>
        <div className="cx-card-title">
          {view.toLocaleDateString("en-IN", { month: "long", year: "numeric" })}
        </div>
        <div className="btn-group">
          <button type="button" className="btn sm icon-only" onClick={() => onMonthChange(offset - 1)}
                  aria-label="Previous month" title="Previous month"><CxIcon name="chevronLeft" size={15}/></button>
          <button type="button" className="btn sm icon-only" onClick={() => onMonthChange(offset + 1)}
                  aria-label="Next month" title="Next month"><CxIcon name="chevronRight" size={15}/></button>
        </div>
      </div>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(7,1fr)", gap: 2, marginBottom: 4 }}>
        {["MO", "TU", "WE", "TH", "FR", "SA", "SU"].map((w) => (
          <div key={w} style={{ textAlign: "center", fontSize: 11, fontWeight: 500, letterSpacing: "0.06em", color: "var(--ink-3)", padding: "4px 0" }}>{w}</div>
        ))}
      </div>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(7,1fr)", gap: 2, marginBottom: 14 }}>
        {cells.map((d, i) => {
          if (d == null) return <div key={i} style={{ height: 34 }}/>;
          const evs = byDay.get(d) || [];
          const isToday = isThisMonth && d === todayD.getDate();
          return (
            <div key={i}
                 title={evs.map((e) => e.title).join(" · ") || undefined}
                 style={{
                   height: 34, display: "flex", alignItems: "center", justifyContent: "center",
                   fontSize: 13, fontWeight: isToday ? 600 : 400, borderRadius: "var(--radius-ctl)",
                   position: "relative", fontVariantNumeric: "tabular-nums",
                   background: isToday ? "var(--primary)" : "transparent",
                   color: isToday ? "var(--primary-ink)" : "var(--ink-0)",
                 }}>
              {d}
              {evs.length > 0 && (
                <span style={{
                  position: "absolute", bottom: 3, left: "50%", marginLeft: -2,
                  width: 4, height: 4, borderRadius: "50%",
                  background: isToday ? "var(--primary-ink)" : evs[0].color,
                }}/>
              )}
            </div>
          );
        })}
      </div>
    </>
  );
};

/* ---------- CSV export ---------- */

// Client-side CSV. Kept here because three screens offer an export and the
// quoting rule (double every quote, wrap every field) must not be re-derived.
const cxExportCsv = (filename, rows) => {
  const csv = rows
    .map((r) => r.map((v) => `"${String(v ?? "").replace(/"/g, '""')}"`).join(","))
    .join("\n");
  const a = document.createElement("a");
  a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv;charset=utf-8" }));
  a.download = filename;
  a.click();
  URL.revokeObjectURL(a.href);
};

window.CX = {
  useCockpitData,
  num: cxNum, money: cxMoney, pct: cxPct, initials: cxInitials,
  avatarTint: cxAvatarTint, relTime: cxRelTime, date: cxDate, time: cxTime,
  title: cxTitle,
  Icon: CxIcon, Card: CxCard, StatCard: CxStatCard, Pill: CxPill, Tag: CxTag, Meter: CxMeter,
  Avatar: CxAvatar, Seg: CxSeg, PageHead: CxPageHead, CardHead: CxCardHead, CardFoot: CxCardFoot,
  Empty: CxEmpty,
  NotConnected: CxNotConnected, Skeleton: CxSkeleton, Screen: CxScreen,
  Donut: CxDonut, BarList: CxBarList, Columns: CxColumns, Calendar: CxCalendar,
  exportCsv: cxExportCsv,
};
