// core.jsx — shell + layout engine + widget registry
//
// Responsibilities:
//  - useFetch / useLayout hooks
//  - widget registry (registerWidget called by widgets/*.jsx files)
//  - shell components (Sidebar, TopBar, PageHeader, StatStrip)
//  - DashboardGrid: maps layout.yml `grid` entries → registered widget components
//  - App composition
//
// Does NOT call ReactDOM.render — boot.jsx does that, after all
// widgets/*.jsx scripts have run and registered themselves.

const { useState, useEffect } = React;

// ── useFetch — TTL cache via sessionStorage, fallback on failure ──────
function useFetch(url, opts = {}) {
  const { ttl = 300_000, fallback = null, parser = (r) => r.json() } = opts;
  const [state, setState] = useState({ loading: !!url, data: fallback, error: null });

  useEffect(() => {
    if (!url) {
      setState({ loading: false, data: fallback, error: null });
      return;
    }
    const cacheKey = `cache:${url}`;
    try {
      const raw = sessionStorage.getItem(cacheKey);
      if (raw) {
        const cached = JSON.parse(raw);
        if (Date.now() - cached.t < ttl) {
          setState({ loading: false, data: cached.d, error: null });
          return;
        }
      }
    } catch {}

    let cancelled = false;
    setState(s => ({ ...s, loading: true }));
    fetch(url)
      .then(r => r.ok ? parser(r) : Promise.reject(new Error(`HTTP ${r.status}`)))
      .then(d => {
        if (cancelled) return;
        try { sessionStorage.setItem(cacheKey, JSON.stringify({ t: Date.now(), d })); } catch {}
        setState({ loading: false, data: d, error: null });
      })
      .catch(err => {
        if (cancelled) return;
        setState({ loading: false, data: fallback, error: err });
      });
    return () => { cancelled = true; };
  }, [url]);

  return state;
}

// ── useLayout — yaml structure + D1 grid overrides (merged per page) ──
// layout.yml is the template (nav, page list, stats, default grids).
// /api/layout returns { [page_id]: grid_array } of user-saved overrides.
// For each page, if the user has saved a custom grid, it replaces the
// yaml default; otherwise the yaml default is used. /api/layout failure
// is non-fatal — we fall back to the bare yaml.
//
// `reloadOverrides` lets the edit UI re-fetch after a save/reset without
// remounting App. `setOverrideLocal(pageId, grid|null)` lets it patch the
// in-memory override map for instant feedback before the PUT round-trips.
function useLayout() {
  const [state, setState] = useState({ loading: true, yaml: null, overrides: {}, error: null });

  const loadOverrides = React.useCallback(() => {
    return fetch('/api/layout')
      .then(r => r.ok ? r.json() : {})
      .catch(() => ({}));
  }, []);

  useEffect(() => {
    let cancelled = false;
    Promise.all([
      fetch('layout.yml').then(r => r.ok ? r.text() : Promise.reject(new Error(`HTTP ${r.status}`))).then(t => jsyaml.load(t)),
      loadOverrides(),
    ])
      .then(([yaml, overrides]) => {
        if (cancelled) return;
        setState({ loading: false, yaml, overrides: overrides || {}, error: null });
      })
      .catch(err => {
        if (cancelled) return;
        console.error('layout load failed:', err);
        setState({ loading: false, yaml: null, overrides: {}, error: err });
      });
    return () => { cancelled = true; };
  }, [loadOverrides]);

  const reloadOverrides = React.useCallback(() => {
    loadOverrides().then(o => setState(s => ({ ...s, overrides: o || {} })));
  }, [loadOverrides]);

  const setOverrideLocal = React.useCallback((pageId, grid) => {
    setState(s => {
      const next = { ...s.overrides };
      if (grid == null) delete next[pageId]; else next[pageId] = grid;
      return { ...s, overrides: next };
    });
  }, []);

  // Merge: yaml.pages[i].grid is replaced when an override exists for that id.
  const layout = React.useMemo(() => {
    if (!state.yaml) return null;
    const merged = { ...state.yaml };
    if (Array.isArray(state.yaml.pages)) {
      merged.pages = state.yaml.pages.map(p =>
        state.overrides[p.id] ? { ...p, grid: state.overrides[p.id] } : p
      );
    }
    return merged;
  }, [state.yaml, state.overrides]);

  return { loading: state.loading, layout, error: state.error, reloadOverrides, setOverrideLocal };
}

