// Daily Prep — App shell, nav, store, and Tweaks integration

const { useState: useS, useEffect: useE, useMemo: useM, useCallback, useRef: useR } = React;
// Components are loaded from sibling babel scripts via window.
const TodayView = window.TodayView;
const WeekView = window.WeekView;
const RoadmapView = window.RoadmapView;
const BacklogView = window.BacklogView;
const JournalView = window.JournalView;
const { useTweaks, TweaksPanel, TweakSection, TweakRadio, TweakColor,
        TweakToggle, TweakText, TweakButton } = window;

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "radius": 2,
  "density": "compact",
  "accent": "#c75a3a",
  "markStyle": "mark",
  "optionCount": 3
}/*EDITMODE-END*/;

const STORE = window.CAIRN_STORE;

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [view, setView] = useS("today");
  const [tasks, setTasks] = useS(() => window.CAIRN.TASKS.slice());
  const [journals, setJournals] = useS(() => []);
  const [pendingJournalTaskId, setPendingJournalTaskId] = useS(null);

  // ── Sync state ──
  const [syncConfig, setSyncConfigState] = useS(() => STORE.config());
  const [syncStatus, setSyncStatus] = useS('idle');          // idle | syncing | error | offline | conflict
  const [syncError, setSyncError] = useS(null);              // last error message
  const [lastSyncedAt, setLastSyncedAt] = useS(null);
  const [testResult, setTestResult] = useS(null);            // {ok, message}
  // Gate auto-backlog until the first hydrate (local + remote reconcile) settles.
  // Without it, an empty boot (incognito / cleared storage) would roll the
  // past-dated *seed* before the cloud pull lands, manufacturing rollover stamps
  // that could regress real completions. See rolloverMissed + store.preferTask.
  const [hydrated, setHydrated] = useS(false);

  // Refs so async callbacks always see the latest arrays without re-binding.
  const tasksRef    = useR(tasks);
  const journalsRef = useR(journals);
  useE(() => { tasksRef.current    = tasks;    }, [tasks]);
  useE(() => { journalsRef.current = journals; }, [journals]);

  const today = window.CAIRN.TODAY;
  const U = window.CAIRN_UTIL;

  // Apply tweaks to CSS variables
  useE(() => {
    const root = document.documentElement;
    root.style.setProperty("--radius", `${t.radius}px`);
    const dens = { compact: 0.72, regular: 1.0, comfy: 1.18 }[t.density] || 1.0;
    root.style.setProperty("--density", String(dens));
    if (t.accent) {
      // Approximate OKLCH from the hex; we just use the hex directly here
      // since the design tokens map to OKLCH conceptually.
      root.style.setProperty("--accent", t.accent);
      // derive a slightly darker press color from the hex (simple shift)
      root.style.setProperty("--accent-2", shadeHex(t.accent, -10));
    }
  }, [t.radius, t.density, t.accent]);

  // ── Task ops ──
  // _modifiedAt stamped on every mutation so the cross-device merger has a
  // last-writer-wins comparator. Missing values are treated as 0 by the merger.
  const updateTask = useCallback((id, patch) => {
    setTasks(prev => prev.map(x => x.id === id ? { ...x, ...patch, _modifiedAt: Date.now() } : x));
  }, []);

  const addTask = useCallback((task) => {
    const id = "t" + Math.random().toString(36).slice(2, 9);
    setTasks(prev => [...prev, { id, ...task, _modifiedAt: Date.now() }]);
  }, []);

  const deleteTask = useCallback((id) => {
    setTasks(prev => prev.filter(x => x.id !== id));
  }, []);

  // ── Journal ops ──
  // updatedAt is the merge key — stamped on add and every update.
  const addJournal = useCallback((entry) => {
    const id = "j" + Math.random().toString(36).slice(2, 9);
    const now = U.nowLocalISOMinute();
    const full = {
      id,
      taskId: null,
      linkedTitleSnapshot: null,
      title: "",
      body: "",
      createdAt: now,
      updatedAt: now,
      ...entry,
    };
    setJournals(prev => [...prev, full]);
    return id;
  }, []);

  const updateJournal = useCallback((id, patch) => {
    setJournals(prev => prev.map(j => j.id === id
      ? { ...j, ...patch, updatedAt: U.nowLocalISOMinute() }
      : j
    ));
  }, []);

  const deleteJournal = useCallback((id) => {
    setJournals(prev => prev.filter(j => j.id !== id));
  }, []);

  // ── Sync callbacks ──
  const onSyncStatus = useCallback((status, info) => {
    setSyncStatus(status);
    if (status === 'idle') {
      setLastSyncedAt(Date.now());
      setSyncError(null);
    } else if (status === 'error' || status === 'offline' || status === 'conflict') {
      setSyncError(info && info.message ? info.message : null);
    }
  }, []);

  const onSyncMerge = useCallback((merged) => {
    // Conflict resolved by merging cloud + local; reflect back into React state
    // so the UI matches what was pushed to R2.
    if (merged && Array.isArray(merged.tasks))    setTasks(merged.tasks);
    if (merged && Array.isArray(merged.journals)) setJournals(merged.journals);
  }, []);

  // ── Hydrate: localStorage first (fast paint), then R2 if sync is on. ──
  useE(() => {
    const local = STORE.loadLocal();
    if (local.tasks)    setTasks(local.tasks);
    if (local.journals) setJournals(local.journals);

    const cfg = STORE.config();
    if (!cfg.enabled) { setHydrated(true); return; }
    let cancelled = false;
    (async () => {
      try {
        setSyncStatus('syncing');
        const remote = await STORE.fetchRemote();
        if (cancelled) return;
        const merged = STORE.mergeState(
          { tasks: local.tasks || window.CAIRN.TASKS.slice(), journals: local.journals || [] },
          { tasks: remote.tasks, journals: remote.journals }
        );
        STORE.setEtag(remote.etag);
        STORE.saveLocal(merged);
        setTasks(merged.tasks);
        setJournals(merged.journals);
        setLastSyncedAt(remote.savedAt || Date.now());
        setSyncStatus('idle');
        setSyncError(null);
      } catch (e) {
        if (cancelled) return;
        if (e && e.name === 'NotFoundError') {
          // No remote object yet — push current local up so the cloud is seeded.
          try {
            const etag = await STORE.pushRemote(
              { tasks: tasksRef.current, journals: journalsRef.current },
              { ifNoneMatch: '*' },
            );
            if (etag) STORE.setEtag(etag);
            setSyncStatus('idle');
            setLastSyncedAt(Date.now());
            setSyncError(null);
          } catch (e2) {
            setSyncStatus('error');
            setSyncError(e2 && e2.message);
          }
          return;
        }
        if (e && e.name === 'AuthError')    { setSyncStatus('error');   setSyncError('Auth failed — check token.'); return; }
        if (e && e.name === 'NetworkError') { setSyncStatus('offline'); setSyncError(e.message); return; }
        setSyncStatus('error'); setSyncError(e && e.message);
      } finally {
        if (!cancelled) setHydrated(true);
      }
    })();
    return () => { cancelled = true; };
  }, []);  // run once on mount

  // ── Persist on every change: localStorage (sync) + debounced R2 PUT. ──
  useE(() => {
    STORE.saveLocal({ tasks, journals });
    if (STORE.config().enabled) {
      STORE.scheduleSave({ tasks, journals }, { onStatus: onSyncStatus, onMerge: onSyncMerge });
    }
  }, [tasks, journals, onSyncStatus, onSyncMerge]);

  // ── Auto-backlog missed tasks. ──
  // A planned task whose scheduledDate slipped into the past would otherwise
  // stay stranded in its old week. Rolling it back to the backlog (scheduledDate
  // = null) re-surfaces it in the queue + Today chooser. Keyed on [tasks, today]
  // so it runs after *every* task source (local load, remote merge, conflict
  // resolution, pull, reset) — it's idempotent, so it settles in one extra pass
  // (the moved tasks no longer match), and the move flows out through the persist
  // effect above to localStorage + R2.
  useE(() => {
    if (!hydrated) return;            // wait for the first local+remote reconcile
    const rolled = U.rolloverMissed(tasks, today);
    if (rolled.changed) setTasks(rolled.tasks);
  }, [tasks, today, hydrated]);

  // ── Reset / clear ──
  const resetData = () => {
    if (!confirm("Reset all tasks back to the seed?")) return;
    const seed = window.CAIRN.TASKS.slice();
    STORE.clearLocalTasks();
    setTasks(seed);
    if (STORE.config().enabled) {
      (async () => {
        try {
          setSyncStatus('syncing');
          const etag = await STORE.pushRemote(
            { tasks: seed, journals: journalsRef.current },
            { ifMatch: '*' },
          );
          if (etag) STORE.setEtag(etag);
          setSyncStatus('idle');
          setLastSyncedAt(Date.now());
        } catch (e) {
          setSyncStatus('error'); setSyncError(e && e.message);
        }
      })();
    } else {
      STORE.setEtag(null);
    }
  };

  const clearJournal = () => {
    if (!confirm("Delete all journal entries?")) return;
    STORE.clearLocalJournals();
    setJournals([]);
    if (STORE.config().enabled) {
      (async () => {
        try {
          setSyncStatus('syncing');
          const etag = await STORE.pushRemote(
            { tasks: tasksRef.current, journals: [] },
            { ifMatch: '*' },
          );
          if (etag) STORE.setEtag(etag);
          setSyncStatus('idle');
          setLastSyncedAt(Date.now());
        } catch (e) {
          setSyncStatus('error'); setSyncError(e && e.message);
        }
      })();
    }
  };

  const resetBacklog = () => {
    if (!hydrated) return;
    const C = window.CAIRN;
    const preview = U.replanBacklog(tasks, today, C.SETTINGS);
    if (!preview.changed) {
      alert("Backlog is already clear — nothing to redistribute.");
      return;
    }
    const end = U.formatLongDate(preview.newEndDate);
    if (!confirm(
      `Redistribute ${preview.movedCount} planned task(s) across today → ${end}, ` +
      `in curriculum order at your daily budgets?\n\nCompleted and skipped work stays put.`
    )) return;
    setTasks(preview.tasks);
  };

  // Route helper used by the Week chip → Journal deep link.
  const openJournalFor = useCallback((taskId) => {
    setPendingJournalTaskId(taskId);
    setView("journal");
  }, []);

  // ── Sync UI handlers ──
  const applySyncConfig = (patch) => {
    const next = STORE.setConfig(patch);
    setSyncConfigState(next);
    setTestResult(null);
  };

  const handleSyncToggle = async (v) => {
    if (!v) { applySyncConfig({ enabled: false }); return; }
    const cfg = STORE.config();
    if (!cfg.url || !cfg.token) {
      setTestResult({ ok: false, message: 'Set Worker URL and token first.' });
      return;
    }
    try {
      setSyncStatus('syncing');
      const head = await STORE.headRemote();
      if (head.exists) {
        const useCloud = confirm(
          "Cloud already has cairn data.\n\n" +
          "OK    → pull cloud into this device (overwrites local).\n" +
          "Cancel → keep local; will overwrite cloud on next change."
        );
        if (useCloud) {
          const remote = await STORE.fetchRemote();
          STORE.setEtag(remote.etag);
          STORE.saveLocal({ tasks: remote.tasks, journals: remote.journals });
          setTasks(remote.tasks);
          setJournals(remote.journals);
          setLastSyncedAt(remote.savedAt || Date.now());
        } else {
          const etag = await STORE.pushRemote(
            { tasks: tasksRef.current, journals: journalsRef.current },
            { ifMatch: '*' },
          );
          if (etag) STORE.setEtag(etag);
          setLastSyncedAt(Date.now());
        }
      } else {
        const etag = await STORE.pushRemote(
          { tasks: tasksRef.current, journals: journalsRef.current },
          { ifNoneMatch: '*' },
        );
        if (etag) STORE.setEtag(etag);
        setLastSyncedAt(Date.now());
      }
      applySyncConfig({ enabled: true });
      setSyncStatus('idle');
      setSyncError(null);
    } catch (e) {
      setSyncStatus('error');
      setSyncError(e && e.message);
      setTestResult({ ok: false, message: (e && e.message) || 'Connection failed.' });
    }
  };

  const handleTestConnection = async () => {
    setTestResult(null);
    try {
      const head = await STORE.headRemote();
      setTestResult({ ok: true, message: head.exists ? 'OK — cloud has data.' : 'OK — cloud is empty.' });
    } catch (e) {
      setTestResult({ ok: false, message: (e && e.message) || 'Connection failed.' });
    }
  };

  const handlePullFromCloud = async () => {
    try {
      setSyncStatus('syncing');
      const remote = await STORE.fetchRemote();
      STORE.setEtag(remote.etag);
      STORE.saveLocal({ tasks: remote.tasks, journals: remote.journals });
      setTasks(remote.tasks);
      setJournals(remote.journals);
      setLastSyncedAt(remote.savedAt || Date.now());
      setSyncStatus('idle'); setSyncError(null);
    } catch (e) {
      setSyncStatus('error'); setSyncError(e && e.message);
    }
  };

  const handlePushLocalUp = async () => {
    if (!confirm("Overwrite cloud with this device's tasks + journals?")) return;
    try {
      setSyncStatus('syncing');
      const etag = await STORE.pushRemote(
        { tasks: tasksRef.current, journals: journalsRef.current },
        { ifMatch: '*' },
      );
      if (etag) STORE.setEtag(etag);
      setLastSyncedAt(Date.now());
      setSyncStatus('idle'); setSyncError(null);
    } catch (e) {
      setSyncStatus('error'); setSyncError(e && e.message);
    }
  };

  // Status dot color (only rendered when sync is enabled).
  const dotColor =
    syncStatus === 'idle'    ? '#3d6b4e' :  // green
    syncStatus === 'syncing' ? '#c79a3a' :  // amber
                               '#c75a3a';   // red for error/offline/conflict
  const dotTitle =
    syncStatus === 'idle'     ? (lastSyncedAt ? `Synced ${relTime(lastSyncedAt)}` : 'Synced') :
    syncStatus === 'syncing'  ? 'Syncing…' :
    syncStatus === 'offline'  ? 'Offline — changes will sync when online' :
    syncStatus === 'conflict' ? 'Conflict — open Tweaks → Sync' :
                                (syncError || 'Sync error');

  return (
    <div className="app">
      <header className="nav">
        <div className="shell shell--wide nav__inner">
          <div className="nav__mark">
            <svg
              className="nav__mark-glyph"
              viewBox="0 0 16 16"
              width="14"
              height="14"
              aria-hidden="true"
              focusable="false"
            >
              <g
                fill="none"
                stroke="currentColor"
                strokeWidth="1.15"
                strokeLinecap="round"
              >
                <path d="M 2.0 13   Q 8 9.5  14 13" />
                <path d="M 3.6 10.4 Q 8 7.6  12.4 10.4" />
                <path d="M 5.2 7.8  Q 8 5.8  10.8 7.8" />
              </g>
              <circle cx="8" cy="4.0" r="0.9" fill="currentColor" />
            </svg>
            <span>cairn</span>
            {syncConfig.enabled && (
              <span
                aria-label={dotTitle}
                title={dotTitle}
                style={{
                  display: 'inline-block',
                  width: 7, height: 7,
                  marginLeft: 8,
                  borderRadius: '50%',
                  background: dotColor,
                  verticalAlign: 'middle',
                  transition: 'background .2s',
                }}
              />
            )}
          </div>
          <nav className="nav__links">
            {["today", "week", "roadmap", "backlog", "journal"].map(v => (
              <button
                key={v}
                className="nav__link"
                aria-current={view === v ? "page" : undefined}
                onClick={() => setView(v)}
              >
                {v[0].toUpperCase() + v.slice(1)}
              </button>
            ))}
          </nav>
        </div>
      </header>

      <main className="main" data-screen-label={view}>
        {view === "today" && (
          <TodayView
            tasks={tasks}
            journals={journals}
            today={today}
            onUpdateTask={updateTask}
            onAddTask={addTask}
            onAddJournal={addJournal}
            onUpdateJournal={updateJournal}
            onDeleteJournal={deleteJournal}
            onNavigate={setView}
            tweaks={t}
          />
        )}
        {view === "week" && (
          <WeekView
            tasks={tasks}
            journals={journals}
            today={today}
            onUpdateTask={updateTask}
            onAddTask={addTask}
            onDeleteTask={deleteTask}
            onOpenJournalFor={openJournalFor}
            tweaks={t}
          />
        )}
        {view === "roadmap" && (
          <RoadmapView tasks={tasks} today={today} tweaks={t} />
        )}
        {view === "backlog" && (
          <BacklogView
            tasks={tasks}
            journals={journals}
            today={today}
            onUpdateTask={updateTask}
            onAddTask={addTask}
            onDeleteTask={deleteTask}
            onAddJournal={addJournal}
            onUpdateJournal={updateJournal}
            onDeleteJournal={deleteJournal}
            tweaks={t}
          />
        )}
        {view === "journal" && JournalView && (
          <JournalView
            tasks={tasks}
            journals={journals}
            today={today}
            pendingTaskId={pendingJournalTaskId}
            onClearPendingTaskId={() => setPendingJournalTaskId(null)}
            onAddJournal={addJournal}
            onUpdateJournal={updateJournal}
            onDeleteJournal={deleteJournal}
            onNavigate={setView}
            tweaks={t}
          />
        )}
      </main>

      <TweaksPanel>
        <TweakSection label="Form" />
        <TweakRadio
          label="Corners"
          value={String(t.radius)}
          options={["0", "2", "4"]}
          onChange={(v) => setTweak("radius", parseInt(v, 10))}
        />
        <TweakRadio
          label="Density"
          value={t.density}
          options={["compact", "regular", "comfy"]}
          onChange={(v) => setTweak("density", v)}
        />
        <TweakRadio
          label="Track mark"
          value={t.markStyle}
          options={["mark", "rule"]}
          onChange={(v) => setTweak("markStyle", v)}
        />

        <TweakSection label="Accent" />
        <TweakColor
          label="Accent hue"
          value={t.accent}
          options={["#c75a3a", "#9c6d36", "#7b6f3b", "#3d6b4e", "#3a5b75", "#5c4d6e"]}
          onChange={(v) => setTweak("accent", v)}
        />

        <TweakSection label="Today" />
        <TweakRadio
          label="Options to show"
          value={String(t.optionCount)}
          options={["2", "3"]}
          onChange={(v) => setTweak("optionCount", parseInt(v, 10))}
        />

        <TweakSection label="Sync" />
        <TweakText
          label="Worker URL"
          value={syncConfig.injected ? syncHostOnly(syncConfig.url) : syncConfig.url}
          placeholder="https://cairn-sync.<acct>.workers.dev"
          onChange={(v) => applySyncConfig({ url: v.trim() })}
          readOnly={syncConfig.injected}
        />
        <TweakText
          label="Token"
          value={syncConfig.injected ? '••••••••' : syncConfig.token}
          placeholder="bearer token"
          onChange={(v) => applySyncConfig({ token: v.trim() })}
          readOnly={syncConfig.injected}
        />
        {syncConfig.injected && (
          <div style={{
            marginTop: -4,
            fontSize: 10.5,
            lineHeight: 1.4,
            color: 'rgba(41,38,27,.55)',
          }}>
            Managed via Vercel env. Toggle still controls whether sync runs.
          </div>
        )}
        <TweakToggle
          label="Cloud sync"
          value={syncConfig.enabled}
          onChange={handleSyncToggle}
        />
        <TweakButton label="Test connection" onClick={handleTestConnection} />
        <TweakButton label="Pull from cloud" secondary onClick={handlePullFromCloud} />
        <TweakButton label="Push local up"   secondary onClick={handlePushLocalUp} />
        <div style={{
          marginTop: 4,
          fontSize: 10.5,
          lineHeight: 1.4,
          color: testResult
            ? (testResult.ok ? '#3d6b4e' : '#c75a3a')
            : (syncStatus === 'error' || syncStatus === 'offline' || syncStatus === 'conflict')
              ? '#c75a3a'
              : 'rgba(41,38,27,.55)',
        }}>
          {testResult
            ? testResult.message
            : syncConfig.enabled
              ? `Status: ${syncStatus}${lastSyncedAt ? ` · last sync ${relTime(lastSyncedAt)}` : ''}${syncError ? ` · ${syncError}` : ''}`
              : 'Off — tasks stay on this device.'}
        </div>

        <TweakSection label="Data" />
        <TweakButton label="Reset to seed" onClick={resetData} />
        <TweakButton label="Reset backlog" secondary onClick={resetBacklog} />
        <TweakButton label="Clear journal" onClick={clearJournal} />
      </TweaksPanel>
    </div>
  );
}

