// Root App — all state, persistence, routing
const { useState, useEffect, useCallback, useRef } = React;

function App() {
  // === STATE ===
  const [sessions, setSessions] = useState(() => loadSessions());
  const [library, setLibrary] = useState(() => {
    const lib = loadLibrary();
    window.EXERCISE_LIBRARY = lib;
    return lib;
  });
  const [activeSession, setActiveSession] = useState(() => ensureBlockUids(loadActiveSession()));
  const [prefs, setPrefs] = useState(() => loadPrefs());
  const [page, setPage] = useState('dashboard');
  const [restTimer, setRestTimer] = useState(null);
  const [restPaused, setRestPaused] = useState(false);
  const [openSessionId, setOpenSessionId] = useState(null);
  const [openSessionViewMode, setOpenSessionViewMode] = useState(false);

  const handleDayPress = daySessions => {
    if (!daySessions.length) return;
    setOpenSessionId(daySessions[0].id);
    setOpenSessionViewMode(true);
    setPage('history');
    setTimeout(() => setOpenSessionId(null), 300);
  };

  const handleViewSession = sess => {
    setOpenSessionId(sess.id);
    setOpenSessionViewMode(true);
    setPage('history');
    setTimeout(() => setOpenSessionId(null), 300);
  };

  // === PERSIST ===
  useEffect(() => { saveSessions(sessions); }, [sessions]);
  useEffect(() => { window.EXERCISE_LIBRARY = library; saveLibrary(library); }, [library]);
  useEffect(() => { saveActiveSession(activeSession); }, [activeSession]);
  useEffect(() => { savePrefs(prefs); }, [prefs]);

  // === CLOUD SYNC — pull-first engine (createCloudSync in data.js). Every sync pulls, then
  // adopts / pushes / merges; a failed pull never pushes. Re-syncs on focus and reconnect. ===
  const [syncStatus, setSyncStatus] = useState('pending');
  const adopting = useRef(false); // true while applying pulled cloud data (so it isn't counted as an edit)
  const stateRef = useRef({});
  stateRef.current = { sessions, library, prefs };
  const sync = useRef(null);
  if (!sync.current) sync.current = createCloudSync({
    // bodyweight has no desktop UI but MUST ride along or a desktop push would drop it from the cloud
    getLocal: () => ({ ...stateRef.current, bodyweight: loadBodyweight() }),
    apply: data => {
      adopting.current = true;
      setSessions(data.sessions || []);
      if (data.library?.length) setLibrary(data.library);
      if (data.prefs)           setPrefs(p => ({ ...p, ...data.prefs }));
      if (data.bodyweight)      saveBodyweight(data.bodyweight);
    },
    onStatus: setSyncStatus,
  });
  useEffect(() => {
    sync.current.run().then(() => {
      // MacroFactor weight import (no desktop UI, but keeps the store current even if only
      // desktop is opened). A change is a local edit → stamp + sync.
      fetchMacroWeights().then(mf => {
        if (!mf) return;
        const p = loadPrefs();
        const before = loadBodyweight();
        const merged = mergeMacroWeights(before, mf, p.phase || '', p.bwIgnoredDates);
        if (!merged) return;
        trackEdits('bodyweight', before, merged);
        saveBodyweight(merged);
        bumpStamp();
        sync.current.schedule(0);
      });
    });
    const onVisible = () => { if (document.visibilityState === 'visible') sync.current.run(); };
    const onOnline = () => sync.current.run();
    document.addEventListener('visibilitychange', onVisible);
    window.addEventListener('online', onOnline);
    return () => { document.removeEventListener('visibilitychange', onVisible); window.removeEventListener('online', onOnline); };
  }, []);
  const syncedOnce = useRef(false); // skip the mount run (initial state isn't an edit)
  const prevData = useRef({ sessions, library });
  useEffect(() => {
    const prev = prevData.current;
    prevData.current = { sessions, library };
    if (!syncedOnce.current) { syncedOnce.current = true; return; }
    if (adopting.current) { adopting.current = false; return; } // pulled data — not an edit
    // Per-record edit stamps + delete tombstones — the merge relies on these (see data.js).
    trackEdits('sessions', prev.sessions, sessions);
    trackEdits('library', prev.library, library);
    bumpStamp();
    sync.current.schedule();
  }, [sessions, library, prefs]);

  // === APPLY CSS VARIABLES ===
  useEffect(() => {
    const meta = ACCENT_MAP[prefs.accent] || ACCENT_MAP['#c6f432'];
    document.documentElement.style.setProperty('--accent', prefs.accent);
    document.documentElement.style.setProperty('--accent-dim', meta.dim);
    document.documentElement.style.setProperty('--accent-glow', meta.glow);
    document.documentElement.dataset.density = prefs.density;
  }, [prefs.accent, prefs.density]);

  // === SESSION OPS ===
  const startSession = useCallback(({ name, preloadLast }) => {
    let exercises = [];
    if (preloadLast) {
      const last = latestSession(sessions, name);
      if (last) exercises = (last.exercises || []).map(b => ({ uid: generateId(), exId: b.exId, sets: [], notes: lastExerciseNote(sessions, b.exId) }));
    }
    setActiveSession({ id: generateId(), name, startedAt: new Date().toISOString(), exercises });
    setPage('active');
  }, [sessions]);

  const finishSession = useCallback(async () => {
    if (!activeSession) return;
    if (!totalSets(activeSession)) {
      await window.uiAlert('Log at least one set before finishing, or cancel the session.', { title:'Nothing logged' });
      return;
    }
    const session = {
      ...activeSession,
      endedAt: new Date().toISOString(),
      durationMin: Math.max(1, Math.round((Date.now() - new Date(activeSession.startedAt).getTime()) / 60000)),
    };
    // Pushed from inside the updater so the PR marks in the event description are computed
    // against the CURRENT history (this callback's `sessions` closure can be stale).
    setSessions(prev => { calendarPushSession(session, { sessions: prev }); return [session, ...prev]; });
    setActiveSession(null);
    setRestTimer(null);
    setPage('dashboard');
  }, [activeSession]);

  const cancelSession = useCallback(async () => {
    if (!(await window.uiConfirm('All logged sets will be lost.', { title:'Discard this session?', confirmLabel:'Discard' }))) return;
    setActiveSession(null);
    setRestTimer(null);
    setPage('dashboard');
  }, []);

  // Block ops are keyed by the block's unique `uid` (not exId) so the same exercise
  // can appear more than once in a session without ops hitting both blocks.
  const updateSession = {
    start: startSession,
    addSet: useCallback((uid, setData) => setActiveSession(s => ({
      ...s, exercises: s.exercises.map(e => e.uid === uid ? { ...e, sets: [...e.sets, setData] } : e)
    })), []),
    deleteSet: useCallback((uid, idx) => setActiveSession(s => ({
      ...s, exercises: s.exercises.map(e => e.uid === uid ? { ...e, sets: e.sets.filter((_, i) => i !== idx) } : e)
    })), []),
    updateSet: useCallback((uid, idx, data) => setActiveSession(s => ({
      ...s, exercises: s.exercises.map(e => e.uid === uid ? { ...e, sets: e.sets.map((x, i) => i === idx ? { ...x, ...data } : x) } : e)
    })), []),
    addExercise: useCallback((exId) => setActiveSession(s => ({
      ...s, exercises: [...s.exercises, { uid: generateId(), exId, sets: [], notes: lastExerciseNote(sessions, exId) }]
    })), [sessions]),
    updateExercise: useCallback((uid, data) => setActiveSession(s => ({
      ...s, exercises: s.exercises.map(e => e.uid === uid ? { ...e, ...data } : e)
    })), []),
    removeExercise: useCallback((uid) => setActiveSession(s => ({
      ...s, exercises: s.exercises.filter(e => e.uid !== uid)
    })), []),
  };

  const startTimer = useCallback((seconds) => {
    if (!seconds) return;
    setRestTimer({ seconds, key: Date.now() });
    setRestPaused(false);
  }, []);

  // === LIBRARY OPS ===
  const addExercise    = useCallback(ex        => setLibrary(prev => [...prev, { id: generateId(), ...ex }]), []);
  const updateExercise = useCallback((id, data) => setLibrary(prev => prev.map(e => e.id === id ? { ...e, ...data } : e)), []);
  const deleteExercise = useCallback(id        => setLibrary(prev => prev.filter(e => e.id !== id)), []);

  // === HISTORY OPS ===
  // Deleting a session also removes its Google Calendar event (no-op if it never had one).
  const deleteSession = useCallback(id => setSessions(prev => {
    const gone = prev.find(s => s.id === id);
    if (gone) calendarDeleteSession(gone);
    return prev.filter(s => s.id !== id);
  }), []);
  const upsertSession = useCallback(session => setSessions(prev => {
    const exists = prev.some(s => s.id === session.id);
    // Editing an existing session: keep its calendar event in sync (time/duration/name).
    // No-op for sessions that never got an event (manually logged past sessions).
    if (exists) calendarUpdateSession(session, prev);
    return exists ? prev.map(s => s.id === session.id ? session : s) : [session, ...prev];
  }), []);

  // === RENDER ===
  const navProps = { page, setPage, sessionActive: !!activeSession };

  return (
    <div style={{ display:'flex', height:'100vh', overflow:'hidden' }}>
      <Sidebar
        {...navProps}
        collapsed={prefs.sidebarCollapsed}
        setCollapsed={c => setPrefs(p => ({ ...p, sidebarCollapsed: c }))}
      />

      <main style={{ flex:1, overflowY:'auto', overflowX:'hidden', height:'100vh' }}>
        {page === 'dashboard' && (
          <DashboardPage
            units={prefs.units} sessions={sessions} library={library}
            onStartSession={() => setPage('active')}
            sessionActive={!!activeSession}
            onDayPress={handleDayPress}
            onGoToHistory={() => setPage('history')}
            onViewSession={handleViewSession}
          />
        )}
        {page === 'active' && (
          <ActiveSessionPage
            units={prefs.units} sessions={sessions} library={library}
            session={activeSession}
            updateSession={updateSession}
            finishSession={finishSession}
            cancelSession={cancelSession}
            startTimer={startTimer}
            gymFilter={prefs.gym}
            onAddToLibrary={addExercise}
          />
        )}
        {page === 'history' && (
          <HistoryPage
            units={prefs.units} sessions={sessions} library={library}
            onDeleteSession={deleteSession}
            onUpsertSession={upsertSession}
            gymFilter={prefs.gym}
            openSessionId={openSessionId}
            openInViewMode={openSessionViewMode}
            onAddToLibrary={addExercise}
          />
        )}
        {page === 'analytics' && (
          <AnalyticsPage units={prefs.units} sessions={sessions} library={library}
            onOpenSession={handleViewSession}/>
        )}
        {page === 'library' && (
          <LibraryPage
            units={prefs.units} library={library} sessions={sessions}
            onAddExercise={addExercise}
            onUpdateExercise={updateExercise}
            onDeleteExercise={deleteExercise}
            gymFilter={prefs.gym}
          />
        )}
        {page === 'settings' && (
          <SettingsPage prefs={prefs} onPrefsChange={setPrefs} sessions={sessions} library={library}
            syncStatus={syncStatus} onSyncNow={() => sync.current.run()}/>
        )}
      </main>

      {restTimer && (
        <RestTimer
          key={restTimer.key}
          seconds={restTimer.seconds}
          paused={restPaused}
          setPaused={setRestPaused}
          onDismiss={() => setRestTimer(null)}
        />
      )}
      <ConfirmHost/>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<ErrorBoundary><App/></ErrorBoundary>);
