// App shell — orchestrates desktop + mobile, manages state, wires Tweaks.

const { useState: aUseState, useEffect: aUseEffect, useMemo: aUseMemo, useCallback: aUseCallback } = React;
const U = window.CAL_UTILS;
const { MiniMonth, MonthBlock, DayDrawer, QuickCapture } = window.DesktopBits;
const { MobileMonth, MobileWeek, MobileDaySheet } = window.MobileBits;
const EventEditor = window.EventEditor;

// Global editor controller — any component can call window.openEventEditor({...})
// to open the modal. The shell owns the state.
window.__editorListeners = new Set();
window.openEventEditor = function (payload) {
  window.__editorListeners.forEach(fn => fn(payload || {}));
};

// Global "focus search" trigger — keyboard shortcut and any UI can call this.
window.__searchFocusListeners = new Set();
window.focusSearch = function () {
  window.__searchFocusListeners.forEach(fn => fn());
};

// Global toast bus — any component dispatches; the App shell renders.
window.__toastListeners = new Set();
window.showToast = function (msg, opts) {
  window.__toastListeners.forEach(fn => fn(msg, opts || {}));
};

// Triggers a hidden file picker on the active import input. Both desktop and
// mobile mount one and register here.
window.__importTriggers = new Set();
window.triggerScreenshotImport = function () {
  // Trigger the first registered input that is connected to the DOM.
  for (const fn of window.__importTriggers) { try { fn(); break; } catch {} }
};

async function handleImportFile(file) {
  if (!file) return;
  window.showToast("Importing screenshot…", { spinner: true, sticky: true });
  try {
    const stamp = await window.GeminiImport.runImport(file);
    const wk = stamp.weekStart;
    const wkLabel = wk ? new Date(wk + "T00:00:00").toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" }) : "this week";
    window.showToast(
      `Imported ${stamp.count} event${stamp.count === 1 ? "" : "s"} for week of ${wkLabel}${stamp.weekInferred ? "" : " (week not detected — defaulted)"}`,
      { action: { label: "Undo", run: () => { if (window.GeminiImport.undoLast()) window.showToast("Import undone."); } } }
    );
  } catch (err) {
    window.showToast("Import failed: " + (err.message || String(err)), { error: true });
  }
}

// ─── ImportButton — toolbar entry point for screenshot import ───────────
function ImportButton() {
  const inputRef = React.useRef(null);
  aUseEffect(() => {
    const trigger = () => { inputRef.current && inputRef.current.click(); };
    window.__importTriggers.add(trigger);
    return () => window.__importTriggers.delete(trigger);
  }, []);
  return (
    <>
      <button
        type="button"
        className="btn icon import-btn"
        title="Import work screenshot"
        aria-label="Import work screenshot"
        onClick={() => inputRef.current && inputRef.current.click()}
      >⇪</button>
      <input
        ref={inputRef}
        type="file"
        accept="image/*"
        style={{ display: "none" }}
        onChange={(e) => {
          const f = e.target.files && e.target.files[0];
          e.target.value = ""; // allow re-importing the same file
          handleImportFile(f);
        }}
      />
    </>
  );
}

// ─── Toast — bottom-of-screen status with optional action ───────────────
function Toast() {
  const [t, setT] = aUseState(null);
  const timerRef = React.useRef(null);
  aUseEffect(() => {
    const fn = (msg, opts) => {
      if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; }
      setT({ msg, ...opts });
      if (!opts.sticky) {
        timerRef.current = setTimeout(() => setT(null), opts.action ? 8000 : 4500);
      }
    };
    window.__toastListeners.add(fn);
    return () => window.__toastListeners.delete(fn);
  }, []);
  if (!t) return null;
  return (
    <div className={`toast ${t.error ? "error" : ""}`} role="status">
      {t.spinner && <span className="toast-spin" aria-hidden="true" />}
      <span className="toast-msg">{t.msg}</span>
      {t.action && (
        <button type="button" className="toast-action" onClick={() => {
          try { t.action.run(); } catch (e) { window.showToast("Failed: " + e.message, { error: true }); }
          setT(null);
        }}>{t.action.label}</button>
      )}
      <button type="button" className="toast-x" aria-label="Dismiss"
              onClick={() => setT(null)}>×</button>
    </div>
  );
}