// ── useHashRoute — listen for window.location.hash changes ────────────
// Returns the current hash without the leading #, or '' if none.
function useHashRoute() {
  const [hash, setHash] = useState(() => window.location.hash.slice(1));
  useEffect(() => {
    const onChange = () => setHash(window.location.hash.slice(1));
    window.addEventListener('hashchange', onChange);
    return () => window.removeEventListener('hashchange', onChange);
  }, []);
  return hash;
}

// ── useTasksList — D1-backed, with same-tab refresh on `tasks-updated` ─
// Returns [tasks, reload]. Pass enabled=false to skip the network call
// (Stats only fetches when a stat actually needs the list). Reload is
// invoked automatically when any other component dispatches a
// `tasks-updated` CustomEvent — see widgets/tasks.jsx for write callsites.
function useTasksList(enabled = true) {
  const [tasks, setTasks] = useState(null);
  const reload = React.useCallback(() => {
    if (!enabled) return;
    fetch('/api/tasks')
      .then(r => r.ok ? r.json() : [])
      .then(setTasks)
      .catch(() => setTasks([]));
  }, [enabled]);
  useEffect(() => {
    if (!enabled) { setTasks(null); return; }
    reload();
    window.addEventListener('tasks-updated', reload);
    return () => window.removeEventListener('tasks-updated', reload);
  }, [enabled, reload]);
  return [tasks, reload];
}
window.useTasksList = useTasksList;

// ── Widget registry ────────────────────────────────────────────────────
// Widgets self-register by calling registerWidget('type', Component) in
// their own *.jsx file. Order of registration doesn't matter as long as
// it happens before App's first render — which boot.jsx ensures.
window.WIDGETS = window.WIDGETS || {};
window.registerWidget = (type, component) => { window.WIDGETS[type] = component; };

// WidgetContext carries layout metadata into every widget so Panel can
// auto-id and span grid rows without each widget threading props through.
// Schema: {type, index, size, config} where size ∈ {'compact', 'large'}.
//
// Two fixed sizes, two fixed cell shapes:
//   compact → 1 col × 1 row
//   large   → 1 col × 2 rows
// User picks one of two; layouts come from arrangement, not size variation.
const WidgetContext = React.createContext({});
window.WidgetContext = WidgetContext;

// Normalize a layout.yml grid item. Tolerates legacy fields (h, w, span)
// so saved D1 overrides written before the schema simplification still
// render reasonably.
function normalizeItem(item) {
  let size = item.size;
  if (size !== 'compact' && size !== 'large') {
    // Derive from legacy h/w fields if present; otherwise default to large.
    if (item.h === 1) size = 'compact';
    else size = 'large';
  }
  return { ...item, size };
}
window.normalizeItem = normalizeItem;

// useWidgetSize: returns 'compact' or 'large'. Widgets use it to switch
// between two body renderings (e.g. markets compact = top mover only).
function useWidgetSize() {
  const ctx = React.useContext(WidgetContext);
  return ctx.size === 'compact' ? 'compact' : 'large';
}
window.useWidgetSize = useWidgetSize;

