import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { unzipSync } from "fflate";
import {
  api, fmtChainage, fmtChainageRange, fmtWhen,
  type CalibrationRow, type CalibrationSummary, type ChainagePoint, type RoadType, type VideoRow,
} from "./api";

/** The import format clients should follow: one Point placemark per survey point,
 *  chainage in the name ("12+400" = 12 km + 400 m, or decimal km). */
const SAMPLE_KML = `<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2"><Document>
  <name>chainage points — sample</name>
  <!-- One Placemark per survey point.
       name        = the chainage at that point: "12+400" (12 km + 400 m) or "12.4" (km)
       coordinates = lon,lat[,alt]  (KML order: longitude first!)
       The dashboard reads the name (or description) and the Point coordinates.
       LineStrings/paths are ignored — only Point placemarks import. -->
  <Placemark><name>0+000</name><Point><coordinates>78.185718,21.105570,0</coordinates></Point></Placemark>
  <Placemark><name>5+000</name><Point><coordinates>78.158000,21.092045,0</coordinates></Point></Placemark>
  <Placemark><name>10+000</name><Point><coordinates>78.123885,21.091447,0</coordinates></Point></Placemark>
</Document></kml>
`;

function downloadSampleKml() {
  const url = URL.createObjectURL(new Blob([SAMPLE_KML], { type: "application/vnd.google-earth.kml+xml" }));
  const a = document.createElement("a");
  a.href = url;
  a.download = "chainage_points_sample.kml";
  a.click();
  URL.revokeObjectURL(url);
}

/** One placemark parsed out of an uploaded KML, editable before import. */
interface KmlRow {
  name: string;
  lat: number;
  lon: number;
  /** Editable chainage in km (prefilled from the placemark name/description). */
  km: string;
  include: boolean;
}

/** Chainage from free text: road notation "12+400" (km+m) always wins; otherwise the
 *  first number, interpreted in `unit`. Returns meters, or null when nothing parses. */
function parseChainageText(text: string, unit: "km" | "m"): number | null {
  const plus = text.match(/(\d+)\s*\+\s*(\d+(?:\.\d+)?)/);
  if (plus) return parseInt(plus[1], 10) * 1000 + parseFloat(plus[2]);
  const num = text.match(/-?\d+(?:[.,]\d+)?/);
  if (!num) return null;
  const v = parseFloat(num[0].replace(",", "."));
  if (isNaN(v)) return null;
  return unit === "km" ? v * 1000 : v;
}

/** Extract Point placemarks (name + lat/lon) from a KML document. LineStrings and
 *  multi-coordinate placemarks are skipped (counted) — only survey points import. */
function parseKmlPoints(text: string): { rows: { name: string; lat: number; lon: number }[]; skipped: number } {
  const doc = new DOMParser().parseFromString(text, "application/xml");
  if (doc.getElementsByTagName("parsererror").length > 0) {
    throw new Error("not a valid KML/XML file (KMZ must be unzipped first)");
  }
  const rows: { name: string; lat: number; lon: number }[] = [];
  let skipped = 0;
  for (const pm of Array.from(doc.getElementsByTagNameNS("*", "Placemark"))) {
    const coords = pm.getElementsByTagNameNS("*", "coordinates")[0]?.textContent?.trim() ?? "";
    const tuples = coords.split(/\s+/).filter(Boolean);
    if (tuples.length !== 1) {
      skipped += 1; // a LineString/Polygon (or empty) — not a survey point
      continue;
    }
    const [lon, lat] = tuples[0].split(",").map(parseFloat);
    if (isNaN(lat) || isNaN(lon)) {
      skipped += 1;
      continue;
    }
    const name = pm.getElementsByTagNameNS("*", "name")[0]?.textContent?.trim()
      || pm.getElementsByTagNameNS("*", "description")[0]?.textContent?.trim()
      || "";
    rows.push({ name, lat, lon });
  }
  return { rows, skipped };
}