// Simple hex shifter for accent press state
function shadeHex(hex, pct) {
  const h = hex.replace("#", "");
  const num = parseInt(h.length === 3
    ? h.split("").map(c => c + c).join("")
    : h, 16);
  let r = (num >> 16) + Math.round(pct * 2.55);
  let g = ((num >> 8) & 0xff) + Math.round(pct * 2.55);
  let b = (num & 0xff) + Math.round(pct * 2.55);
  r = Math.max(0, Math.min(255, r));
  g = Math.max(0, Math.min(255, g));
  b = Math.max(0, Math.min(255, b));
  return "#" + ((1 << 24) | (r << 16) | (g << 8) | b).toString(16).slice(1);
}

// Show only the host of an injected sync URL in the Tweaks panel.
// Avoids leaking the full URL into the rendered DOM on shared devices, while
// still giving the user a visual confirmation that the env wiring landed.
function syncHostOnly(url) {
  if (!url) return '';
  try { return new URL(url).host; }
  catch (_) { return url; }
}

function relTime(ms) {
  if (!ms) return '';
  const diff = Math.floor((Date.now() - ms) / 1000);
  if (diff < 5)    return 'just now';
  if (diff < 60)   return `${diff}s ago`;
  if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
  if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
  return `${Math.floor(diff / 86400)}d ago`;
}

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