// ── Panel — shared widget shell ───────────────────────────────────────
// Every widget renders <Panel title=… hint=… action=…>body</Panel>.
// `hint` is a small muted note next to the title.
// `action` is a right-aligned button/element in the header row.
// `footer` renders a "panel-foot" row at the bottom.
//
// Children are wrapped in `.panel-body` which is a flex column with
// overflow-y: auto, so any widget content that exceeds the cell's
// fixed height scrolls inside the panel rather than blowing the row up.
function Panel({ title, hint, action, footer, children, className }) {
  const ctx = React.useContext(WidgetContext);
  const anchorId = ctx.type != null ? `widget-${ctx.type}-${ctx.index}` : undefined;
  const size = ctx.size === 'compact' ? 'compact' : 'large';
  // compact = 1 row tall (default), large = 2 rows tall (gridRow span 2).
  const style = size === 'large' ? { gridRow: 'span 2' } : undefined;
  return (
    <section id={anchorId}
             className={`panel ${className ?? ''}`}
             data-size={size}
             style={style}>
      <div className="panel-head">
        <div className="panel-title">
          {title}
          {hint && <span className="panel-hint">{hint}</span>}
        </div>
        {action}
      </div>
      <div className="panel-body">{children}</div>
      {footer && <div className="panel-foot">{footer}</div>}
    </section>
  );
}
window.Panel = Panel;

// Helper: derive a "(mock — reason)" hint string from useFetch state +
// upstream sentinel (the case where Worker returns 200 but every entry
// has an `error` field — see markets/calendar widgets).
function mockHint({ error, allErrored, reason }) {
  if (!error && !allErrored) return null;
  const why = reason || (error && error.message) || (allErrored && 'upstream blocked') || 'unreachable';
  return `(mock — ${why})`;
}
window.mockHint = mockHint;

// ── Shell ─────────────────────────────────────────────────────────────
// nav.id matches a page.id → click changes URL hash → App re-renders that page.
// If a nav.id has no matching page, click falls back to scrolling to a
// widget with matching id on the current page (legacy single-page behaviour).
function Sidebar({ brand, nav, activeId, pageIds }) {
  const onClick = (id) => {
    if (pageIds.has(id)) {
      window.location.hash = id;
    } else {
      const el = document.querySelector(`#widget-${id}-0`);
      if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
    }
  };
  return (
    <aside className="sidebar">
      <div className="sb-brand">
        <div className="sb-brand-mark">{(brand && brand[0]) ? brand[0].toUpperCase() : 'S'}</div>
        <div className="sb-brand-name">{brand}</div>
      </div>
      <nav className="sb-nav">
        {(nav ?? []).map((n) => (
          <button key={n.id}
                  className={`sb-item ${n.id === activeId ? 'is-active' : ''}`}
                  onClick={() => onClick(n.id)}>
            <img className="ico" src={n.icon} alt="" />
            <span>{n.label}</span>
            {n.badge && <span className="badge">{n.badge}</span>}
          </button>
        ))}
      </nav>
      <div className="sb-quick-capture"
           onClick={() => window.dispatchEvent(new CustomEvent('focus-task-input'))}
           style={{cursor: 'pointer'}}
           title="Jump to Add task input">
        <img className="ico" src="icons/add.svg" alt="" style={{width: 14, height: 14, opacity: 0.6, filter: 'invert(1) brightness(0.95)'}} />
        <span>Quick Capture</span>
        <span className="kbd">N</span>
      </div>
    </aside>
  );
}

function TopBar({ mode, onToggleMode, editMode, onToggleEditMode }) {
  const isDark = mode === 'dark';
  const searchRef = React.useRef(null);

  // ⌘K (or Ctrl+K on non-Mac) focuses the search input from anywhere.
  React.useEffect(() => {
    const onKey = (e) => {
      if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')) {
        e.preventDefault();
        searchRef.current?.focus();
        searchRef.current?.select();
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, []);

  return (
    <header className="topbar">
      <div className="search">
        <img className="search-ico" src="icons/search.svg" alt="" />
        <input ref={searchRef} placeholder="Search anything..." />
        <span className="search-kbd">⌘K</span>
      </div>
      <button className="icon-btn" aria-label="Toggle theme" onClick={onToggleMode} title={isDark ? 'Switch to light' : 'Switch to dark'}>
        {isDark ? (
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{opacity: 0.7}}>
            <path d="M21 12.79A9 9 0 1 1 11.21 3a7 7 0 0 0 9.79 9.79z"/>
          </svg>
        ) : (
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{opacity: 0.55}}>
            <circle cx="12" cy="12" r="4"/>
            <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
          </svg>
        )}
      </button>
      <button
        className={`icon-btn ${editMode ? 'is-active' : ''}`}
        aria-label="Toggle edit mode"
        onClick={onToggleEditMode}
        title={editMode ? 'Exit edit mode' : 'Edit layout (drag, resize, add, remove)'}
      >
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
             strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{opacity: 0.7}}>
          <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
          <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
        </svg>
      </button>
      <button className="icon-btn" aria-label="Notifications">
        <img src="icons/bell.svg" alt="" />
        <span className="dot"></span>
      </button>
      <button className="avatar-btn" aria-label="Account">S</button>
    </header>
  );
}