// ─── Search bar — title/notes/location filter with keyboard nav ──────────
function SearchBar({ onPick, mobile = false }) {
  const [q, setQ] = aUseState("");
  const [open, setOpen] = aUseState(false);
  const [active, setActive] = aUseState(0);
  const inputRef = React.useRef(null);

  aUseEffect(() => {
    const fn = () => { inputRef.current && inputRef.current.focus(); setOpen(true); };
    window.__searchFocusListeners.add(fn);
    return () => window.__searchFocusListeners.delete(fn);
  }, []);

  const results = aUseMemo(() => {
    const term = q.trim().toLowerCase();
    if (!term) return [];
    const events = window.CALENDAR_DATA.EVENTS;
    const seen = new Set();
    const out = [];
    for (const e of events) {
      const hay = `${e.title || ""} ${e.notes || ""} ${e.location || ""}`.toLowerCase();
      if (!hay.includes(term)) continue;
      // Collapse recurrence instances back to the parent so we don't get 50 hits
      const key = (e._recurParent || e.id) + "@" + e.date;
      if (seen.has(key)) continue;
      seen.add(key);
      out.push(e);
      if (out.length >= 12) break;
    }
    return out.sort((a, b) => (a.date || "").localeCompare(b.date || ""));
  }, [q]);

  aUseEffect(() => { setActive(0); }, [q]);

  function commit(idx) {
    const e = results[idx];
    if (!e) return;
    setOpen(false);
    setQ("");
    onPick(e);
  }

  function onKeyDown(ev) {
    if (ev.key === "ArrowDown") { ev.preventDefault(); setActive(a => Math.min(results.length - 1, a + 1)); }
    else if (ev.key === "ArrowUp") { ev.preventDefault(); setActive(a => Math.max(0, a - 1)); }
    else if (ev.key === "Enter") { ev.preventDefault(); commit(active); }
    else if (ev.key === "Escape") { ev.preventDefault(); setOpen(false); setQ(""); inputRef.current && inputRef.current.blur(); }
  }

  const cats = window.CALENDAR_DATA.CATEGORIES;

  return (
    <div className={`search ${mobile ? "search-mobile" : ""}`}>
      <div className="search-row">
        <span className="search-icon" aria-hidden="true">⌕</span>
        <input
          ref={inputRef}
          type="text"
          className="search-input"
          placeholder="Search events…"
          value={q}
          onChange={(e) => { setQ(e.target.value); setOpen(true); }}
          onFocus={() => setOpen(true)}
          onBlur={() => setTimeout(() => setOpen(false), 120)}
          onKeyDown={onKeyDown}
        />
        {!mobile && <kbd className="search-kbd">/</kbd>}
      </div>
      {open && q.trim() && (
        <ul className="search-results" role="listbox">
          {results.length === 0 && (
            <li className="search-empty">No matches for “{q}”.</li>
          )}
          {results.map((e, i) => {
            const c = cats[e.cat] || cats.personal;
            const d = U.parseKey(e.date);
            const dateLabel = `${U.DOW_SHORT[U.dowMon(d)]} ${d.getDate()} ${U.MONTH_SHORT[d.getMonth()]} ${d.getFullYear()}`;
            return (
              <li
                key={e.id + "@" + e.date}
                className={`search-item ${i === active ? "active" : ""}`}
                role="option"
                aria-selected={i === active}
                onMouseDown={(ev) => { ev.preventDefault(); commit(i); }}
                onMouseEnter={() => setActive(i)}
              >
                <span className="search-dot" style={{ background: c.dot }} />
                <span className="search-title">{e.title}</span>
                <span className="search-meta">
                  {e.allDay ? "All day" : (e.start || "")} · {dateLabel}
                </span>
              </li>
            );
          })}
        </ul>
      )}
    </div>
  );
}

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "aesthetic": "minimal",
  "density": "balanced",
  "showHabitStrip": true,
  "showQuickCapture": true,
  "forcedView": "auto",
  "theme": "auto"
}/*EDITMODE-END*/;