/**
 * Admin chainage panel: manage road-type labels, enter the client's calibration
 * points (lat/lon ↔ chainage), run the two-point quick assign ("first video starts
 * at X, last ends at Y") or a full recompute over the current group, and read the
 * resulting per-span scales + per-video chainage. `sr` (GPS-measured) chainage is
 * shown but never editable — only the client-calibrated values come from points.
 */
export function ChainagePanel({
  projectId,
  videos,
  roadTypes,
  onChanged,
}: {
  projectId: number;
  videos: VideoRow[];
  roadTypes: RoadType[];
  onChanged: () => void;
}) {
  const [open, setOpen] = useState(false);
  const [group, setGroup] = useState(""); // road_type calibration group ('' = all videos)
  const [points, setPoints] = useState<ChainagePoint[]>([]);
  const [calibs, setCalibs] = useState<CalibrationRow[]>([]);
  const [calibsErr, setCalibsErr] = useState<string | null>(null);
  const [summary, setSummary] = useState<CalibrationSummary | null>(null);
  // true = loaded from the server (someone's earlier compute); false = just run here.
  const [summaryIsSaved, setSummaryIsSaved] = useState(false);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);
  // add-point form
  const [latlon, setLatlon] = useState("");
  const [pointKm, setPointKm] = useState("");
  const [pointNote, setPointNote] = useState("");
  // inline note edit on an existing point
  const [noteEdit, setNoteEdit] = useState<{ id: number; text: string } | null>(null);
  // quick two-point form
  const [startKm, setStartKm] = useState("0");
  const [endKm, setEndKm] = useState("");
  // road-type editor
  const [newType, setNewType] = useState("");
  // KML import preview
  const [kmlRows, setKmlRows] = useState<KmlRow[] | null>(null);
  const [kmlUnit, setKmlUnit] = useState<"km" | "m">("km");
  const [kmlSkipped, setKmlSkipped] = useState(0);
  const kmlFileRef = useRef<HTMLInputElement>(null);

  const loadPoints = useCallback(async () => {
    try {
      setPoints(await api.chainagePoints(projectId));
    } catch (e) {
      setError(String(e));
    }
    // Calibration list failing must not hide the points editor — but it must not
    // fail silently either (a broken endpoint/auth issue should be visible).
    try {
      setCalibs(await api.chainageCalibrations(projectId));
      setCalibsErr(null);
    } catch (e) {
      setCalibs([]);
      setCalibsErr(String(e));
    }
  }, [projectId]);

  useEffect(() => {
    if (open) void loadPoints();
  }, [open, loadPoints]);

  // On open / group change, show the SAVED result of the last recompute (persisted
  // server-side) so a freshly signed-in admin sees the existing compute for LHS/RHS
  // instead of an empty panel asking to recompute.
  useEffect(() => {
    setSummary(null);
    setSummaryIsSaved(false);
    if (!open) return;
    let alive = true;
    api.chainageSummary(projectId, group)
      .then((s) => {
        if (alive && s && typeof s === "object") {
          setSummary(s);
          setSummaryIsSaved(true);
        }
      })
      .catch(() => {}); // calibrations block surfaces endpoint failures already
    return () => {
      alive = false;
    };
  }, [projectId, group, open]);

  /** The route's videos = current group, ignored ones excluded. */
  const groupVideos = useMemo(
    () => videos.filter((v) => !v.ignored && (group === "" || v.road_type === group)),
    [videos, group],
  );
  const groupPoints = useMemo(
    () => points.filter((p) => p.road_type === group || p.road_type === ""),
    [points, group],
  );

  // Suggest quick-assign values from the group's own videos: an existing
  // calibration's range when one exists, else 0 → the summed GPS length rounded to
  // the nearest whole km. Re-suggested when the group changes (still editable).
  useEffect(() => {
    if (!open) return;
    const ch = groupVideos
      .flatMap((v) => [v.chainage_start_m, v.chainage_end_m])
      .filter((x): x is number => x != null);
    if (ch.length > 0) {
      setStartKm((Math.min(...ch) / 1000).toFixed(4));
      setEndKm((Math.max(...ch) / 1000).toFixed(4));
    } else {
      const len = groupVideos.reduce((s, v) => s + (v.gps_len_m ?? 0), 0);
      setStartKm("0");
      setEndKm(len > 0 ? String(Math.max(1, Math.round(len / 1000))) : "");
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [open, group]);

  const run = async (fn: () => Promise<CalibrationSummary>) => {
    setBusy(true);
    setError(null);
    try {
      setSummary(await fn());
      setSummaryIsSaved(false);
      await loadPoints();
      onChanged();
    } catch (e) {
      setError(String(e));
    } finally {
      setBusy(false);
    }
  };

  const addPoint = async () => {
    const m = latlon.trim().match(/^\s*(-?\d+(?:\.\d+)?)\s*[,;\s]\s*(-?\d+(?:\.\d+)?)\s*$/);
    const km = parseFloat(pointKm);
    if (!m || isNaN(km)) {
      setError('Point needs "lat, lon" and a chainage in km.');
      return;
    }
    setBusy(true);
    setError(null);
    try {
      await api.addChainagePoint(projectId, {
        lat: parseFloat(m[1]),
        lon: parseFloat(m[2]),
        chainage_m: km * 1000,
        note: pointNote.trim(),
        road_type: group,
      });
      setLatlon("");
      setPointKm("");
      setPointNote("");
      await loadPoints();
    } catch (e) {
      setError(String(e));
    } finally {
      setBusy(false);
    }
  };

  const deletePoint = async (id: number) => {
    setBusy(true);
    try {
      await api.deleteChainagePoint(id);
      await loadPoints();
    } catch (e) {
      setError(String(e));
    } finally {
      setBusy(false);
    }
  };

  /** Persist an inline note edit (Enter or blur). */
  const saveNote = async () => {
    if (!noteEdit) return;
    const { id, text } = noteEdit;
    setNoteEdit(null);
    try {
      await api.updateChainagePointNote(id, text);
      await loadPoints();
    } catch (e) {
      setError(String(e));
    }
  };

  const quick = () => {
    const s = parseFloat(startKm);
    const e = parseFloat(endKm);
    if (isNaN(s) || isNaN(e)) {
      setError("Quick assign needs start and end chainage in km.");
      return;
    }
    void run(() =>
      api.chainageQuick(projectId, {
        video_ids: groupVideos.map((v) => v.id),
        start_m: s * 1000,
        end_m: e * 1000,
        road_type: group,
      }),
    );
  };

  const recompute = () =>
    run(() => api.chainageRecompute(projectId, groupVideos.map((v) => v.id), group));

  const addRoadType = async () => {
    const name = newType.trim();
    if (!name) return;
    setBusy(true);
    setError(null);
    try {
      await api.setRoadTypes(projectId, [...roadTypes.map((r) => r.name), name]);
      setNewType("");
      onChanged();
    } catch (e) {
      setError(String(e));
    } finally {
      setBusy(false);
    }
  };

  const onKmlFile = async (file: File) => {
    setError(null);
    try {
      let text: string;
      if (/\.kmz$/i.test(file.name)) {
        // KMZ = zipped KML; take the first .kml entry (usually doc.kml).
        const entries = unzipSync(new Uint8Array(await file.arrayBuffer()));
        const key = Object.keys(entries).find((k) => k.toLowerCase() === "doc.kml")
          ?? Object.keys(entries).find((k) => /\.kml$/i.test(k));
        if (!key) {
          setError(`${file.name} contains no .kml document.`);
          return;
        }
        text = new TextDecoder().decode(entries[key]);
      } else {
        text = await file.text();
      }
      const { rows, skipped } = parseKmlPoints(text);
      if (rows.length === 0) {
        setError(`No point placemarks found in ${file.name}${skipped ? ` (${skipped} non-point entries skipped)` : ""}.`);
        return;
      }
      setKmlSkipped(skipped);
      setKmlRows(rows.map((r) => {
        const m = parseChainageText(r.name, kmlUnit);
        return { ...r, km: m != null ? (m / 1000).toFixed(4) : "", include: m != null };
      }));
    } catch (e) {
      setError(String(e));
    } finally {
      if (kmlFileRef.current) kmlFileRef.current.value = ""; // allow re-picking the same file
    }
  };

  /** Re-read every row's chainage from its placemark name under the new unit. */
  const setKmlUnitAndReparse = (u: "km" | "m") => {
    setKmlUnit(u);
    setKmlRows((rows) => rows?.map((r) => {
      const m = parseChainageText(r.name, u);
      return { ...r, km: m != null ? (m / 1000).toFixed(4) : "", include: m != null };
    }) ?? null);
  };

  const importKml = async () => {
    if (!kmlRows) return;
    const points = kmlRows
      .filter((r) => r.include && r.km.trim() !== "" && !isNaN(parseFloat(r.km)))
      .map((r) => ({
        lat: r.lat,
        lon: r.lon,
        chainage_m: parseFloat(r.km) * 1000,
        note: r.name,
        road_type: group,
      }));
    if (points.length === 0) {
      setError("No rows selected with a valid chainage.");
      return;
    }
    setBusy(true);
    setError(null);
    try {
      await api.addChainagePointsBulk(projectId, points);
      setKmlRows(null);
      await loadPoints();
      onChanged();
    } catch (e) {
      setError(String(e));
    } finally {
      setBusy(false);
    }
  };

  const removeRoadType = async (name: string) => {
    setBusy(true);
    setError(null);
    try {
      await api.setRoadTypes(projectId, roadTypes.map((r) => r.name).filter((n) => n !== name));
      if (group === name) setGroup("");
      onChanged();
    } catch (e) {
      setError(String(e));
    } finally {
      setBusy(false);
    }
  };

  if (!open) {
    return (
      <section className="panel">
        <div className="panel-head">
          <h3>Chainage &amp; road types</h3>
          <button className="btn" onClick={() => setOpen(true)}>Open ▾</button>
        </div>
      </section>
    );
  }

  return (
    <section className="panel">
      <div className="panel-head">
        <h3>Chainage &amp; road types</h3>
        <button className="btn" onClick={() => setOpen(false)}>Close ▴</button>
      </div>

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

      {/* Road-type labels (project-wide) */}
      <div className="chainage-block">
        <div className="dim block-label">Road types (assign them to selected videos in the table below)</div>
        <div className="member-chips">
          {roadTypes.map((r) => (
            <span key={r.name} className="member-chip">
              {r.name} <span className="dim">({r.video_count})</span>
              <button className="chip-x" disabled={busy} onClick={() => removeRoadType(r.name)} title="Remove label">×</button>
            </span>
          ))}
          <input
            className="chip-input"
            value={newType}
            onChange={(e) => setNewType(e.target.value)}
            onKeyDown={(e) => { if (e.key === "Enter") void addRoadType(); }}
            placeholder="Add: LHS / RHS / MCW…"
          />
          <button className="btn" disabled={busy || !newType.trim()} onClick={addRoadType}>Add</button>
        </div>
      </div>

      {/* Calibration group + the videos it covers */}
      <div className="chainage-block form-row">
        <label className="dim">Calibration group</label>
        <select value={group} onChange={(e) => setGroup(e.target.value)}>
          <option value="">(all videos)</option>
          {roadTypes.map((r) => (
            <option key={r.name} value={r.name}>{r.name}</option>
          ))}
        </select>
        <span className="dim">
          {groupVideos.length} video(s) in the route · {groupPoints.length} calibration point(s)
        </span>
      </div>

      {/* Saved calibrations — persisted server-side, so every user (dashboard AND
          desktop exports) sees the same computed chainage without re-running anything. */}
      {(calibs.length > 0 || calibsErr) && (
        <div className="chainage-block">
          <div className="dim block-label">Saved calibrations (shared — all users see these chainages)</div>
          {calibsErr && <div className="err-tag" style={{ fontSize: 12 }}>⚠ calibrations unavailable: {calibsErr}</div>}
          {calibs.map((c) => (
            <div key={c.road_type || "(any)"} className="dim" style={{ fontSize: 12 }}>
              <b>{c.road_type || "(ungrouped)"}</b> · route {fmtChainage(c.route_len_m)} km
              · {c.anchor_count} anchor(s) · computed by {c.computed_by} · {fmtWhen(c.computed_at)}
              {!c.has_summary && (
                <span className="err-tag" title="This calibration predates saved results — select its group and hit 'Recompute from points' once (same points ⇒ same values); after that every admin sees the full result here.">
                  {" "}· result not saved yet — recompute once
                </span>
              )}
            </div>
          ))}
        </div>
      )}

      <div className="grid2">
        {/* Control points */}
        <div className="chainage-block">
          <div className="dim block-label">Client calibration points (lat/lon ↔ chainage)</div>
          <table>
            <thead>
              <tr><th>Chainage</th><th>Lat, Lon</th><th>Group</th><th>Note</th><th></th></tr>
            </thead>
            <tbody>
              {groupPoints.map((p) => (
                <tr key={p.id}>
                  <td>
                    {fmtChainage(p.chainage_m)}
                    {p.origin === "quick" && (
                      <span className="dim" title="Generated by the two-point quick assign — replaced automatically on the next quick run"> · auto</span>
                    )}
                  </td>
                  <td className="mono">{p.lat.toFixed(6)}, {p.lon.toFixed(6)}</td>
                  <td className="dim">{p.road_type || "any"}</td>
                  <td
                    className="dim"
                    style={{ cursor: "pointer" }}
                    title="Click to edit — e.g. 'start point', 'toll plaza', a survey remark"
                    onClick={() => { if (noteEdit?.id !== p.id) setNoteEdit({ id: p.id, text: p.note }); }}
                  >
                    {noteEdit?.id === p.id ? (
                      <input
                        autoFocus
                        value={noteEdit.text}
                        style={{ width: 140 }}
                        onChange={(e) => setNoteEdit({ id: p.id, text: e.target.value })}
                        onKeyDown={(e) => {
                          if (e.key === "Enter") void saveNote();
                          if (e.key === "Escape") setNoteEdit(null);
                        }}
                        onBlur={() => void saveNote()}
                      />
                    ) : (
                      p.note || "— (click to add)"
                    )}
                  </td>
                  <td><button className="chip-x" disabled={busy} onClick={() => deletePoint(p.id)} title="Delete point">×</button></td>
                </tr>
              ))}
              {groupPoints.length === 0 && (
                <tr><td colSpan={5} className="dim">no points yet — add them, or use the quick assign</td></tr>
              )}
            </tbody>
          </table>
          <div className="form-row">
            <input value={latlon} onChange={(e) => setLatlon(e.target.value)} placeholder="lat, lon" style={{ width: 190 }} />
            <input value={pointKm} onChange={(e) => setPointKm(e.target.value)} placeholder="chainage (km)" style={{ width: 110 }} />
            <input value={pointNote} onChange={(e) => setPointNote(e.target.value)} placeholder="note (optional)" style={{ width: 140 }} />
            <button className="btn" disabled={busy} onClick={addPoint}>Add point</button>
            <input
              ref={kmlFileRef}
              type="file"
              accept=".kml,.kmz,.xml"
              style={{ display: "none" }}
              onChange={(e) => { const f = e.target.files?.[0]; if (f) void onKmlFile(f); }}
            />
            <button className="btn" disabled={busy} onClick={() => kmlFileRef.current?.click()}
              title="Upload a KML/KMZ of survey points — chainage read from each placemark's name (12+400 or a number)">
              Import KML/KMZ…
            </button>
            <button className="btn" onClick={downloadSampleKml}
              title="Download the template your client's points file should follow">
              Sample KML
            </button>
          </div>

          {kmlRows && (
            <div className="kml-preview">
              <div className="form-row">
                <b>{kmlRows.filter((r) => r.include).length}</b>
                <span className="dim">of {kmlRows.length} points will import into group "{group || "any"}"
                  {kmlSkipped > 0 && ` · ${kmlSkipped} non-point placemark(s) skipped`}</span>
                <span className="dim">· values in names are</span>
                <select value={kmlUnit} onChange={(e) => setKmlUnitAndReparse(e.target.value as "km" | "m")}>
                  <option value="km">km</option>
                  <option value="m">meters</option>
                </select>
                <span className="dim">("12+400" always = 12 km + 400 m)</span>
              </div>
              <div className="kml-preview-list">
                <table>
                  <thead>
                    <tr><th></th><th>Placemark</th><th>Lat, Lon</th><th>Chainage (km)</th></tr>
                  </thead>
                  <tbody>
                    {kmlRows.map((r, i) => (
                      <tr key={i} className={r.include ? undefined : "row-ignored"}>
                        <td><input type="checkbox" checked={r.include}
                          onChange={() => setKmlRows((rows) => rows!.map((x, k) => k === i ? { ...x, include: !x.include } : x))} /></td>
                        <td className="mono">{r.name || <span className="dim">(unnamed)</span>}</td>
                        <td className="mono dim">{r.lat.toFixed(6)}, {r.lon.toFixed(6)}</td>
                        <td><input value={r.km} style={{ width: 90 }} placeholder="?"
                          onChange={(e) => setKmlRows((rows) => rows!.map((x, k) => k === i ? { ...x, km: e.target.value, include: true } : x))} /></td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
              <div className="form-row">
                <button className="btn primary" disabled={busy} onClick={importKml}>
                  Import {kmlRows.filter((r) => r.include).length} point(s)
                </button>
                <button className="btn" disabled={busy} onClick={() => setKmlRows(null)}>Cancel</button>
              </div>
            </div>
          )}
        </div>

        {/* Quick assign + recompute */}
        <div className="chainage-block">
          <div className="dim block-label">
            Two-point quick assign — client gives just the chainage at the route's two ends; the
            error is distributed over the whole route. Values are pre-filled from the {group ? `"${group}"` : "selected"} videos.
          </div>
          <div className="form-row">
            <label className="dim">One end</label>
            <input value={startKm} onChange={(e) => setStartKm(e.target.value)} style={{ width: 90 }} /> km
            <label className="dim">other end</label>
            <input value={endKm} onChange={(e) => setEndKm(e.target.value)} style={{ width: 90 }} /> km
            <button className="btn" disabled={busy}
              title="The 0 landed on the wrong physical end? Swap the two values and re-compute."
              onClick={() => { const s = startKm; setStartKm(endKm); setEndKm(s); }}>
              ⇄ swap
            </button>
            <button className="btn primary" disabled={busy || groupVideos.length === 0} onClick={quick}>
              Assign &amp; compute
            </button>
          </div>
          <div className="dim" style={{ marginTop: 6, fontSize: 12 }}>
            The two auto points ("quick: chain start/end") are saved into group <b>{group || "any"}</b>.
            After computing, check the km stones on the map — if 0 sits at the wrong end of the road,
            hit ⇄ swap and compute again. Footage direction doesn't matter (handled automatically).
          </div>
          <div className="form-row" style={{ marginTop: 10 }}>
            <button className="btn" disabled={busy || groupVideos.length === 0} onClick={recompute}
              title="Re-run the calibration with the points above">
              Recompute from points
            </button>
            <span className="dim">orders the {groupVideos.length} video(s) into a route, snaps the points, distributes the error</span>
          </div>
        </div>
      </div>

      {/* Result summary — freshly run, or the saved result of the last recompute */}
      {summary && (
        <div className="chainage-block">
          {summaryIsSaved && (() => {
            const c = calibs.find((x) => x.road_type === group);
            return (
              <div className="dim" style={{ fontSize: 12, marginBottom: 4 }}>
                Saved result of the last compute
                {c ? <> — by <b>{c.computed_by}</b> · {fmtWhen(c.computed_at)}</> : null}
                {" "}(recompute only if points or videos changed)
              </div>
            );
          })()}
          <div className="block-label">
            Route <b>{(summary.route_len_m / 1000).toFixed(3)} km</b> (GPS-measured)
            · {summary.points_used} point(s) used
            {summary.points_excluded > 0 && <span className="err-tag"> · {summary.points_excluded} excluded</span>}
            {summary.reversed && <span className="dim"> · direction auto-reversed</span>}
            {summary.majority_flipped && (
              <span className="dim" title="Most videos here were filmed driving toward decreasing chainage — that's this route's normal pattern, so only videos running AGAINST it are flagged below.">
                {" "}· footage mostly runs high → low chainage
              </span>
            )}
            {summary.videos.length > 1 && (() => {
              const ag = summary.videos.filter((v) => v.against_flow).length;
              return (
                <span className={ag > 0 ? "err-tag" : "dim"}
                  title={ag > 0 ? "Videos running against the route's dominant direction — on a divided road this often means the video belongs to the other carriageway (wrong group)." : "Every video runs in the route's dominant direction."}>
                  {" "}· {summary.videos.length - ag}/{summary.videos.length} follow the dominant direction{ag > 0 ? `, ${ag} against` : ""}
                </span>
              );
            })()}
          </div>
          {summary.warnings.length > 0 && (
            <ul className="chainage-warnings">
              {summary.warnings.map((w, i) => <li key={i}>⚠ {w}</li>)}
            </ul>
          )}
          {summary.spans.length > 0 && (
            <table>
              <thead>
                <tr><th>Span (client)</th><th>GPS length</th><th>Client length</th><th>Scale</th></tr>
              </thead>
              <tbody>
                {summary.spans.map((sp, i) => (
                  <tr key={i} className={Math.abs(sp.scale - 1) > 0.05 ? "span-warn" : undefined}>
                    <td>{fmtChainage(sp.from_chainage_m)} → {fmtChainage(sp.to_chainage_m)}</td>
                    <td>{(sp.gps_len_m / 1000).toFixed(3)} km</td>
                    <td>{(sp.client_len_m / 1000).toFixed(3)} km</td>
                    <td>×{sp.scale.toFixed(4)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          )}
          <table style={{ marginTop: 8 }}>
            <thead>
              <tr><th>#</th><th>Video</th><th>Chainage (client)</th><th>SR chainage (GPS)</th><th></th></tr>
            </thead>
            <tbody>
              {summary.videos.map((v) => (
                <tr key={v.id}>
                  <td className="dim">{v.seq + 1}</td>
                  <td className="mono">{v.file_name}</td>
                  <td>{fmtChainageRange(v.chainage_start_m, v.chainage_end_m)}</td>
                  <td className="dim">{fmtChainageRange(v.sr_start_m, v.sr_end_m)}</td>
                  <td className="dim">{v.against_flow && (
                    <span className="err-tag" title="This video's travel direction is OPPOSITE to most of the route. Chainage is still computed correctly — but if it's unexpected, the video may belong to the other carriageway (wrong group).">
                      ⇄ against flow
                    </span>
                  )}</td>
                </tr>
              ))}
            </tbody>
          </table>
          {summary.skipped.length > 0 && (
            <div className="dim" style={{ marginTop: 6 }}>
              Skipped (no GPS): {summary.skipped.join(", ")}
            </div>
          )}
        </div>
      )}
    </section>
  );
}
