// Shared UI primitives + helpers used across feature-group files.
//
// Lives between the data modules (which provide window.useGuides etc.) and
// the feature-group files (note-screen.jsx, guide-screens.jsx, ...) so its
// exports are available everywhere downstream via window.* lookup.
//
// Rule for what goes here: used by 2+ consumers (multiple feature groups,
// or one feature group + the app.jsx shell). Single-consumer helpers stay
// with their consumer. See CLAUDE.md "File layout" for the full split.
//
// IIFE-wrapped so module-local declarations don't pollute Babel's shared
// script scope. Explicit Object.assign(window, { ... }) at the bottom is
// what makes these reachable from other files.

(function () {

// ─── Tiny hash router ──────────────────────────────────────────────────────

function parseHash() {
  const h = window.location.hash;
  return h.startsWith('#') ? (h.slice(1) || '/') : '/';
}
function navigate(p) {
  if (window.location.hash !== '#' + p) window.location.hash = p;
}
function useRoute() {
  const [path, setPath] = React.useState(parseHash());
  React.useEffect(() => {
    const on = () => setPath(parseHash());
    window.addEventListener('hashchange', on);
    if (!window.location.hash) navigate('/');
    return () => window.removeEventListener('hashchange', on);
  }, []);
  return path;
}
function match(pat, path) {
  const pp = pat.split('/').filter(Boolean);
  const ap = path.split('/').filter(Boolean);
  if (pp.length !== ap.length) return null;
  const params = {};
  for (let i = 0; i < pp.length; i++) {
    if (pp[i].startsWith(':')) params[pp[i].slice(1)] = decodeURIComponent(ap[i]);
    else if (pp[i] !== ap[i]) return null;
  }
  return params;
}

// ─── Skeleton gate ─────────────────────────────────────────────────────────
// Skeleton gate. Returns true (show skeleton) when EITHER the cache isn't
// loaded yet OR the per-mount minimum-display window hasn't elapsed.
// _kickLoaders warms every cache at sign-in, so by the time the user navigates
// to a screen via the in-app router `loaded` is already true and the bare
// `!loaded` check never trips. The min-window gives every screen mount a
// perceptible loading hint without lying about an actual fetch.
function useSkeletonGate(loaded, minMs = 320) {
  const [pastMin, setPastMin] = React.useState(false);
  React.useEffect(() => {
    const t = setTimeout(() => setPastMin(true), minMs);
    return () => clearTimeout(t);
  }, []);
  return !pastMin || !loaded;
}

// ─── Formatters ────────────────────────────────────────────────────────────

function relativeTime(iso) {
  if (!iso) return '';
  const sec = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
  if (sec < 60)    return 'just now';
  if (sec < 3600)  return Math.floor(sec / 60) + 'm ago';
  if (sec < 86400) return Math.floor(sec / 3600) + 'h ago';
  return Math.floor(sec / 86400) + 'd ago';
}
function formatChars(n) {
  if (n < 1000) return n + ' chars';
  if (n < 1000000) return (n / 1000).toFixed(1) + 'k chars';
  return (n / 1000000).toFixed(2) + 'M chars';
}
function formatBytes(b) {
  if (b < 1024) return b + ' B';
  if (b < 1024 * 1024) return (b / 1024).toFixed(1) + ' KB';
  return (b / 1024 / 1024).toFixed(1) + ' MB';
}

// ─── AI branding glyph ────────────────────────────────────────────────────
// Tiny "AI" branding glyph — a frosted-glass icon in /icons. Drop in next to
// every label or button that's about the in-app AI features so the brand mark
// reads consistently across the dashboard, note + guide AI panels, AI-edit
// dialogs, and smart-extract checkboxes. Sized like an inline glyph so it
// behaves like a one-character prefix.
function AiGlyph({ size = 14, style }) {
  return (
    <img
      src="icons/ai%20glass.png"
      alt=""
      aria-hidden="true"
      className="ai-glyph"
      style={{
        width: size,
        height: size,
        verticalAlign: '-2px',
        marginRight: 6,
        flexShrink: 0,
        ...style,
      }}
    />
  );
}

// ─── Nav icons ────────────────────────────────────────────────────────────
// SVG icon set used in the sidebar nav. Inlined so they pick up currentColor.
const NAV_ICONS = {
  dashboard: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="3" width="7" height="9" rx="2"/><rect x="14" y="3" width="7" height="5" rx="2"/><rect x="14" y="12" width="7" height="9" rx="2"/><rect x="3" y="16" width="7" height="5" rx="2"/></svg>,
  library:   <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><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"/></svg>,
  calendar:  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="4" width="18" height="18" rx="3"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>,
  notes:     <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>,
  community: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="9" cy="7" r="4"/><path d="M17 11a4 4 0 1 0-3-7.5"/><path d="M3 21a6 6 0 0 1 12 0"/><path d="M21 21a6 6 0 0 0-3-5.2"/></svg>,
  search:    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>,
  chevDown:  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M6 9l6 6 6-6"/></svg>,
  chevLeft:  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M15 18l-6-6 6-6"/></svg>,
};

// ─── Page chrome ──────────────────────────────────────────────────────────
// Page renders Sidebar, DashboardDoodles, and doodleGroupForPath via bare
// identifiers — those still live in app.jsx (Sidebar) or Wave 7's
// dashboard.jsx (DashboardDoodles, doodleGroupForPath). They resolve via
// window at render time because those files declare them with `function`
// at top level (non-IIFE). If Wave 7 moves DashboardDoodles into an IIFE
// without Object.assign-ing it, Page will start rendering without doodles
// until that's fixed.

function Topbar({ crumbs, actions, leadingActions }) {
  // Show only the last crumb as the lead, with parent crumbs as clickable links
  // separated by " / ". Matches the comp's simplified topbar.
  const items = crumbs || [];
  return (
    <div className="cd-topbar">
      <div className="cd-crumb">
        {leadingActions && <span style={{ marginRight: 10 }}>{leadingActions}</span>}
        {items.map((c, i) => {
          const isLast = i === items.length - 1;
          const label = typeof c === 'object' ? c.label : c;
          const to    = typeof c === 'object' ? c.to    : null;
          return (
            <React.Fragment key={i}>
              {i > 0 && <span className="sep">/</span>}
              {isLast || !to
                ? <span>{label}</span>
                : <a onClick={() => navigate(to)}>{label}</a>}
            </React.Fragment>
          );
        })}
      </div>
      <div className="cd-top-actions">{actions}</div>
    </div>
  );
}

function Page({ children, crumbs, actions, leadingActions, hideChrome }) {
  if (hideChrome) {
    return <div className="app no-chrome"><div className="app-main">{children}</div></div>;
  }
  const path = useRoute();
  return (
    <div className="app">
      <Sidebar path={path} />
      <main className="cd-main" style={{ position: 'relative', isolation: 'isolate' }}>
        <DashboardDoodles group={doodleGroupForPath(path)} />
        <Topbar crumbs={crumbs} actions={actions} leadingActions={leadingActions} />
        {children}
      </main>
    </div>
  );
}

// ─── Calendar math ────────────────────────────────────────────────────────

function monthCells(y, m) {
  const first = new Date(y, m, 1);
  const startDay = first.getDay();
  const out = [];
  for (let i = startDay - 1; i >= 0; i--) {
    const d = new Date(y, m, -i);
    out.push({ day: d.getDate(), month: d.getMonth(), date: dateISO(d) });
  }
  const last = new Date(y, m + 1, 0).getDate();
  for (let d = 1; d <= last; d++) {
    const dt = new Date(y, m, d);
    out.push({ day: d, month: m, date: dateISO(dt) });
  }
  while (out.length % 7 !== 0) {
    const dt = new Date(out[out.length - 1].date + 'T00:00:00');
    dt.setDate(dt.getDate() + 1);
    out.push({ day: dt.getDate(), month: dt.getMonth(), date: dateISO(dt) });
  }
  return out;
}
function dateISO(d) {
  const y = d.getFullYear(), m = String(d.getMonth() + 1).padStart(2, '0'), day = String(d.getDate()).padStart(2, '0');
  return `${y}-${m}-${day}`;
}
function monthOffset({ y, m }, n) {
  const d = new Date(y, m + n, 1);
  return { y: d.getFullYear(), m: d.getMonth() };
}

// ─── Modal primitives ─────────────────────────────────────────────────────

function ModalShell({ children, width = 420, onClose }) {
  return (
    <div onClick={onClose}
      style={{
        position: 'fixed', inset: 0, background: 'rgba(20, 22, 33, 0.4)',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        zIndex: 200, padding: 20, backdropFilter: 'blur(4px)',
      }}>
      <div onClick={(e) => e.stopPropagation()}
        style={{
          width, maxWidth: '95vw', maxHeight: '90vh',
          background: 'var(--page)', borderRadius: 'var(--r-lg)',
          border: '1px solid var(--hairline-bold)',
          boxShadow: '0 20px 60px -10px rgba(0, 0, 0, 0.25), 0 8px 16px rgba(0, 0, 0, 0.08)',
          padding: 22, display: 'flex', flexDirection: 'column', gap: 14, overflow: 'hidden',
        }}>
        {children}
      </div>
    </div>
  );
}

function ModalHead({ title, sub, onClose }) {
  return (
    <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
      <div>
        <h2 style={{ margin: 0, fontFamily: 'var(--serif)', fontSize: 20, fontWeight: 500, color: 'var(--ink-strong)', letterSpacing: '-0.01em' }}>{title}</h2>
        {sub && <div style={{ fontSize: 12.5, color: 'var(--ink-mute)', marginTop: 4 }}>{sub}</div>}
      </div>
      <button type="button" onClick={onClose}
        style={{ border: 'none', background: 'transparent', fontSize: 20, cursor: 'pointer', color: 'var(--ink-mute)', padding: 0, lineHeight: 1 }}>×</button>
    </div>
  );
}

// ─── Reusable confirm dialog ──────────────────────────────────────────────

function ConfirmDialog({ title, message, confirmLabel = 'Confirm', cancelLabel = 'Cancel', tone = 'default', onConfirm, onCancel }) {
  const [busy, setBusy] = React.useState(false);
  const submit = async () => {
    setBusy(true);
    try { await onConfirm(); }
    finally { setBusy(false); }
  };
  return (
    <ModalShell width={380} onClose={onCancel}>
      <ModalHead title={title} onClose={onCancel} />
      {message && (
        <div style={{ fontSize: 13.5, color: 'var(--ink-soft)', lineHeight: 1.55 }}>{message}</div>
      )}
      <div style={{ display: 'flex', gap: 8, marginTop: 4 }}>
        <button type="button" className="btn ghost sm" onClick={onCancel} disabled={busy}>{cancelLabel}</button>
        <div style={{ flex: 1 }} />
        <button type="button" className={"btn sm " + (tone === 'danger' ? 'danger' : 'accent')}
          onClick={submit} disabled={busy} autoFocus>
          {busy ? '…' : confirmLabel}
        </button>
      </div>
    </ModalShell>
  );
}

// ─── Exports ───────────────────────────────────────────────────────────────

Object.assign(window, {
  parseHash, navigate, useRoute, match,
  useSkeletonGate,
  relativeTime, formatChars, formatBytes,
  AiGlyph,
  NAV_ICONS,
  Topbar, Page,
  monthCells, dateISO, monthOffset,
  ModalShell, ModalHead, ConfirmDialog,
});

})();