function PageHeader({ name, dateStr, weekday }) {
  const hour = new Date().getHours();
  const greet = hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening';
  return (
    <div className="page-head">
      <div>
        <h1>{name}'s Dash</h1>
        <div className="greet">{greet}, {name}. Have a focused and productive day.</div>
      </div>
      <div className="date">
        <div className="date-d">{dateStr}</div>
        <div className="date-w">{weekday}</div>
      </div>
    </div>
  );
}

// Stat — derives its value from one of several sources based on `type`.
// All hooks are called unconditionally (Rules of Hooks); only the matching
// derivation actually uses its data.
function Stat({ config: s }) {
  // Hooks: always run, gated by enabled=false / null url so they no-op
  // when not relevant to this stat.
  const [tasks] = useTasksList(s.type === 'tasksRemaining');

  const feedUrl = s.type === 'feedCount' ? (s.endpoint || '/api/feed') : null;
  const { data: feedData } = useFetch(feedUrl, { ttl: 10 * 60_000, fallback: [] });

  let value = s.value ?? '—';
  let sub = s.sub ?? '';
  let bar = s.bar;

  if (s.type === 'tasksRemaining' && tasks) {
    const done = tasks.filter(t => t.done).length;
    const remaining = tasks.length - done;
    value = String(remaining);
    sub = `${done} done`;
    bar = tasks.length ? done / tasks.length : 0;
  } else if (s.type === 'feedCount' && Array.isArray(feedData)) {
    value = String(feedData.length);
  } else if (s.type === 'count' && Array.isArray(s.items)) {
    value = String(s.items.length);
  }

  return (
    <div className="stat">
      <div className="stat-head">
        <div className={`stat-icon ${s.accent ? 'accent' : ''}`}>
          <img src={s.icon} alt="" />
        </div>
        <span>{s.label}</span>
      </div>
      <div className="stat-value">{value}</div>
      {bar != null ? (
        <div>
          <div className="stat-sub" style={{marginBottom: 6}}>{sub}</div>
          <div className="stat-bar">
            <div className={`stat-bar-fill ${s.accent ? 'accent' : ''}`} style={{ width: `${Math.min(1, bar) * 100}%` }} />
          </div>
        </div>
      ) : (
        <div className="stat-sub">{sub}</div>
      )}
    </div>
  );
}

function StatStrip({ stats }) {
  if (!stats || stats.length === 0) return null;
  return (
    <div className="stats">
      {stats.map((s) => <Stat key={s.id} config={s} />)}
    </div>
  );
}

function DashboardGrid({ items }) {
  return (
    <div className="grid">
      {(items ?? []).map((item, i) => {
        const Component = window.WIDGETS[item.type];
        const norm = normalizeItem(item);
        if (!Component) {
          return (
            <section key={i} id={`widget-${item.type}-${i}`} className="panel"
                     style={norm.size === 'large' ? { gridRow: 'span 2' } : undefined}>
              <div className="panel-head">
                <div className="panel-title">unknown widget: <code>{item.type}</code></div>
              </div>
              <div className="muted" style={{padding: 12}}>
                Did you forget to load <code>widgets/{item.type}.jsx</code> in index.html?
              </div>
            </section>
          );
        }
        return (
          <WidgetContext.Provider key={i} value={{ type: item.type, index: i, ...norm }}>
            <Component config={item.config} />
          </WidgetContext.Provider>
        );
      })}
    </div>
  );
}

