import { useCallback, useEffect, useState } from "react";
import {
  api, exportProjectUrl, exportVideoUrl, fmtChainageRange, fmtDuration, fmtWhen, passDeltas,
  setAuthToken, statusLabel,
  type Activity, type Member, type ProjectSummary, type RoadType, type Stats, type UserRow, type VideoRow,
} from "./api";
import { RemarkCell } from "./RemarkCell";
import { AdminPanel } from "./AdminPanel";
import { MembersPicker } from "./MembersPicker";
import { FoldersPicker } from "./FoldersPicker";
import { SignIn } from "./SignIn";
import { MapView } from "./MapView";
import { ChainagePanel } from "./ChainagePanel";

const TOKEN_KEY = "central-token";
const POLL_MS = 10_000;
const IDLE_MS = 10 * 60_000; // auto-logout after 10 minutes of inactivity

type Auth = { username: string; role: string };

export function App() {
  // ---- auth gate ----
  const [token, setToken] = useState<string>(() => localStorage.getItem(TOKEN_KEY) ?? "");
  const [auth, setAuth] = useState<Auth | null>(null);
  const [authChecked, setAuthChecked] = useState(false);
  const [expiredReason, setExpiredReason] = useState<string | null>(null);
  const isAdmin = auth?.role === "admin";

  const signOut = useCallback((reason?: string) => {
    setAuth(null);
    setToken("");
    setAuthToken("");
    localStorage.removeItem(TOKEN_KEY);
    setExpiredReason(reason ?? null);
  }, []);

  const onSignedIn = useCallback((t: string, me: Auth) => {
    setAuthToken(t);
    localStorage.setItem(TOKEN_KEY, t);
    setToken(t);
    setAuth(me);
    setExpiredReason(null);
  }, []);

  // Restore a persisted session on load (silent whoami — no audit spam).
  useEffect(() => {
    let cancel = false;
    (async () => {
      if (token) {
        setAuthToken(token);
        try {
          const me = await api.whoami(token);
          if (!cancel) setAuth(me);
        } catch {
          if (!cancel) { setAuthToken(""); localStorage.removeItem(TOKEN_KEY); }
        }
      }
      if (!cancel) setAuthChecked(true);
    })();
    return () => { cancel = true; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Idle auto-logout: any activity resets a 10-minute timer.
  useEffect(() => {
    if (!auth) return;
    let timer = 0;
    const reset = () => {
      clearTimeout(timer);
      timer = window.setTimeout(
        () => signOut("Your session expired because of inactivity. Please sign in again."),
        IDLE_MS,
      );
    };
    const events = ["mousemove", "mousedown", "keydown", "scroll", "touchstart", "click"];
    events.forEach((e) => window.addEventListener(e, reset, { passive: true }));
    reset();
    return () => { clearTimeout(timer); events.forEach((e) => window.removeEventListener(e, reset)); };
  }, [auth, signOut]);

  // ---- dashboard data ----
  const [view, setView] = useState<"dashboard" | "admin">("dashboard");
  const [projects, setProjects] = useState<ProjectSummary[]>([]);
  const [projectId, setProjectId] = useState<number | null>(null);
  const [stats, setStats] = useState<Stats | null>(null);
  const [activity, setActivity] = useState<Activity | null>(null);
  const [videos, setVideos] = useState<VideoRow[]>([]);
  const [error, setError] = useState<string | null>(null);
  const [busy, setBusy] = useState(false);
  // Assignment (admin): selected video ids; the dropdown lists the project's members.
  const [selected, setSelected] = useState<Set<number>>(new Set());
  const [members, setMembers] = useState<Member[]>([]);
  const [assignee, setAssignee] = useState("");
  // Videos panel: table ↔ map toggle + road types (labels for assignment/calibration).
  const [videoView, setVideoView] = useState<"table" | "map">("table");
  const [roadTypes, setRoadTypes] = useState<RoadType[]>([]);
  const [rtAssign, setRtAssign] = useState("");
  // All ml_support users (admin only) — the pool the members picker chooses from.
  const [users, setUsers] = useState<UserRow[]>([]);
  const mlUsers = users.filter((u) => u.role === "ml_support");

  useEffect(() => {
    if (!isAdmin) { setUsers([]); return; }
    api.listUsers().then(setUsers).catch(() => setUsers([]));
  }, [isAdmin]);

  const toggleSel = (id: number) =>
    setSelected((s) => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });

  const loadProject = useCallback(async (id: number) => {
    try {
      const [s, v, a, m, rt] = await Promise.all([
        api.stats(id), api.videos(id), api.activity(id),
        api.members(id).catch(() => [] as Member[]),
        api.roadTypes(id).catch(() => [] as RoadType[]),
      ]);
      setStats(s);
      setVideos(v);
      setActivity(a);
      setMembers(m);
      setRoadTypes(rt);
      setError(null);
    } catch (e) {
      setError(String(e));
    }
  }, []);

  const loadProjects = useCallback(async () => {
    try {
      const p = await api.projects();
      setProjects(p);
      setProjectId((cur) => cur ?? (p[0]?.id ?? null));
      setError(null);
    } catch (e) {
      setError(String(e));
    }
  }, []);

  const assignSelected = useCallback(async () => {
    if (!isAdmin || selected.size === 0 || projectId == null) return;
    setBusy(true);
    try {
      await api.assign(projectId, [...selected], assignee);
      setSelected(new Set());
      await loadProject(projectId);
      setError(null);
    } catch (e) {
      setError(String(e));
    } finally {
      setBusy(false);
    }
  }, [isAdmin, selected, assignee, projectId, loadProject]);

  // Bulk-assign the picked road type to the selected videos (admin; "" clears).
  const applyRoadType = useCallback(async () => {
    if (!isAdmin || selected.size === 0 || projectId == null) return;
    setBusy(true);
    try {
      await api.assignRoadType(projectId, [...selected], rtAssign);
      setSelected(new Set());
      await loadProject(projectId);
      setError(null);
    } catch (e) {
      setError(String(e));
    } finally {
      setBusy(false);
    }
  }, [isAdmin, selected, rtAssign, projectId, loadProject]);

  // A deleted road-type label can leave a stale dropdown selection: the select
  // LOOKS like "(clear road type)" but still sends the dead label, which the server
  // rejects — making un-assign impossible. Snap back to '' whenever that happens.
  useEffect(() => {
    if (rtAssign !== "" && !roadTypes.some((r) => r.name === rtAssign)) setRtAssign("");
  }, [roadTypes, rtAssign]);

  useEffect(() => { if (auth) void loadProjects(); }, [auth, loadProjects]);

  useEffect(() => {
    if (!auth || projectId == null) return;
    setSelected(new Set()); // clear selection when switching projects
    void loadProject(projectId);
    const t = setInterval(() => { void loadProject(projectId); void loadProjects(); }, POLL_MS);
    return () => clearInterval(t);
  }, [auth, projectId, loadProject, loadProjects]);

  const timer = useCallback(async (action: "start" | "stop" | "reset") => {
    if (projectId == null) return;
    if (action === "reset" && !window.confirm("Reset the project timer back to zero?")) return;
    try {
      await api.setTimer(projectId, action);
      await loadProject(projectId);
    } catch (e) {
      setError(String(e));
    }
  }, [projectId, loadProject]);

  const toggleIgnore = useCallback(async (v: VideoRow) => {
    if (!isAdmin || projectId == null) return;
    try {
      await api.setIgnore(v.id, !v.ignored);
      await loadProject(projectId);
    } catch (e) {
      setError(String(e));
    }
  }, [isAdmin, projectId, loadProject]);

  const reingest = useCallback(async () => {
    if (projectId == null) return;
    setBusy(true);
    try {
      await api.ingest(projectId);
      await loadProject(projectId);
      await loadProjects();
    } catch (e) {
      setError(String(e));
    } finally {
      setBusy(false);
    }
  }, [projectId, loadProject, loadProjects]);

  if (!authChecked) {
    return <div className="signin-screen"><div className="dim">Loading…</div></div>;
  }
  if (!auth) {
    return <SignIn onSignedIn={onSignedIn} reason={expiredReason} />;
  }

  const project = projects.find((p) => p.id === projectId) ?? null;
  const ov = stats?.overview;
  const groups = groupByClient(projects);

  return (
    <div className="page">
      <header className="topbar">
        <div className="brand">Verification Dashboard</div>
        <div className="viewtabs">
          <button className={view === "dashboard" ? "active" : ""} onClick={() => setView("dashboard")}>Dashboard</button>
          <button className={view === "admin" ? "active" : ""} onClick={() => setView("admin")}>Admin</button>
        </div>
        {view === "dashboard" && (
          <>
            <select
              value={projectId ?? ""}
              onChange={(e) => setProjectId(Number(e.target.value))}
              disabled={projects.length === 0}
            >
              {groups.map(([client, ps]) => (
                <optgroup key={client} label={client}>
                  {ps.map((p) => (
                    <option key={p.id} value={p.id}>{p.name}</option>
                  ))}
                </optgroup>
              ))}
            </select>
            <button onClick={reingest} disabled={busy || projectId == null}>
              {busy ? "Ingesting…" : "Re-ingest"}
            </button>
          </>
        )}
        <span className="spacer" />
        {view === "dashboard" && project && (
          <span className="dim path" title={project.source_path}>
            {project.client_name ? `${project.client_name} · ` : ""}{project.source_path}
          </span>
        )}
        <span className="whoami" title={`role: ${auth.role}`}>{auth.username} · {auth.role}</span>
        <button className="signout-btn" onClick={() => signOut()}>Sign out</button>
      </header>

      {error && <div className="error">⚠ {error}</div>}

      {view === "admin" && <AdminPanel token={token} role={auth.role} onChanged={loadProjects} />}

      {view === "dashboard" && (
      <>
      {/* Overview */}
      {ov && (
        <section className="cards">
          <Card label="Total videos" value={ov.total} />
          <Card label="Pending" value={ov.pending} tone="warn" />
          <Card label="Annotated" value={ov.annotated} tone="info" />
          <Card label="Verified" value={ov.verified} tone="ok" />
          <div className="card progress-card">
            <div className="card-label">Verified progress</div>
            <div className="bar">
              <div className="bar-fill" style={{ width: `${ov.pct_done}%` }} />
            </div>
            <div className="card-value sm">{ov.pct_done.toFixed(1)}%
              {stats?.timeline.eta_ms != null && (
                <span className="dim"> · ETA ~{fmtDuration(stats.timeline.eta_ms)}</span>
              )}
            </div>
          </div>
        </section>
      )}

      {/* Project time & estimate (driven by the assigned users) */}
      {stats && ov && (
        <section className="panel time-panel">
          <div className="panel-head">
            <h3>Project time &amp; estimate</h3>
            {isAdmin && projectId != null && (
              <div className="timer-controls">
                <span className={`timer-state ${stats.timeline.time_running ? "on" : "off"}`}>
                  {stats.timeline.time_running ? "● running" : "⏸ stopped"}
                </span>
                {stats.timeline.time_running
                  ? <button className="btn" onClick={() => timer("stop")}>Stop</button>
                  : <button className="btn" onClick={() => timer("start")}>Start</button>}
                <button className="btn danger" onClick={() => timer("reset")}>Reset</button>
              </div>
            )}
          </div>
          <div className="time-grid">
            <div className="tcell big">
              <span className="dim">Total project time</span>
              <b>{fmtDuration(stats.timeline.elapsed_ms)} {stats.timeline.all_verified ? "✓" : ""}</b>
              <span className="dim sub">inception → {stats.timeline.all_verified ? "all verified" : "now (ongoing)"}</span>
            </div>
            <div className="tcell">
              <span className="dim">Total hours — all users</span>
              <b>{fmtDuration(stats.timeline.total_spent_ms)}</b>
              <span className="dim sub">annotation + verify, summed</span>
            </div>
            <div className="tcell">
              <span className="dim">Assigned users</span>
              <b>{stats.timeline.active_workers}</b>
              <span className="dim sub">{stats.timeline.active_workers === 0 ? "assign users to estimate" : "drives the estimate"}</span>
            </div>
            <div className="tcell">
              <span className="dim">Avg per video</span>
              <b>{fmtDuration(stats.timeline.avg_video_ms)}</b>
              <span className="dim sub">over verified videos</span>
            </div>
            <div className="tcell big">
              <span className="dim">Estimated time to finish</span>
              <b>{stats.timeline.eta_ms != null ? fmtDuration(stats.timeline.eta_ms) : "—"}</b>
              <span className="dim sub">
                {ov.total - ov.verified} left ÷ {Math.max(1, stats.timeline.active_workers)} user(s) @ {fmtDuration(stats.timeline.avg_video_ms)}/video
              </span>
            </div>
          </div>
        </section>
      )}

      <div className="grid2">
        {/* Leaderboard + project members picker */}
        <section className="panel">
          <div className="panel-head">
            <h3>Per-user leaderboard</h3>
            {isAdmin && projectId != null && (
              <MembersPicker
                projectId={projectId}
                users={mlUsers}
                members={members}
                onChange={() => loadProject(projectId)}
              />
            )}
          </div>
          {isAdmin && members.length === 0 && (
            <div className="members-cta">No users assigned to this project yet — use <b>+ Assign users</b> to pick ml_support, then assign them videos.</div>
          )}
          <table>
            <thead>
              <tr><th>User</th><th>Videos</th><th>Annotations</th><th>Total time</th><th>Avg / video</th></tr>
            </thead>
            <tbody>
              {stats?.leaderboard.map((r) => (
                <tr key={r.user}>
                  <td>{r.user}</td>
                  <td>{r.videos}</td>
                  <td>{r.annotations}</td>
                  <td>{fmtDuration(r.total_time_ms)}</td>
                  <td>{fmtDuration(r.avg_time_ms)}</td>
                </tr>
              ))}
              {(!stats || stats.leaderboard.length === 0) && (
                <tr><td colSpan={5} className="dim">no annotators yet</td></tr>
              )}
            </tbody>
          </table>
        </section>

        {/* Verification pace (rate-based, replaces the old per-day bars) */}
        <section className="panel">
          <h3>Verification pace</h3>
          {stats && ov ? (() => {
            const t = stats.timeline;
            const perDay = t.recent_window_days > 0 ? t.recent_verified / t.recent_window_days : 0;
            const remaining = ov.total - ov.verified;
            const paceDays = perDay > 0 ? Math.ceil(remaining / perDay) : null;
            return (
              <div className="pace-grid">
                <div className="pcell">
                  <span className="dim">Verified / day</span>
                  <b>{perDay.toFixed(1)}</b>
                  <span className="dim sub">last {t.recent_window_days} days ({t.recent_verified} done)</span>
                </div>
                <div className="pcell">
                  <span className="dim">Avg per video</span>
                  <b>{fmtDuration(t.avg_video_ms)}</b>
                  <span className="dim sub">over verified videos</span>
                </div>
                <div className="pcell big">
                  <span className="dim">Finish at recent pace</span>
                  <b>{paceDays != null ? `~${paceDays} day${paceDays === 1 ? "" : "s"}` : "—"}</b>
                  <span className="dim sub">{perDay > 0 ? `${remaining} left ÷ ${perDay.toFixed(1)}/day (all users' actual output)` : "no recent activity"}</span>
                </div>
                <div className="pcell big">
                  <span className="dim">Est. to finish · {Math.max(1, t.active_workers)} user{t.active_workers === 1 ? "" : "s"}</span>
                  <b>{t.eta_ms != null ? fmtDuration(t.eta_ms) : "—"}</b>
                  <span className="dim sub">{remaining} left × {fmtDuration(t.avg_video_ms)} ÷ {Math.max(1, t.active_workers)} user{t.active_workers === 1 ? "" : "s"}</span>
                </div>
              </div>
            );
          })() : <div className="dim">no data yet</div>}
        </section>
      </div>

      <div className="grid2">
        {/* Verification leaderboard — assigned users ranked by annotation throughput
            (most annotations in the least time), not by video count. */}
        <section className="panel">
          <h3>Verification leaderboard <span className="dim">· most annotations / min</span></h3>
          <table>
            <thead>
              <tr><th>Rank</th><th>User</th><th>Annotations</th><th>Anno/min</th><th>Verified</th><th>Total time</th></tr>
            </thead>
            <tbody>
              {stats?.verifiers.map((r) => (
                <tr key={r.user} className={r.rank === 1 ? "rank-top" : undefined}>
                  <td>{r.rank > 0 ? (r.rank === 1 ? "🥇 1" : `#${r.rank}`) : <span className="dim">—</span>}</td>
                  <td>{r.user}</td>
                  <td>{r.annotations > 0 ? r.annotations : <span className="dim">0</span>}</td>
                  <td>{r.annotations_per_min > 0 ? r.annotations_per_min.toFixed(1) : <span className="dim">—</span>}</td>
                  <td>{r.videos > 0 ? r.videos : <span className="dim">0</span>}</td>
                  <td>{r.total_time_ms > 0 ? fmtDuration(r.total_time_ms) : <span className="dim">—</span>}</td>
                </tr>
              ))}
              {(!stats || stats.verifiers.length === 0) && (
                <tr><td colSpan={6} className="dim">no assigned users yet</td></tr>
              )}
            </tbody>
          </table>
        </section>

        {/* Recent activity (live claims + event feed) */}
        <section className="panel">
          <h3>Recent activity</h3>
          {activity && activity.active_claims.length > 0 && (
            <div className="active-claims">
              {activity.active_claims.map((c) => (
                <div className="active-claim" key={c.video_id} title={`lease expires ${fmtWhen(c.lease_expires_at)}`}>
                  🔒 <b>{c.claimed_by}</b> has <span className="mono">{c.file_name}</span> <span className="dim">· since {fmtWhen(c.claimed_at)}</span>
                </div>
              ))}
            </div>
          )}
          <div className="activity-feed">
            {activity?.recent_events.map((e, i) => (
              <div className="activity-row" key={i}>
                <span className={`evt evt-${e.event}`}>{e.event}</span>
                <span className="mono">{e.file_name}</span>
                <span className="dim">{e.username} · {fmtWhen(e.at)}</span>
              </div>
            ))}
            {(!activity || activity.recent_events.length === 0) && <div className="dim">no activity yet</div>}
          </div>
        </section>
      </div>

      {/* Per-video table / map */}
      <section className="panel">
        <div className="panel-head">
          <h3>
            Videos ({videos.length})
            <span className="viewtabs videos-toggle">
              <button className={videoView === "table" ? "active" : ""} onClick={() => setVideoView("table")}>Table</button>
              <button className={videoView === "map" ? "active" : ""} onClick={() => setVideoView("map")}>Map</button>
            </span>
          </h3>
          <div className="panel-actions">
            {isAdmin && projectId != null && (
              <FoldersPicker projectId={projectId} onChange={() => loadProject(projectId)} />
            )}
            {isAdmin && videoView === "table" && (
              <span className="assign-bar">
                <span className="dim">{selected.size} selected →</span>
                <select value={assignee} onChange={(e) => setAssignee(e.target.value)} title="Assign to a project member (or unassign)">
                  <option value="">(unassign)</option>
                  {members.map((m) => <option key={m.username} value={m.username}>{m.username}</option>)}
                </select>
                <button className="btn" disabled={busy || selected.size === 0 || (assignee !== "" && members.length === 0)} onClick={assignSelected}>Assign</button>
                <select value={rtAssign} onChange={(e) => setRtAssign(e.target.value)} title="Set the road type of the selected videos (or clear it)">
                  <option value="">(clear road type)</option>
                  {roadTypes.map((r) => <option key={r.name} value={r.name}>{r.name}</option>)}
                </select>
                <button className="btn" disabled={busy || selected.size === 0} onClick={applyRoadType}
                  title={rtAssign ? "Apply the picked road type to the selected videos" : "Remove the road type from the selected videos"}>
                  {rtAssign ? "Set road" : "Clear road"}
                </button>
              </span>
            )}
            {projectId != null && videos.some((v) => v.annotation_count > 0) && (
              <a className="btn" href={exportProjectUrl(projectId)} download title="Download all annotations in this project as one JSON">
                ⬇ Export all
              </a>
            )}
          </div>
        </div>
        {videoView === "map" && projectId != null && (
          <MapView projectId={projectId} isAdmin={isAdmin} onChanged={() => loadProject(projectId)} />
        )}
        {videoView === "table" && (
        <div className="table-scroll">
        <table>
          <thead>
            <tr>
              {isAdmin && <th><input type="checkbox" title="select all"
                checked={selected.size > 0 && selected.size === videos.length}
                onChange={(e) => setSelected(e.target.checked ? new Set(videos.map((v) => v.id)) : new Set())} /></th>}
              <th>#</th><th>Folder</th><th>File</th><th>Status</th><th>Road · Chainage</th><th>Annotator</th><th>Annotations</th>
              <th>Review</th><th>Verify time</th><th>Resolution</th><th>Annotated</th><th>Remarks</th>
            </tr>
          </thead>
          <tbody>
            {videos.map((v, idx) => (
              <tr key={v.id} className={`${selected.has(v.id) ? "row-sel " : ""}${v.ignored ? "row-ignored" : ""}`}>
                {isAdmin && <td><input type="checkbox" checked={selected.has(v.id)} onChange={() => toggleSel(v.id)} /></td>}
                <td className="dim">{idx + 1}</td>
                <td className="dim mono" title={v.rel_path}>{subfolder(v.rel_path) || "—"}</td>
                <td className="mono">{v.file_name}</td>
                <td>
                  <span className={`badge ${v.workflow_status}`}>{statusLabel(v.workflow_status)}</span>
                  {v.ignored && <span className="badge ignored-tag" title={v.ignored_by ? `ignored by ${v.ignored_by}` : "ignored"}>ignored</span>}
                  {isAdmin && (
                    <button className="ignore-btn" onClick={() => toggleIgnore(v)}
                      title={v.ignored ? "Restore this video" : "Ignore this video (cross it out)"}>
                      {v.ignored ? "undo ignore" : "ignore"}
                    </button>
                  )}
                  {v.claimed_by ? (
                    <div className="claim-line" title={v.lease_expires_at ? `lease expires ${fmtWhen(v.lease_expires_at)}` : undefined}>
                      🔒 claimed by <b>{v.claimed_by}</b>{v.claimed_at ? ` · ${fmtWhen(v.claimed_at)}` : ""}
                    </div>
                  ) : v.workflow_status === "assigned" && v.assigned_to ? (
                    <div className="claim-line">→ assigned to <b>{v.assigned_to}</b></div>
                  ) : v.completed_at ? (
                    <div className="claim-line" title={`last push ${fmtWhen(v.completed_at)}`}>✓ <b>{v.verifiers || v.completed_by}</b> · {fmtWhen(v.completed_at)}</div>
                  ) : null}
                </td>
                <td>
                  {v.road_type ? <span className="badge road-tag">{v.road_type}</span> : <span className="dim">—</span>}
                  <div className="claim-line" title={
                    v.sr_chainage_start_m != null
                      ? `sr (GPS-measured): ${fmtChainageRange(v.sr_chainage_start_m, v.sr_chainage_end_m)}`
                      : v.gps_status === "ok" ? "GPS scanned — chainage not calibrated yet"
                      : v.gps_status === "error" ? `GPS scan failed: ${v.gps_error ?? ""}`
                      : v.gps_status === "none" ? "no GPS metadata in this video"
                      : "GPS scan pending"
                  }>
                    {v.chainage_start_m != null
                      ? fmtChainageRange(v.chainage_start_m, v.chainage_end_m)
                      : <span className="dim">{v.gps_status === "ok" ? "gps ✓" : v.gps_status === "pending" ? "gps …" : `gps ${v.gps_status}`}</span>}
                  </div>
                  <GpsPoints v={v} />
                </td>
                <td>{v.verifiers || v.primary_annotator || (v.assigned_to ? <span className="dim">assigned: {v.assigned_to}</span> : "—")}</td>
                <td>
                  {v.annotation_count > 0
                    ? <a href={exportVideoUrl(v.id)} download title="Download this video's annotations (JSON)">{v.annotation_count} ⬇</a>
                    : 0}
                  {(v.imported_count > 0 || (v.passes?.length ?? 0) > 0) && (() => {
                    const deltas = passDeltas(v.imported_count, v.passes ?? []);
                    return (
                      <div className="imported-line" title="imported baseline → per-pass changes (verify / re-verify)">
                        <span className="dim">imported {v.imported_count}</span>
                        {deltas.length > 0
                          ? deltas.map((d, i) => (
                              <span key={i} className={d.delta >= 0 ? "ann-add" : "ann-del"}
                                title={`${d.label} by ${d.username} → ${d.total} total`}>
                                {" · "}{d.label} {d.delta >= 0 ? "+" : ""}{d.delta}
                              </span>
                            ))
                          : <>
                              {v.annotation_count < v.imported_count &&
                                <span className="ann-del" title={`${v.imported_count - v.annotation_count} deleted by the user`}> · −{v.imported_count - v.annotation_count}</span>}
                              {v.annotation_count > v.imported_count &&
                                <span className="ann-add" title={`${v.annotation_count - v.imported_count} added by the user`}> · +{v.annotation_count - v.imported_count}</span>}
                            </>}
                      </div>
                    );
                  })()}
                </td>
                <td>{v.review_count > 0
                  ? <span className="review-flag" title={`${v.review_count} annotation(s) flagged for review`}>🚩 {v.review_count}</span>
                  : <span className="dim">—</span>}</td>
                <td title="total across all passes">{fmtDuration(v.verify_time_ms)}</td>
                <td className="dim">{v.width && v.height ? `${v.width}×${v.height}` : "—"}</td>
                <td className="dim">{v.annotated_at ? v.annotated_at.slice(0, 10) : "—"}</td>
                <td className="remark-col">
                  <RemarkCell
                    videoId={v.id}
                    initial={v.remarks}
                    handRaised={v.hand_raised}
                    handBy={v.hand_raised_by}
                  />
                </td>
              </tr>
            ))}
            {videos.length === 0 && <tr><td colSpan={isAdmin ? 13 : 12} className="dim">no videos ingested</td></tr>}
          </tbody>
        </table>
        </div>
        )}
      </section>

      {/* Chainage calibration + road-type labels (admin) */}
      {isAdmin && projectId != null && (
        <ChainagePanel
          projectId={projectId}
          videos={videos}
          roadTypes={roadTypes}
          onChanged={() => loadProject(projectId)}
        />
      )}
      </>
      )}
    </div>
  );
}

/** The subfolder portion of a rel_path (everything before the last '/'), or "" at the root. */
function subfolder(rel: string): string {
  const i = rel.lastIndexOf("/");
  return i >= 0 ? rel.slice(0, i) : "";
}

/** Group projects under their client name, preserving the server's ordering. */
function groupByClient(projects: ProjectSummary[]): [string, ProjectSummary[]][] {
  const order: string[] = [];
  const map = new Map<string, ProjectSummary[]>();
  for (const p of projects) {
    const key = p.client_name || "—";
    if (!map.has(key)) { map.set(key, []); order.push(key); }
    map.get(key)!.push(p);
  }
  return order.map((k) => [k, map.get(k)!]);
}

/** GPS fix count vs the ~1 fix/s expected for the video's duration. A mismatch gets
 *  a ⚠ that expands (on click) into a plain-language explanation of how the missing/
 *  extra data is handled — interpolation, start-stitching, end clamping. */
function GpsPoints({ v }: { v: VideoRow }) {
  const [open, setOpen] = useState(false);
  if (v.gps_status !== "ok" || v.gps_sample_count == null) return null;
  const durS = v.duration_s ?? (v.frame_count && v.fps ? v.frame_count / v.fps : null);
  const expected = durS ? Math.round(durS) : null;
  const diff = expected != null ? v.gps_sample_count - expected : 0;
  const off = expected != null && Math.abs(diff) > Math.max(3, expected * 0.05);
  const bigGap = (v.gps_max_gap_s ?? 0) > 5;
  return (
    <div className="claim-line gps-pts">
      {v.gps_sample_count} gps pts
      {expected != null && <span className="dim"> / ~{expected}</span>}
      {(off || bigGap) && (
        <button
          className="gps-warn"
          onClick={() => setOpen((o) => !o)}
          title="Fix count doesn't match the video duration — click to see how the gaps are handled"
        >
          ⚠
        </button>
      )}
      {open && (off || bigGap) && (
        <div className="gps-explain" onClick={() => setOpen(false)}>
          {diff < 0 || (!off && bigGap) ? (
            <>
              Expected ~{expected ?? "?"} fixes (1 per second of video), found {v.gps_sample_count}
              {bigGap && <> — largest dropout <b>{v.gps_max_gap_s!.toFixed(0)} s</b></>}.
              Positions inside a dropout are <b>time-interpolated</b>: each frame is placed
              proportionally on a straight line between the two nearest real fixes (constant
              speed assumed), and its chainage follows the same interpolation. A missing{" "}
              <b>start</b> is stitched from the previous video via GPS timestamps when possible;
              a missing <b>end</b> clamps to the last fix. The longer the dropout, the rougher
              the approximation.
            </>
          ) : diff > 0 ? (
            <>
              Found {v.gps_sample_count} fixes but only ~{expected} expected — the camera wrote
              more than one fix per second (or duplicated samples). Extra fixes are used as-is;
              nothing is approximated.
            </>
          ) : null}
        </div>
      )}
    </div>
  );
}

function Card({ label, value, tone }: { label: string; value: number; tone?: string }) {
  return (
    <div className={`card ${tone ?? ""}`}>
      <div className="card-label">{label}</div>
      <div className="card-value">{value}</div>
    </div>
  );
}
