// Source: index.html script block (extracted). Loaded via <script type="text/babel"> in index.html.
const { useState, useEffect } = React;

function App() {
  const [user, setUser] = useState(null);
  const [authReady, setAuthReady] = useState(false);
  const [route, setRoute] = useState(() => localStorage.getItem("caa_route") || "queue");
  const [activeId, setActiveId] = useState(() => {
    const stored = localStorage.getItem("caa_task");
    return stored && TASKS.some(t => t.id === stored) ? stored : (TASKS[0] && TASKS[0].id);
  });
  // Tasks live in state so new tasks can be authored in-app (see createBlankTask).
  const [tasks, setTasks] = useState(() => TASKS);
  const [loadingTasks, setLoadingTasks] = useState(true);
  // D1 counts (total + by_status + by_domain) for UI widgets. null until the
  // first /api/tasks/stats resolves; falls back to in-memory reduce when null.
  const [taskStats, setTaskStats] = useState(null);
  // Keep the global in sync so other components (Simulator seed, Review counts)
  // that still reference window.TASKS see the latest list.
  useEffect(() => { window.TASKS = tasks; }, [tasks]);

  // Merge a batch of tasks fetched by downstream components (e.g. Queue's
  // load-more / server search) back into app-level state, so navigating into
  // Editor/Simulator can still look them up by id.
  const onTasksLoaded = React.useCallback((incoming) => {
    if (!Array.isArray(incoming) || !incoming.length) return;
    setTasks(prev => {
      const byId = new Map(prev.map(t => [t.id, t]));
      let changed = false;
      for (const t of incoming) {
        if (!t || !t.id) continue;
        if (!byId.has(t.id)) { byId.set(t.id, t); changed = true; }
        // Don't clobber locally-edited rows; App is the source of truth for those.
      }
      return changed ? Array.from(byId.values()) : prev;
    });
  }, []);

  // Pull counts from D1. Debounced refresh is exposed via refreshStats() so
  // mutations can invalidate without hammering the endpoint.
  const refreshStatsTimer = React.useRef(null);
  const refreshStats = React.useCallback(() => {
    const api = window.tasksApi;
    if (!api || api.OFFLINE) return;
    if (refreshStatsTimer.current) clearTimeout(refreshStatsTimer.current);
    refreshStatsTimer.current = setTimeout(async () => {
      try {
        if (api.invalidateStats) api.invalidateStats();
        const s = await api.stats({});
        if (s && typeof s.total === "number") setTaskStats(s);
      } catch (err) {
        if (!(err && err.offline)) console.warn("tasksApi.stats failed:", err && err.message || err);
      }
    }, 150);
  }, []);
  useEffect(() => () => { if (refreshStatsTimer.current) clearTimeout(refreshStatsTimer.current); }, []);

  // Fetch the first page from D1 via /api/tasks. If the backend isn't
  // available (Python static server, missing D1 binding) fall back to the
  // static primekg_tasks.json seed so local dev still works.
  useEffect(() => {
    let cancelled = false;
    const mergeIntoState = (incoming) => {
      if (!Array.isArray(incoming) || !incoming.length) return;
      setTasks(prev => {
        const byId = new Map();
        for (const t of incoming) if (t && t.id) byId.set(t.id, t);
        // Preserve any locally-edited seed rows (e.g. in-app authored tasks).
        for (const t of prev) if (t && t.id) byId.set(t.id, t);
        return Array.from(byId.values());
      });
    };
    (async () => {
      try {
        const api = window.tasksApi;
        if (!api) throw Object.assign(new Error("tasksApi missing"), { offline: true });
        const res = await api.list({ limit: 200 });
        if (cancelled) return;
        mergeIntoState(res && res.items);
        // Kick off stats in the background — don't block the list load on it.
        api.stats({}).then(s => {
          if (!cancelled && s && typeof s.total === "number") setTaskStats(s);
        }).catch(err => {
          if (!(err && err.offline)) console.warn("tasksApi.stats initial fetch failed:", err && err.message || err);
        });
      } catch (err) {
        if (cancelled) return;
        if (!err || !err.offline) console.warn("tasks api list failed:", err);
        try {
          const full = await loadAllPrimekgTasks();
          if (!cancelled) mergeIntoState(full);
        } catch (e2) {
          console.warn("static primekg fallback failed:", e2);
        }
      } finally {
        if (!cancelled) setLoadingTasks(false);
      }
    })();
    return () => { cancelled = true; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Best-effort server writes. Local state stays snappy regardless — if the
  // server is offline or rejects the call, we just log and move on.
  const syncTaskToServer = (action, task, patch) => {
    const api = window.tasksApi;
    if (!api || api.OFFLINE) return;
    const run = action === "create"
      ? api.create(task)
      : api.update(task.id, patch || task);
    run.catch(err => {
      if (err && err.offline) return;
      console.warn(`tasksApi.${action} failed for ${task && task.id}:`, err && err.message || err);
    });
  };

  const addTask = (partial) => {
    const t = createBlankTask(partial || {});
    setTasks(prev => [t, ...prev]);
    setActiveId(t.id);
    syncTaskToServer("create", t);
    refreshStats();
    return t;
  };
  const updateTask = (id, patch) => {
    setTasks(prev => prev.map(t => t.id === id ? { ...t, ...patch, lastEdited: "just now" } : t));
    syncTaskToServer("update", { id }, patch);
    // Only re-fetch counts when the patch touches a denormalized column that
    // shows up in stats. Covers the "approve" click and status changes.
    if (patch && (("status" in patch) || ("domain" in patch))) refreshStats();
  };

  useEffect(()=>{ localStorage.setItem("caa_route", route); }, [route]);
  useEffect(()=>{ localStorage.setItem("caa_task", activeId); }, [activeId]);

  // Server is the source of truth for auth. On mount, ask /api/auth/me
  // whether we have a live session cookie. Anything in localStorage from
  // earlier builds is ignored.
  useEffect(() => {
    let cancelled = false;
    fetch("/api/auth/me", { credentials: "same-origin" })
      .then(r => (r.ok ? r.json() : { user: null }))
      .then(data => {
        if (cancelled) return;
        const u = data && data.user;
        if (u && u.email) {
          const name = u.name || u.email.split("@")[0];
          const role = u.role === "manager" ? "Data lead · Manager" : "Annotator · Clinical";
          setUser({
            name,
            email: u.email,
            initials: (window.initialsFromName && window.initialsFromName(name)) || name.slice(0,2).toUpperCase(),
            role,
            kind: u.role === "manager" ? "manager" : "clinician",
            avatar_url: u.avatar_url || null,
          });
        } else {
          setUser(null);
        }
      })
      .catch(() => { if (!cancelled) setUser(null); })
      .finally(() => { if (!cancelled) setAuthReady(true); });
    return () => { cancelled = true; };
  }, []);

  const onLogout = async () => {
    try { await fetch("/api/auth/logout", { method: "POST", credentials: "same-origin" }); } catch {}
    try { localStorage.removeItem("caa_user"); } catch {}
    setUser(null);
    try { window.location.reload(); } catch {}
  };

  // Expose the logged-in user to non-React callers (e.g. the Docs converter
  // panel reads this to populate lastEditor). Kept in sync with React state.
  useEffect(() => { window.__CAA_USER = user; }, [user]);

  // Load the global tool catalog once we have a session, and refresh it
  // whenever something invalidates it (saveTool/updateTool/deleteTool fire
  // `caa-tool-catalog-invalidated`). Non-blocking — buildToolsForTask and
  // the simulator executor fall back gracefully when the catalog is empty.
  useEffect(() => {
    if (!user) return;
    const api = window.tasksApi;
    if (!api || !api.loadToolCatalog) return;
    let cancelled = false;
    const reload = () => { if (!cancelled) api.loadToolCatalog().catch(() => {}); };
    reload();
    window.addEventListener("caa-tool-catalog-invalidated", reload);
    return () => {
      cancelled = true;
      window.removeEventListener("caa-tool-catalog-invalidated", reload);
    };
  }, [user]);

  // Converter panel → app bridge. Two events:
  //   caa-add-converted-task — append a converted task into state, persist to
  //     D1, and ack via caa-converted-task-result so the panel can toast.
  //   caa-set-route — switch the app to a route (e.g. "queue") with an active
  //     task + an optional cohort prefilter. Uses the same prefilter channel
  //     the sidebar uses (window.__caaQueuePrefilter + caa-queue-prefilter).
  useEffect(() => {
    const ackResult = (taskId, ok, error) => {
      try {
        window.dispatchEvent(new CustomEvent("caa-converted-task-result", {
          detail: { taskId, ok, error: error || "" },
        }));
      } catch (e) { /* ignore */ }
    };

    const onAddConverted = (ev) => {
      const detail = ev && ev.detail;
      if (!detail || !detail.task || !detail.task.id) return;
      const { task, cohortId, cohortLabel, format } = detail;
      const withCohort = { ...task, cohortId: cohortId || task.cohortId };
      setTasks(prev => {
        const exists = prev.some(t => t.id === withCohort.id);
        return exists ? prev.map(t => t.id === withCohort.id ? withCohort : t) : [withCohort, ...prev];
      });
      setActiveId(withCohort.id);

      const api = window.tasksApi;
      if (!api || api.OFFLINE) {
        // No backend — local-only add still counts as success from the user's POV.
        ackResult(withCohort.id, true);
        refreshStats();
        return;
      }
      // Prefer importCohort (creates the cohort row + task in one go) when the
      // current session is a manager. Non-managers fall back to plain create();
      // the task still gets the cohort_id column so stats group correctly, we
      // just can't upsert the cohorts table without manager role.
      const isManager = user && user.kind === "manager";
      const persist = isManager
        ? api.importCohort({
            cohort: { id: cohortId, name: cohortLabel, source: "converter", generator: format },
            tasks: [withCohort],
          })
        : api.create(withCohort);
      persist
        .then(() => {
          ackResult(withCohort.id, true);
          if (api.invalidateStats) api.invalidateStats();
          refreshStats();
        })
        .catch(err => {
          if (err && err.offline) {
            ackResult(withCohort.id, true); // local add only; don't alarm the user
            return;
          }
          ackResult(withCohort.id, false, (err && err.message) || String(err));
        });
    };

    const onSetRoute = (ev) => {
      const detail = ev && ev.detail;
      if (!detail || !detail.route) return;
      if (detail.activeId) setActiveId(detail.activeId);
      if (detail.route === "queue") {
        const pf = detail.prefilter || null;
        window.__caaQueuePrefilter = pf;
        try {
          window.dispatchEvent(new CustomEvent("caa-queue-prefilter", { detail: pf }));
        } catch (e) { /* ignore */ }
      }
      setRoute(detail.route);
    };

    window.addEventListener("caa-add-converted-task", onAddConverted);
    window.addEventListener("caa-set-route", onSetRoute);
    return () => {
      window.removeEventListener("caa-add-converted-task", onAddConverted);
      window.removeEventListener("caa-set-route", onSetRoute);
    };
  }, [user, refreshStats]);

  // keyboard shortcuts
  useEffect(()=>{
    const h = (e) => {
      if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
      if (e.key === 'e' || e.key === 'E') { setRoute('editor'); }
      if (e.key === 'r' || e.key === 'R') { setRoute('simulate'); }
      if (e.key === 'q' || e.key === 'Q') { setRoute('queue'); }
      if (e.key === '/') { e.preventDefault(); const i = document.querySelector('.search input'); if (i) i.focus(); }
    };
    window.addEventListener('keydown', h);
    return () => window.removeEventListener('keydown', h);
  }, []);

  const [showSettings, setShowSettings] = useState(false);
  const [cfgVersion, setCfgVersion] = useState(0);
  useEffect(() => {
    const bump = () => setCfgVersion(v => v + 1);
    window.addEventListener("caa-llm-cfg-changed", bump);
    return () => window.removeEventListener("caa-llm-cfg-changed", bump);
  }, []);
  const saveLlmConfig = (patch) => {
    setLlmConfig(patch);
    setShowSettings(false);
  };

  if (!authReady) return (<><Tweaks/></>);
  if (!user) return (<><Login/><Tweaks/></>);

  const active = tasks.find(t => t.id === activeId) || tasks[0];
  const openFromQueue = (where) => setRoute(where);
  const newTaskFromQueue = () => {
    const t = addTask({ author: user?.name });
    setRoute("editor");
    return t;
  };

  const queueProps = {
    tasks,
    activeId,
    setActive: setActiveId,
    openEditor: openFromQueue,
    onNewTask: newTaskFromQueue,
    taskStats,
    onTasksLoaded,
    user,
  };

  let body = null;
  if (route === "queue") body = <Queue {...queueProps}/>;
  else if (route === "editor") body = <Editor task={active} setRoute={setRoute} user={user} onPatch={(patch)=>updateTask(active.id, patch)}/>;
  else if (route === "simulate") body = <Simulator task={active} setRoute={setRoute}/>;
  else if (route === "review") body = <ReviewDash setRoute={setRoute} setActive={setActiveId}/>;
  else if (route === "cohorts") body = user.kind === "manager" ? <Cohorts user={user} onNewTask={newTaskFromQueue}/> : <Queue {...queueProps}/>;
  else if (route === "users") body = (user.kind === "manager" && typeof UsersPage === "function") ? <UsersPage user={user}/> : <Queue {...queueProps}/>;
  else if (route === "docs") body = (typeof Docs === "function") ? <Docs/> : <Queue {...queueProps}/>;
  else body = <Queue {...queueProps}/>;

  return (
    <>
      <AppShell
        key={cfgVersion}
        route={route} setRoute={setRoute}
        activeTask={(route==='editor'||route==='simulate') ? active : null}
        user={user} onLogout={onLogout}
        onOpenSettings={()=>setShowSettings(true)}
        taskStats={taskStats}
      >
        {body}
      </AppShell>
      <Tweaks/>
      {showSettings && (
        <SettingsModal
          cfg={getLlmConfig()}
          onSave={saveLlmConfig}
          onClose={()=>setShowSettings(false)}
        />
      )}
    </>
  );
}

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