// ── EditableGrid — drag-reorder + size/span/add/remove ────────────────
// Local draft state. Save → PUT /api/layout (parent does the actual call);
// Cancel discards draft; ResetPage sends DELETE so the page falls back to
// layout.yml. Re-initialises whenever the active page changes.
function EditableGrid({ page, onSave, onCancel, onResetPage }) {
  const [draft, setDraft] = useState(page.grid || []);
  const [dragIdx, setDragIdx] = useState(null);
  const [dropIdx, setDropIdx] = useState(null);
  const [showAdd, setShowAdd] = useState(false);

  React.useEffect(() => { setDraft(page.grid || []); }, [page.id, page.grid]);

  const update = (i, patch) => setDraft(d => d.map((it, j) => j === i ? { ...it, ...patch } : it));
  const remove = (i) => setDraft(d => d.filter((_, j) => j !== i));
  const move = (from, to) => setDraft(d => {
    if (from === to || to == null) return d;
    const next = [...d];
    const [item] = next.splice(from, 1);
    next.splice(to > from ? to - 1 : to, 0, item);
    return next;
  });
  const add = (type) => {
    setDraft(d => [...d, { type, size: 'large' }]);
    setShowAdd(false);
  };

  const dirty = JSON.stringify(draft) !== JSON.stringify(page.grid || []);

  return (
    <>
      <div className="edit-toolbar">
        <span className="muted">Editing layout for <strong>{page.id}</strong></span>
        <span style={{flex: 1}} />
        {dirty && <span className="edit-dirty">● unsaved</span>}
        <button className="panel-action" onClick={onCancel}>Cancel</button>
        <button className="panel-action" onClick={onResetPage}
                title="Drop your saved layout for this page; falls back to layout.yml">
          Reset to default
        </button>
        <button className="panel-action edit-save" disabled={!dirty}
                onClick={() => onSave(draft)}>Save</button>
      </div>
      <div className="grid grid-editing">
        {draft.map((item, i) => (
          <EditPanel
            key={i}
            item={item}
            isDragging={dragIdx === i}
            isDropTarget={dropIdx === i && dragIdx !== i}
            onDragStart={() => setDragIdx(i)}
            onDragEnter={() => dragIdx != null && setDropIdx(i)}
            onDragOver={(e) => { e.preventDefault(); }}
            onDragEnd={() => { setDragIdx(null); setDropIdx(null); }}
            onDrop={(e) => {
              e.preventDefault();
              if (dragIdx != null) move(dragIdx, i);
              setDragIdx(null); setDropIdx(null);
            }}
            onUpdate={(patch) => update(i, patch)}
            onRemove={() => remove(i)}
          />
        ))}
        <button className="add-tile" onClick={() => setShowAdd(true)}
                title="Add a widget to this page">
          <span className="add-tile-plus">+</span>
          <span>Add widget</span>
        </button>
      </div>
      {showAdd && (
        <AddWidgetPicker
          types={Object.keys(window.WIDGETS).sort()}
          onPick={add}
          onClose={() => setShowAdd(false)}
        />
      )}
    </>
  );
}