// ─── Habit strip (desktop) — shows last 7 days for each habit; click a dot to toggle
function HabitStrip({ anchorKey }) {
  const habitState = window.useHabits();
  const habits = window.CALENDAR_DATA.HABITS;
  const anchor = U.parseKey(anchorKey);
  const days = Array.from({ length: 7 }, (_, i) => U.addDays(anchor, i - 6));
  return (
    <div className="habit-strip">
      <div className="habit-strip-label">Last 7 days</div>
      {habits.map((h) => (
        <div key={h.id} className="habit-track" style={{ color: h.color }}>
          <div className="habit-track-name">{h.label}</div>
          <div className="habit-week">
            {days.map((d) => {
              const k = U.keyOf(d);
              const done = habitState.done(h.id, k);
              return (
                <button
                  type="button"
                  key={k}
                  className={`habit-dot-week ${done ? "done" : "empty"}`}
                  title={`${h.label} · ${k}`}
                  onClick={() => habitState.toggle(h.id, k)}
                  aria-pressed={done}
                />
              );
            })}
          </div>
        </div>
      ))}
    </div>
  );
}

// ─── Week view (desktop) ────────────────────────────────────────────
function WeekView({ anchorKey, onSelect }) {
  const cats = window.CALENDAR_DATA.CATEGORIES;
  const anchor = U.parseKey(anchorKey);
  const offset = U.dowMon(anchor);
  const start = U.addDays(anchor, -offset);
  const days = Array.from({ length: 7 }, (_, i) => U.addDays(start, i));
  const todayKey = U.keyOf(new Date());

  // Hours 7..22 (16 rows × 50px = 800)
  const HOURS = Array.from({ length: 16 }, (_, i) => i + 7);

  function eventTop(t) {
    const [h, m] = t.split(":").map(Number);
    return ((h - 7) + m / 60) * 50;
  }
  function eventHeight(s, e) {
    const [sh, sm] = s.split(":").map(Number);
    const [eh, em] = e.split(":").map(Number);
    return Math.max(20, ((eh + em / 60) - (sh + sm / 60)) * 50);
  }

  return (
    <div className="week-view">
      <div className="week-head">
        <div className="week-head-cell" />
        {days.map((d) => (
          <div key={U.keyOf(d)} className={`week-head-cell ${U.keyOf(d) === todayKey ? "today" : ""}`}>
            <div className="week-head-dow">{U.DOW_SHORT[U.dowMon(d)]}</div>
            <div className="week-head-num">{d.getDate()}</div>
          </div>
        ))}
      </div>

      <div className="week-allday">
        <div className="week-allday-label">All day</div>
        {days.map((d) => {
          const k = U.keyOf(d);
          const allDay = U.eventsOnDate(window.CALENDAR_DATA.EVENTS, k).filter(e => e.allDay);
          return (
            <div key={k} className="week-allday-col">
              {allDay.map((e) => {
                const c = cats[e.cat];
                return (
                  <div key={e.id} className="week-allday-chip"
                    style={{ background: c.soft, color: c.ink, borderColor: c.dot }}>
                    {U.clipTitle(e.title, 22)}
                  </div>
                );
              })}
            </div>
          );
        })}
      </div>

      <div className="week-grid">
        <div className="week-hours">
          {HOURS.map((h) => (
            <div key={h} className="week-hour">{String(h).padStart(2, "0")}:00</div>
          ))}
        </div>
        {days.map((d) => {
          const k = U.keyOf(d);
          const timed = U.eventsOnDate(window.CALENDAR_DATA.EVENTS, k).filter(e => !e.allDay && e.start);
          return (
            <div key={k} className="week-col" onClick={() => onSelect(k)}>
              {HOURS.map((h) => <div key={h} className="week-col-hour" />)}
              {timed.map((e) => {
                const c = cats[e.cat];
                return (
                  <div key={e.id} className="week-evt"
                    style={{
                      top: eventTop(e.start),
                      height: eventHeight(e.start, e.end),
                      background: c.soft, color: c.ink, borderLeftColor: c.dot
                    }}>
                    <div className="week-evt-time">{e.start}–{e.end}</div>
                    <div className="week-evt-title">{U.clipTitle(e.title, 24)}</div>
                  </div>
                );
              })}
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ─── Desktop App ────────────────────────────────────────────────────
function DesktopApp({ tweaks }) {
  window.useCalendarEvents(); // subscribe to re-render on store changes
  const TODAY = U.keyOf(new Date());
  const [anchor, setAnchor] = aUseState(() => { const t = new Date(); return new Date(t.getFullYear(), t.getMonth(), 1); }); // first month displayed
  const [selectedKey, setSelectedKey] = aUseState(TODAY);
  const [view, setView] = aUseState("two-month"); // two-month | week
  const cats = window.CALENDAR_DATA.CATEGORIES;
  const [activeCats, setActiveCats] = aUseState(Object.keys(cats));

  function toggleCat(id) {
    setActiveCats(prev => prev.includes(id) ? prev.filter(c => c !== id) : [...prev, id]);
  }

  function pickFromSearch(e) {
    setSelectedKey(e.date);
    const target = U.parseKey(e.date);
    setAnchor(new Date(target.getFullYear(), target.getMonth(), 1));
    window.openEventEditor({ initial: e });
  }

  // Keyboard shortcuts. Ignore typing-context events except Esc.
  aUseEffect(() => {
    function isTyping(t) {
      if (!t) return false;
      const tag = (t.tagName || "").toLowerCase();
      return tag === "input" || tag === "textarea" || tag === "select" || t.isContentEditable;
    }
    function onKey(e) {
      if (e.key === "Escape") {
        // let modals handle their own Esc; nothing to do at app level
        return;
      }
      if (isTyping(e.target)) return;
      // Allow shortcuts only with no modifier (allow shift for letters)
      if (e.metaKey || e.ctrlKey || e.altKey) return;
      if (e.key === "ArrowLeft") {
        e.preventDefault();
        if (view === "week") setSelectedKey(U.keyOf(U.addDays(U.parseKey(selectedKey), -7)));
        else setAnchor(U.addMonths(anchor, -1));
      } else if (e.key === "ArrowRight") {
        e.preventDefault();
        if (view === "week") setSelectedKey(U.keyOf(U.addDays(U.parseKey(selectedKey), 7)));
        else setAnchor(U.addMonths(anchor, 1));
      } else if (e.key === "t" || e.key === "T") {
        e.preventDefault();
        const t = new Date();
        setAnchor(new Date(t.getFullYear(), t.getMonth(), 1));
        setSelectedKey(TODAY);
      } else if (e.key === "n" || e.key === "N") {
        e.preventDefault();
        window.openEventEditor({ defaultDate: selectedKey });
      } else if (e.key === "/") {
        e.preventDefault();
        window.focusSearch();
      }
    }
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [anchor, selectedKey, view]);

  const m1 = anchor;
  const m2 = U.addMonths(anchor, 1);

  return (
    <div className="app-desktop" data-force={tweaks.forcedView}>
      {/* Left rail */}
      <aside className="rail">
        <div className="rail-brand">
          <div className="rail-brand-mark">
            <window.BrandMarks.Monogram size={42} />
          </div>
          <button
            type="button"
            className="rail-tweaks-btn"
            onClick={() => window.toggleTweaks && window.toggleTweaks()}
            aria-label="Open tweaks"
            title="Tweaks (look & feel)"
          >⚙</button>
        </div>

        <div className="view-tabs">
          <button className={view === "two-month" ? "active" : ""} onClick={() => setView("two-month")}>Months</button>
          <button className={view === "week" ? "active" : ""} onClick={() => setView("week")}>Week</button>
        </div>

        <div>
          <div className="rail-section-label">Jump to</div>
          <div className="year-jump">
            <button onClick={() => setAnchor(U.addMonths(anchor, -12))}>‹‹</button>
            <span className="year-jump-num">{anchor.getFullYear()}</span>
            <button onClick={() => setAnchor(U.addMonths(anchor, 12))}>››</button>
          </div>
        </div>

        <MiniMonth
          year={U.addMonths(anchor, -1).getFullYear()}
          month0={U.addMonths(anchor, -1).getMonth()}
          selectedKey={selectedKey}
          onPick={(k) => { setSelectedKey(k); setAnchor(U.parseKey(k)); }}
        />

        <div>
          <div className="rail-section-label">Categories</div>
          <div className="cat-list">
            {Object.entries(cats).map(([id, c]) => (
              <button key={id}
                className={`cat-item ${activeCats.includes(id) ? "" : "off"}`}
                onClick={() => toggleCat(id)}>
                <span className="cat-dot" style={{ background: c.dot }} />
                <span>{c.label}</span>
              </button>
            ))}
          </div>
        </div>
      </aside>

      {/* Main */}
      <main className="main">
        <div className="main-titlebar">
          <window.BrandMarks.Wordmark text="GEORGE NETTLETON CALENDAR" size={18} className="main-wordmark" />
          <div className="main-titlebar-meta">
            <span className="main-titlebar-divider" />
            <span className="main-titlebar-tag">v1 · {new Date().toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short", year: "numeric" })}</span>
          </div>
        </div>
        <div className="main-head">
          <div>
            <h1 className="main-title">
              {view === "week"
                ? `Week of ${U.parseKey(selectedKey).getDate()} ${U.MONTH_SHORT[U.parseKey(selectedKey).getMonth()]}`
                : `${U.MONTH_NAMES[m1.getMonth()]} – ${U.MONTH_NAMES[m2.getMonth()]} ${m2.getFullYear()}`}
            </h1>
            <div className="main-sub">
              {view === "two-month" ? "Two months at a glance · click a day for details" : "Hour-by-hour schedule"}
            </div>
          </div>
          <div className="main-nav">
            <button className="btn icon" onClick={() => {
              if (view === "week") setSelectedKey(U.keyOf(U.addDays(U.parseKey(selectedKey), -7)));
              else setAnchor(U.addMonths(anchor, -1));
            }} aria-label="Previous">‹</button>
            <button className="btn" onClick={() => { const t = new Date(); setAnchor(new Date(t.getFullYear(), t.getMonth(), 1)); setSelectedKey(TODAY); }}>Today</button>
            <button className="btn icon" onClick={() => {
              if (view === "week") setSelectedKey(U.keyOf(U.addDays(U.parseKey(selectedKey), 7)));
              else setAnchor(U.addMonths(anchor, 1));
            }} aria-label="Next">›</button>
            <ImportButton />
            <button className="btn primary" onClick={() => window.openEventEditor({ defaultDate: selectedKey })}>+ New event</button>
          </div>
        </div>

        <SearchBar onPick={pickFromSearch} />

        {tweaks.showQuickCapture && (
          <QuickCapture onAdd={(p) => {
            const evt = { title: p.title, date: p.dateKey, cat: p.cat };
            if (p.start) {
              evt.start = p.start;
              // Default 1h duration
              const [h, m] = p.start.split(":").map(Number);
              const eh = (h + 1) % 24;
              evt.end = `${String(eh).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
            } else {
              evt.allDay = true;
            }
            window.eventStore.addEvent(evt);
            setSelectedKey(p.dateKey);
          }} />
        )}

        {tweaks.showHabitStrip && <HabitStrip anchorKey={selectedKey} />}

        {view === "two-month" ? (
          <div className="two-month">
            <MonthBlock year={m1.getFullYear()} month0={m1.getMonth()} selectedKey={selectedKey} onSelect={setSelectedKey} density={tweaks.density} />
            <MonthBlock year={m2.getFullYear()} month0={m2.getMonth()} selectedKey={selectedKey} onSelect={setSelectedKey} density={tweaks.density} />
          </div>
        ) : (
          <WeekView anchorKey={selectedKey} onSelect={setSelectedKey} />
        )}
      </main>

      {/* Drawer */}
      <DayDrawer dateKey={selectedKey} onClose={() => {}} onJump={setSelectedKey} />
    </div>
  );
}

// ─── Mobile App ─────────────────────────────────────────────────────
function MobileApp({ tweaks }) {
  window.useCalendarEvents();
  const TODAY = U.keyOf(new Date());
  const [anchor, setAnchor] = aUseState(() => { const t = new Date(); return new Date(t.getFullYear(), t.getMonth(), 1); });
  const [weekAnchor, setWeekAnchor] = aUseState(TODAY);
  const [selectedKey, setSelectedKey] = aUseState(null);
  const [view, setView] = aUseState("month"); // month | week

  function goPrev() {
    if (view === "week") setWeekAnchor(U.keyOf(U.addDays(U.parseKey(weekAnchor), -7)));
    else setAnchor(U.addMonths(anchor, -1));
  }
  function goNext() {
    if (view === "week") setWeekAnchor(U.keyOf(U.addDays(U.parseKey(weekAnchor), 7)));
    else setAnchor(U.addMonths(anchor, 1));
  }
  function goToday() {
    const t = new Date();
    setAnchor(new Date(t.getFullYear(), t.getMonth(), 1));
    setWeekAnchor(TODAY);
  }

  return (
    <div className="app-mobile" data-force={tweaks.forcedView}>
      <div className="m-top">
        <div className="m-top-row">
          <div className="m-brand">
            <window.BrandMarks.Monogram size={28} />
            <div className="m-brand-text">
              <window.BrandMarks.Wordmark text="GEORGE NETTLETON" size={6} />
              <window.BrandMarks.Wordmark text="CALENDAR" size={8} />
            </div>
          </div>
          <div className="m-nav-btns">
            <button onClick={goPrev} aria-label="Previous">‹</button>
            <button onClick={goToday} aria-label="Today">·</button>
            <button onClick={goNext} aria-label="Next">›</button>
            <button onClick={() => window.triggerScreenshotImport()} aria-label="Import screenshot" title="Import work screenshot">⇪</button>
            <button onClick={() => window.toggleTweaks && window.toggleTweaks()} aria-label="Tweaks" title="Tweaks">⚙</button>
          </div>
        </div>
        <div className="m-top-row m-top-month">
          <div>
            <span className="m-month-title">{U.MONTH_NAMES[anchor.getMonth()]}</span>
            <span className="m-month-year">{anchor.getFullYear()}</span>
          </div>
        </div>
        <div className="m-view-tabs">
          <button className={view === "month" ? "active" : ""} onClick={() => setView("month")}>Month</button>
          <button className={view === "week" ? "active" : ""} onClick={() => setView("week")}>Week</button>
        </div>
      </div>

      <div className="m-body">
        <SearchBar
          mobile
          onPick={(e) => {
            setSelectedKey(e.date);
            const target = U.parseKey(e.date);
            setAnchor(new Date(target.getFullYear(), target.getMonth(), 1));
            window.openEventEditor({ initial: e });
          }}
        />
        {view === "month"
          ? <MobileMonth year={anchor.getFullYear()} month0={anchor.getMonth()} selectedKey={selectedKey} onSelect={setSelectedKey} />
          : <MobileWeek anchorKey={weekAnchor} onPickDay={setSelectedKey} />}
      </div>

      <button className="fab" onClick={() => window.openEventEditor({ defaultDate: selectedKey || TODAY })} aria-label="Quick add">＋</button>

      <MobileDaySheet dateKey={selectedKey} onClose={() => setSelectedKey(null)} />
    </div>
  );
}

// ─── Tweaks panel ───────────────────────────────────────────────────
function App() {
  const [tweaks, setTweak] = window.useTweaks(TWEAK_DEFAULTS);
  const { add, update, remove } = window.useCalendarEvents();
  const [editor, setEditor] = aUseState({ open: false, initial: null, defaultDate: null });
  const [apiKey, setApiKeyState] = aUseState(() => window.GeminiImport.getKey());
  const [lastImport, setLastImport] = aUseState(() => window.GeminiImport.getLast());
  const [gcal, setGcal] = aUseState({ loading: true, connected: false, connectedAt: null });

  // Re-read last-import stamp whenever events change (covers Undo too).
  aUseEffect(() => window.eventStore.subscribe(() => setLastImport(window.GeminiImport.getLast())), []);

  // Probe Google Calendar connection status (cloud only). On file:// the API
  // doesn't exist and the fetch will fail — that's fine, we just hide the row.
  aUseEffect(() => {
    if (typeof location === "undefined" || location.protocol !== "https:") {
      setGcal({ loading: false, connected: false, connectedAt: null, unavailable: true });
      return;
    }
    fetch("/api/google/status", { credentials: "include" })
      .then(r => r.ok ? r.json() : { connected: false })
      .then(j => setGcal({ loading: false, connected: !!j.connected, connectedAt: j.connectedAt || null }))
      .catch(() => setGcal({ loading: false, connected: false, connectedAt: null, unavailable: true }));

    // Catch the post-OAuth bounce-back and show a confirmation toast.
    if (location.search.includes("gcal=connected")) {
      if (window.showToast) window.showToast("Google Calendar connected.");
      const url = new URL(location.href);
      url.searchParams.delete("gcal");
      history.replaceState({}, "", url.toString());
    }
  }, []);

  function disconnectGcal() {
    fetch("/api/google/disconnect", { method: "POST", credentials: "include" })
      .then(() => setGcal({ loading: false, connected: false, connectedAt: null }))
      .then(() => window.showToast && window.showToast("Disconnected."))
      .catch(() => window.showToast && window.showToast({ text: "Disconnect failed.", variant: "error" }));
  }

  function setApiKey(v) {
    window.GeminiImport.setKey(v);
    setApiKeyState(v);
  }

  aUseEffect(() => {
    const fn = (payload) => {
      setEditor({ open: true, initial: payload.initial || null, defaultDate: payload.defaultDate || null });
    };
    window.__editorListeners.add(fn);
    return () => window.__editorListeners.delete(fn);
  }, []);

  function closeEditor() { setEditor({ open: false, initial: null, defaultDate: null }); }

  function saveEvent(payload) {
    if (editor.initial && editor.initial.id) {
      update(editor.initial.id, payload);
    } else {
      add(payload);
    }
    closeEditor();
  }

  function deleteEvent(id) {
    remove(id);
    closeEditor();
  }

  aUseEffect(() => {
    if (tweaks.aesthetic) {
      document.documentElement.setAttribute("data-aesthetic", tweaks.aesthetic);
    }
  }, [tweaks.aesthetic]);

  aUseEffect(() => {
    if (tweaks.theme === "auto") {
      document.documentElement.removeAttribute("data-theme");
    } else {
      document.documentElement.setAttribute("data-theme", tweaks.theme);
    }
  }, [tweaks.theme]);

  return (
    <React.Fragment>
      <DesktopApp tweaks={tweaks} />
      <MobileApp tweaks={tweaks} />
      <Toast />

      <EventEditor
        open={editor.open}
        initial={editor.initial}
        defaultDate={editor.defaultDate}
        categories={window.CALENDAR_DATA.CATEGORIES}
        onSave={saveEvent}
        onDelete={deleteEvent}
        onClose={closeEditor}
      />

      <window.TweaksPanel>
        <window.TweakSection title="Look & feel">
          <window.TweakRadio
            label="Aesthetic"
            value={tweaks.aesthetic}
            options={[
              { value: "editorial", label: "Editorial" },
              { value: "minimal", label: "Minimal" },
              { value: "mono", label: "Mono" },
            ]}
            onChange={(v) => setTweak("aesthetic", v)}
          />
          <window.TweakRadio
            label="Density"
            value={tweaks.density}
            options={[
              { value: "roomy", label: "Roomy" },
              { value: "balanced", label: "Balanced" },
              { value: "dense", label: "Dense" },
            ]}
            onChange={(v) => setTweak("density", v)}
          />
          <window.TweakRadio
            label="Theme"
            value={tweaks.theme}
            options={[
              { value: "auto", label: "Auto" },
              { value: "light", label: "Light" },
              { value: "dark", label: "Dark" },
            ]}
            onChange={(v) => setTweak("theme", v)}
          />
        </window.TweakSection>

        <window.TweakSection title="Modules">
          <window.TweakToggle
            label="Habit strip"
            value={tweaks.showHabitStrip}
            onChange={(v) => setTweak("showHabitStrip", v)}
          />
          <window.TweakToggle
            label="Quick capture"
            value={tweaks.showQuickCapture}
            onChange={(v) => setTweak("showQuickCapture", v)}
          />
        </window.TweakSection>

        <window.TweakSection title="Preview">
          <window.TweakRadio
            label="Force view"
            value={tweaks.forcedView}
            options={[
              { value: "auto", label: "Auto" },
              { value: "desktop", label: "Desktop" },
              { value: "mobile", label: "Mobile" },
            ]}
            onChange={(v) => setTweak("forcedView", v)}
          />
        </window.TweakSection>

        <window.TweakSection title="Work calendar import">
          <window.TweakSecret
            label="Gemini API key"
            value={apiKey}
            placeholder="AIza…"
            onChange={setApiKey}
          />
          <window.TweakHelp>
            Stored locally in this browser. Restrict the key to the Generative Language API in Google Cloud Console.
          </window.TweakHelp>
          <window.TweakButton
            label="Import a screenshot"
            onClick={() => window.triggerScreenshotImport()}
          />
          {lastImport && (
            <div className="twk-status">
              Last: {lastImport.count} event{lastImport.count === 1 ? "" : "s"} ·{" "}
              {new Date(lastImport.at).toLocaleString("en-GB", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" })}
              {lastImport.weekStart ? ` · wk ${lastImport.weekStart}` : ""}
            </div>
          )}
        </window.TweakSection>

        {!gcal.unavailable && (
          <window.TweakSection title="Google Calendar (personal)">
            <window.TweakHelp>
              Two-way sync with your personal Google Calendar. Work-screenshot
              events stay local — only this calendar's events sync up.
            </window.TweakHelp>
            {gcal.loading ? (
              <div className="twk-status">Checking…</div>
            ) : gcal.connected ? (
              <React.Fragment>
                <div className="twk-status">
                  Connected{gcal.connectedAt
                    ? ` · since ${new Date(gcal.connectedAt).toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" })}`
                    : ""}
                </div>
                <window.TweakButton label="Disconnect" onClick={disconnectGcal} />
              </React.Fragment>
            ) : (
              <window.TweakButton
                label="Connect Google Calendar"
                onClick={() => { location.href = "/api/google/connect"; }}
              />
            )}
          </window.TweakSection>
        )}
      </window.TweaksPanel>
    </React.Fragment>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