function EditPanel({ item, isDragging, isDropTarget,
                     onDragStart, onDragEnter, onDragOver, onDragEnd, onDrop,
                     onUpdate, onRemove }) {
  const Component = window.WIDGETS[item.type];
  const norm = normalizeItem(item);
  const style = norm.size === 'large' ? { gridRow: 'span 2' } : undefined;
  const cls = `edit-panel-wrap ${isDragging ? 'is-dragging' : ''} ${isDropTarget ? 'is-drop' : ''}`;
  return (
    <div className={cls} style={style}
         draggable
         onDragStart={onDragStart}
         onDragEnter={onDragEnter}
         onDragOver={onDragOver}
         onDragEnd={onDragEnd}
         onDrop={onDrop}>
      <div className="edit-panel-toolbar">
        <span className="edit-grip" title="Drag to reorder">⠿</span>
        <span className="edit-type">{item.type}</span>
        <span style={{flex: 1}} />
        <button className="panel-action edit-size-btn"
                onClick={() => onUpdate({ size: norm.size === 'compact' ? 'large' : 'compact' })}
                title={`Switch to ${norm.size === 'compact' ? 'large' : 'compact'}`}>
          {norm.size}
        </button>
        <button className="edit-x" onClick={onRemove} title="Remove from page">×</button>
      </div>
      <div className="edit-panel-preview">
        {Component ? (
          <WidgetContext.Provider value={{ type: item.type, index: 0, ...norm }}>
            <Component config={item.config} />
          </WidgetContext.Provider>
        ) : (
          <div className="muted" style={{padding: 12}}>unknown widget: {item.type}</div>
        )}
      </div>
    </div>
  );
}


function AddWidgetPicker({ types, onPick, onClose }) {
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onClose]);
  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()}>
        <div className="modal-title">Add widget to this page</div>
        <div className="modal-grid">
          {types.map(t => (
            <button key={t} className="modal-tile" onClick={() => onPick(t)}>{t}</button>
          ))}
        </div>
        <div className="modal-foot">
          <span className="muted" style={{fontSize: 11}}>
            New widgets get default size + span; tweak then Save.
          </span>
          <button className="panel-action" onClick={onClose}>Cancel</button>
        </div>
      </div>
    </div>
  );
}

// ── App ───────────────────────────────────────────────────────────────
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "tone": "warm",
  "accent": "#d97757",
  "radius": "round",
  "density": "regular",
  "sidebar": "dark",
  "mode": "light",
  "userName": "Setsushin"
}/*EDITMODE-END*/;

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const { loading: layoutLoading, layout, error: layoutError, reloadOverrides, setOverrideLocal } = useLayout();
  const [editMode, setEditMode] = useState(false);

  React.useEffect(() => {
    document.body.dataset.tone = t.tone;
    document.body.dataset.density = t.density;
    document.body.dataset.sidebar = t.sidebar;
    document.body.dataset.radius = t.radius;
    document.body.dataset.mode = t.mode;
    document.documentElement.style.setProperty('--accent', t.accent);
    document.documentElement.style.setProperty('--accent-soft', hexToSoft(t.accent));
  }, [t]);

  React.useEffect(() => {
    if (layout?.brand) document.title = `${t.userName || layout.brand} · Dashboard`;
  }, [layout, t.userName]);

  const today = new Date();
  const dateStr = today.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
  const weekday = today.toLocaleDateString('en-US', { weekday: 'long' });

  const hash = useHashRoute();

  // Switching pages while editing exits edit mode (drops the draft) so we
  // don't accidentally PUT the wrong page's draft. If you want to keep
  // edits across pages later, hoist the draft into App and key it by page.id.
  React.useEffect(() => { setEditMode(false); }, [hash]);

  if (layoutLoading) return <div className="boot">Loading layout…</div>;
  if (layoutError) return <div className="boot boot-err">Failed to load layout.yml: {String(layoutError.message)}</div>;

  const pages = layout.pages || [];
  const pageIds = new Set(pages.map(p => p.id));
  const page = pages.find(p => p.id === hash) || pages[0];

  const onSaveLayout = async (grid) => {
    if (!page) return;
    setOverrideLocal(page.id, grid);   // optimistic; UI updates instantly
    try {
      const r = await fetch('/api/layout', {
        method: 'PUT',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ page_id: page.id, grid }),
      });
      if (!r.ok) throw new Error(`PUT failed: HTTP ${r.status}`);
      setEditMode(false);
    } catch (err) {
      console.error('layout save failed:', err);
      alert(`Save failed: ${err.message}\nYour changes are still in the editor.`);
      reloadOverrides();   // resync in case server state diverged
    }
  };

  const onResetPage = async () => {
    if (!page) return;
    if (!window.confirm(`Reset "${page.id}" to layout.yml default?`)) return;
    try {
      const r = await fetch(`/api/layout?page_id=${encodeURIComponent(page.id)}`, { method: 'DELETE' });
      if (!r.ok) throw new Error(`DELETE failed: HTTP ${r.status}`);
      setOverrideLocal(page.id, null);
      setEditMode(false);
    } catch (err) {
      console.error('layout reset failed:', err);
      alert(`Reset failed: ${err.message}`);
    }
  };

  return (
    <div className="app">
      <Sidebar brand={t.userName} nav={layout.nav} activeId={page?.id} pageIds={pageIds} />
      <main className="main">
        <TopBar
          mode={t.mode}
          onToggleMode={() => setTweak('mode', t.mode === 'dark' ? 'light' : 'dark')}
          editMode={editMode}
          onToggleEditMode={() => setEditMode(v => !v)}
        />
        <div className="content" data-screen-label={page?.id} data-edit-mode={editMode || undefined}>
          <PageHeader name={t.userName} dateStr={dateStr} weekday={weekday} />
          <StatStrip stats={page?.stats} />
          {editMode && page
            ? <EditableGrid page={page} onSave={onSaveLayout}
                            onCancel={() => setEditMode(false)} onResetPage={onResetPage} />
            : <DashboardGrid items={page?.grid} />}
        </div>
        <div className="footer">
          <span>Simplicity is the ultimate sophistication.</span>
          <span className="att">— Leonardo da Vinci</span>
        </div>
      </main>

      <TweaksPanel title="Tweaks">
        <TweakSection label="Theme" />
        <TweakRadio label="Mode" value={t.mode}
          options={[{ value: 'light', label: 'Light' }, { value: 'dark', label: 'Dark' }]}
          onChange={(v) => setTweak('mode', v)} />
        <TweakRadio label="Tone" value={t.tone}
          options={[
            { value: 'warm', label: 'Warm' },
            { value: 'sage', label: 'Sage' },
            { value: 'cool', label: 'Cool' },
            { value: 'lavender', label: 'Lilac' },
          ]}
          onChange={(v) => {
            const accentByTone = { warm: '#d97757', sage: '#6f8e5a', cool: '#5b6cff', lavender: '#9a72c4' };
            setTweak({ tone: v, accent: accentByTone[v] });
          }} />
        <TweakColor label="Accent color" value={t.accent}
          onChange={(v) => setTweak('accent', v)} />
        <TweakRadio label="Sidebar" value={t.sidebar}
          options={[{ value: 'dark', label: 'Dark' }, { value: 'light', label: 'Light' }]}
          onChange={(v) => setTweak('sidebar', v)} />

        <TweakSection label="Layout" />
        <TweakRadio label="Roundness" value={t.radius}
          options={[
            { value: 'square', label: 'Subtle' },
            { value: 'round', label: 'Soft' },
            { value: 'extra', label: 'Pillowy' },
          ]}
          onChange={(v) => setTweak('radius', v)} />
        <TweakRadio label="Density" value={t.density}
          options={[
            { value: 'compact', label: 'Compact' },
            { value: 'regular', label: 'Regular' },
            { value: 'comfy', label: 'Comfy' },
          ]}
          onChange={(v) => setTweak('density', v)} />

        <TweakSection label="Identity" />
        <TweakText label="Name" value={t.userName}
          onChange={(v) => setTweak('userName', v)} />
      </TweaksPanel>
    </div>
  );
}

function hexToSoft(hex) {
  const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
  if (!m) return 'rgba(217, 119, 87, 0.22)';
  const n = parseInt(m[1], 16);
  const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255;
  const bg = [251, 247, 240];
  const mix = (a, b, t) => Math.round(a * t + b * (1 - t));
  return `rgb(${mix(r, bg[0], 0.28)}, ${mix(g, bg[1], 0.28)}, ${mix(b, bg[2], 0.28)})`;
}

window.App = App;
