// Solo Vision AI — admin Mission Control UI. Drives the cloud-side API
// added in routes/admin-vision-ai.ts / routes/vision-ai-device.ts /
// migrations/0059_solo_vision_ai.sql (see that migration's header
// comment for the full scoping rationale: this is the Raspberry Pi
// CM5 + Hailo-8L node's cloud counterpart, not the edge AI itself).
//
// Three tabs, one page, same "single admin-*-page.jsx with local tab
// state" idiom already used by admin-rut241-dashboard-style pages
// rather than three separate routed pages -- there's no deep-linkable
// sub-page need yet (no per-node URL, no per-event URL), so a page-level
// useState tab switch is the simplest thing that works:
//   Nodes  — fleet list, provision new node (one-time token reveal),
//            assign to asset, mode controls (product/performance/floor)
//   Zones  — pick a camera (via its asset), draw/edit zone polygons on
//            top of that camera's live snapshot (reuses the existing
//            admin snapshot proxy, GET /api/admin/units/:id/cameras/:slot/snapshot)
//   Events — list + filter, evidence viewer, human feedback loop
//
// Reuses ModalShell/ModalError/SectionLabel/EmptyNote/MiniField/
// ActionButton/adminInputStyle/adminSelectStyle from admin-assets-page.jsx
// and AdminShell/IconCamera from admin-shell.jsx/portal-icons.jsx (loaded
// earlier -- see index.html) rather than redefining any of them.

const VISION_TABS = [
  { id: "nodes", label: "Nodes" },
  { id: "zones", label: "Zones" },
  { id: "events", label: "AI Events" },
];

const VISION_PERFORMANCE_MODES = ["eco", "normal", "performance"];
const VISION_PRODUCT_MODES = ["off", "monitor", "armed_ai", "maintenance"];
const VISION_ZONE_TYPES = [
  { value: "restricted", label: "Restricted area" },
  { value: "detection", label: "Detection area" },
  { value: "ignore", label: "Ignore area" },
  { value: "vehicle", label: "Vehicle" },
  { value: "pedestrian", label: "Pedestrian" },
  { value: "entry", label: "Entry" },
  { value: "exit", label: "Exit" },
  { value: "loitering", label: "Loitering" },
  { value: "line_crossing", label: "Line crossing" },
];

function visionHealthTone(state) {
  if (state === "healthy") return { bg: "rgba(20,140,60,0.1)", fg: "rgba(20,120,50,0.95)" };
  if (state === "provisioned") return { bg: "rgba(0,0,0,0.05)", fg: "rgba(0,0,0,0.5)" };
  if (state === "offline") return { bg: "rgba(120,120,120,0.12)", fg: "rgba(0,0,0,0.55)" };
  if (state === "degraded" || state === "storage_warning") return { bg: "rgba(220,150,10,0.12)", fg: "rgba(180,110,0,0.95)" };
  return { bg: "rgba(190,40,40,0.1)", fg: "rgba(170,30,30,0.95)" }; // camera_error/model_error/ai_error/thermal_limit/power_limit/configuration_error
}

function VisionHealthBadge({ state }) {
  const tone = visionHealthTone(state);
  return (
    <span style={{
      display: "inline-block", padding: "3px 9px", borderRadius: 3,
      background: tone.bg, color: tone.fg,
      fontFamily: "var(--font-body)", fontSize: 10, fontWeight: 600,
      letterSpacing: "0.08em", textTransform: "uppercase",
    }}>
      {(state || "unknown").replace(/_/g, " ")}
    </span>
  );
}

// Entry-phase asset restructuring (migrations/0061_asset_category_vision_ai.sql,
// customer: "Edge and Cloud management (Ai) will be two separate areas
// under assets") -- this is the actual Nodes/Zones/Events tab content,
// pulled out of AdminVisionAiPage's own <AdminShell> so admin-assets-page.jsx
// can embed it directly as the Assets page's "Vision AI Edge" section
// instead of it only being reachable as its own top-level page. Takes no
// `onNavigate`/shell props -- the caller already owns the surrounding
// AdminShell.
function VisionAiEdgeManagement({ isReadOnly }) {
  const [tab, setTab] = useState("nodes");

  return (
    <React.Fragment>
      <EmptyNote>
        Cloud-side control for Solo Vision AI Edge nodes (Raspberry Pi CM5 + Hailo-8L on-device
        hardware) — provisioning, health/telemetry, zone configuration, and the AI event feedback
        loop. The node's own on-device AI pipeline is separate embedded hardware/firmware, not
        part of this page.
      </EmptyNote>

      <div style={{ display: "flex", gap: 2, marginBottom: 30, borderBottom: "1px solid rgba(0,0,0,0.1)" }}>
        {VISION_TABS.map((t) => (
          <button key={t.id} type="button" onClick={() => setTab(t.id)} data-testid={`vision-tab-${t.id}`}
            style={{
              background: "none", border: "none", cursor: "pointer",
              padding: "10px 18px 14px", fontFamily: "var(--font-body)", fontSize: 12.5,
              fontWeight: 600, letterSpacing: "0.08em", textTransform: "uppercase",
              color: tab === t.id ? "#000" : "rgba(0,0,0,0.45)",
              borderBottom: tab === t.id ? "2px solid #000" : "2px solid transparent",
              marginBottom: -1,
            }}>
            {t.label}
          </button>
        ))}
      </div>

      {tab === "nodes" && <VisionNodesTab isReadOnly={isReadOnly} />}
      {tab === "zones" && <VisionZonesTab isReadOnly={isReadOnly} />}
      {tab === "events" && <VisionEventsTab isReadOnly={isReadOnly} />}
    </React.Fragment>
  );
}

// Standalone top-level page wrapper -- kept on disk (unrouted from the
// sidebar nav as of this pass, see admin-shell.jsx's header comment)
// purely so re-exposing Vision AI Edge as its own page again later (the
// customer's own "may move later" caveat) is a one-line routing change,
// not a rebuild. Its ENTIRE body is now just VisionAiEdgeManagement
// inside a shell, so the two can never drift apart.
function AdminVisionAiPage({ onNavigate }) {
  const [admin, setAdmin] = useState(null);
  const [status, setStatus] = useState("loading");

  useEffect(() => {
    fetch("/api/admin/me", { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((me) => { setAdmin(me.admin); setStatus("ready"); })
      .catch(() => onNavigate("admin-login"));
  }, []); // eslint-disable-line react-hooks/exhaustive-deps -- mount-only

  if (status === "loading") {
    return <section style={{ background: "#fff", minHeight: "calc(100vh - 88px)" }} />;
  }

  const isReadOnly = admin && admin.role !== "super_admin";

  return (
    <AdminShell admin={admin} page="admin-vision-ai" onNavigate={onNavigate}
      subtitle="Staff only · Mission Control" title="Solo Vision AI Edge">
      <VisionAiEdgeManagement isReadOnly={isReadOnly} />
    </AdminShell>
  );
}

// ---------------------------------------------------------------------
// Nodes tab

function VisionNodesTab({ isReadOnly }) {
  const [state, setState] = useState({ status: "loading", nodes: [] });
  const [showProvision, setShowProvision] = useState(false);
  const [managingNode, setManagingNode] = useState(null);

  const load = () => {
    fetch("/api/admin/vision-nodes", { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => setState({ status: "ready", nodes: data.nodes || [] }))
      .catch(() => setState({ status: "error", nodes: [] }));
  };
  useEffect(load, []); // eslint-disable-line react-hooks/exhaustive-deps

  return (
    <React.Fragment>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
        <SectionLabel>Nodes ({state.nodes.length})</SectionLabel>
        <ActionButton readOnly={isReadOnly} onClick={() => setShowProvision(true)} testId="vision-node-provision-btn">
          + Provision node
        </ActionButton>
      </div>

      {state.status === "loading" && (
        <div style={{ fontFamily: "var(--font-body)", fontSize: 13, color: "rgba(0,0,0,0.5)" }}>Loading…</div>
      )}
      {state.status === "error" && <ModalError>Couldn't load Vision AI nodes.</ModalError>}
      {state.status === "ready" && state.nodes.length === 0 && (
        <EmptyNote>No Vision AI nodes provisioned yet.</EmptyNote>
      )}
      {state.status === "ready" && state.nodes.length > 0 && (
        <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
          {state.nodes.map((node) => (
            <VisionNodeRow key={node.id} node={node} onManage={() => setManagingNode(node)} />
          ))}
        </div>
      )}

      {showProvision && (
        <ProvisionNodeModal onCancel={() => setShowProvision(false)} onProvisioned={() => { setShowProvision(false); load(); }} />
      )}
      {managingNode && (
        <ManageNodeModal node={managingNode} isReadOnly={isReadOnly}
          onCancel={() => setManagingNode(null)}
          onChanged={() => { load(); }}
        />
      )}
    </React.Fragment>
  );
}

function VisionNodeRow({ node, onManage }) {
  return (
    <div data-testid={`vision-node-row-${node.id}`} style={{
      background: "rgba(0,0,0,0.03)", border: "1px solid rgba(0,0,0,0.14)",
      padding: "16px 20px", display: "flex", justifyContent: "space-between",
      alignItems: "center", flexWrap: "wrap", gap: 14,
    }} onClick={onManage} role="button">
      <div>
        <div style={{ fontFamily: "var(--font-body)", fontSize: 14.5, fontWeight: 600, color: "#000" }}>
          {node.nodeSerial}
          <span style={{ marginLeft: 10 }}><VisionHealthBadge state={node.healthState} /></span>
        </div>
        <div style={{ fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(0,0,0,0.55)", marginTop: 4 }}>
          {node.assetUnitName ? `Assigned to ${node.assetUnitName}` : "Not yet assigned to an asset"}
        </div>
        <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.45)", marginTop: 4 }}>
          Mode: {node.productMode} · Performance: {node.performanceMode} (floor: {node.minimumPerformanceMode})
          {node.camerasTotal > 0 && ` · Cameras ${node.camerasOnline}/${node.camerasTotal} online`}
        </div>
      </div>
      <ActionButton onClick={onManage}>Manage</ActionButton>
    </div>
  );
}

function ProvisionNodeModal({ onCancel, onProvisioned }) {
  const [nodeSerial, setNodeSerial] = useState("");
  const [status, setStatus] = useState("idle");
  const [error, setError] = useState("");
  const [result, setResult] = useState(null); // { nodeSerial, deviceToken }

  const handleSubmit = async (e) => {
    e.preventDefault();
    setStatus("submitting");
    setError("");
    try {
      const res = await fetch("/api/admin/vision-nodes", {
        method: "POST", credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ nodeSerial }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) { setError(data.error || "Something went wrong."); setStatus("idle"); return; }
      setResult(data);
    } catch {
      setError("Couldn't reach the server. Check your connection and try again.");
      setStatus("idle");
    }
  };

  if (result) {
    return (
      <ModalShell title="Node provisioned" onCancel={onProvisioned}>
        <div style={{
          background: "rgba(190,40,40,0.06)", border: "1px solid rgba(190,40,40,0.3)",
          padding: "12px 14px", marginBottom: 16, fontFamily: "var(--font-body)", fontSize: 12.5,
          color: "#8a1f1f", lineHeight: 1.5,
        }}>
          This device token is shown <strong>once</strong> and can't be retrieved again. Copy it now
          and flash it onto node <strong>{result.nodeSerial}</strong>.
        </div>
        <MiniField label="Device token">
          <div style={{ display: "flex", gap: 8 }}>
            <input readOnly value={result.deviceToken} style={{ ...adminInputStyle, fontFamily: "monospace" }}
              onFocus={(e) => e.target.select()} />
            <ActionButton onClick={() => { navigator.clipboard && navigator.clipboard.writeText(result.deviceToken); }}>
              Copy
            </ActionButton>
          </div>
        </MiniField>
        <div style={{ display: "flex", justifyContent: "flex-end", marginTop: 18 }}>
          <ActionButton onClick={onProvisioned}>Done</ActionButton>
        </div>
      </ModalShell>
    );
  }

  return (
    <ModalShell title="Provision Vision AI node" onCancel={onCancel}>
      {error && <ModalError>{error}</ModalError>}
      <form onSubmit={handleSubmit}>
        <MiniField label="Node serial">
          <input style={adminInputStyle} value={nodeSerial} onChange={(e) => setNodeSerial(e.target.value)} required
            placeholder="e.g. SVAI-0000124" />
        </MiniField>
        <div style={{ display: "flex", justifyContent: "flex-end", gap: 10, marginTop: 10 }}>
          <ActionButton disabled={status === "submitting"} danger onClick={onCancel}>Cancel</ActionButton>
          <button type="submit" disabled={status === "submitting"} style={{
            background: "#000", color: "#fff", border: "1px solid #000",
            padding: "10px 18px", cursor: "pointer",
            fontFamily: "var(--font-body)", fontSize: 11.5, fontWeight: 500,
            letterSpacing: "0.14em", textTransform: "uppercase",
          }}>
            {status === "submitting" ? "Provisioning…" : "Provision"}
          </button>
        </div>
      </form>
    </ModalShell>
  );
}

function ManageNodeModal({ node, isReadOnly, onCancel, onChanged }) {
  const [detail, setDetail] = useState(null);
  const [assetUnitId, setAssetUnitId] = useState(node.assetUnitId ? String(node.assetUnitId) : "");
  const [unitOptions, setUnitOptions] = useState([]);
  const [productMode, setProductMode] = useState(node.productMode);
  const [performanceMode, setPerformanceMode] = useState(node.performanceMode);
  const [minimumPerformanceMode, setMinimumPerformanceMode] = useState(node.minimumPerformanceMode);
  const [dynamicEnergyMode, setDynamicEnergyMode] = useState(node.dynamicEnergyMode);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");

  const load = () => {
    Promise.all([
      fetch(`/api/admin/vision-nodes/${node.id}`, { credentials: "same-origin" }).then((r) => (r.ok ? r.json() : Promise.reject())),
      // Only offer assets actually tagged as Vision AI Edge hardware
      // (migrations/0061's products.category) -- the backend enforces
      // this too (see admin-vision-ai.ts's assign/create routes), this
      // is just so the picker doesn't even list an asset that would be
      // rejected.
      fetch("/api/admin/units?category=vision_ai_edge", { credentials: "same-origin" }).then((r) => (r.ok ? r.json() : Promise.reject())),
    ])
      .then(([nodeData, unitsData]) => {
        setDetail(nodeData.node);
        setUnitOptions(unitsData.units || []);
      })
      .catch(() => setError("Couldn't load node details."));
  };
  useEffect(load, [node.id]); // eslint-disable-line react-hooks/exhaustive-deps

  const saveAssign = async () => {
    setBusy(true); setError("");
    try {
      const res = await fetch(`/api/admin/vision-nodes/${node.id}/assign`, {
        method: "PUT", credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ assetUnitId: assetUnitId ? Number(assetUnitId) : null }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) { setError(data.error || "Something went wrong."); return; }
      load(); onChanged();
    } finally { setBusy(false); }
  };

  const saveModes = async () => {
    setBusy(true); setError("");
    try {
      const res = await fetch(`/api/admin/vision-nodes/${node.id}/modes`, {
        method: "PUT", credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ productMode, performanceMode, minimumPerformanceMode, dynamicEnergyMode }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) { setError(data.error || "Something went wrong."); return; }
      load(); onChanged();
    } finally { setBusy(false); }
  };

  return (
    <ModalShell title={`Node — ${node.nodeSerial}`} onCancel={onCancel} wide>
      {error && <ModalError>{error}</ModalError>}
      {!detail ? (
        <div style={{ fontFamily: "var(--font-body)", fontSize: 13, color: "rgba(0,0,0,0.5)" }}>Loading…</div>
      ) : (
        <React.Fragment>
          <div style={{ marginBottom: 8 }}><VisionHealthBadge state={detail.healthState} /></div>
          <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.5)", marginBottom: 20 }}>
            Last heartbeat: {detail.lastHeartbeatAt || "never"} · Cameras {detail.camerasOnline}/{detail.camerasTotal}
            {detail.softwareVersion ? ` · Vision Agent ${detail.softwareVersion}` : ""}
          </div>

          {(detail.cpuPct != null || detail.hailoTempC != null || detail.inferenceFps != null) && (
            <React.Fragment>
              <SectionLabel>Telemetry</SectionLabel>
              <div style={{ display: "flex", gap: 18, flexWrap: "wrap", marginBottom: 24, fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(0,0,0,0.65)" }}>
                {detail.cpuPct != null && <span>CPU {detail.cpuPct}%</span>}
                {detail.memoryPct != null && <span>Memory {detail.memoryPct}%</span>}
                {detail.cpuTempC != null && <span>CPU temp {detail.cpuTempC}°C</span>}
                {detail.hailoTempC != null && <span>Hailo temp {detail.hailoTempC}°C</span>}
                {detail.inferenceFps != null && <span>Inference {detail.inferenceFps} fps</span>}
                {detail.powerWatts != null && <span>{detail.powerWatts} W</span>}
              </div>
            </React.Fragment>
          )}

          <SectionLabel>Assignment</SectionLabel>
          <MiniField label="Assigned SOLO asset">
            <div style={{ display: "flex", gap: 10 }}>
              <select style={{ ...adminSelectStyle, flex: 1 }} value={assetUnitId} onChange={(e) => setAssetUnitId(e.target.value)} disabled={isReadOnly}>
                <option value="">— None —</option>
                {unitOptions.map((u) => <option key={u.id} value={u.id}>{u.serial_number}</option>)}
              </select>
              <ActionButton disabled={busy} readOnly={isReadOnly} onClick={saveAssign}>Save</ActionButton>
            </div>
          </MiniField>

          <SectionLabel>Modes</SectionLabel>
          <div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
            <div style={{ flex: "1 1 160px" }}>
              <MiniField label="Product mode">
                <select style={adminSelectStyle} value={productMode} onChange={(e) => setProductMode(e.target.value)} disabled={isReadOnly}>
                  {VISION_PRODUCT_MODES.map((m) => <option key={m} value={m}>{m}</option>)}
                </select>
              </MiniField>
            </div>
            <div style={{ flex: "1 1 160px" }}>
              <MiniField label="Performance mode">
                <select style={adminSelectStyle} value={performanceMode} onChange={(e) => setPerformanceMode(e.target.value)} disabled={isReadOnly}>
                  {VISION_PERFORMANCE_MODES.map((m) => <option key={m} value={m}>{m}</option>)}
                </select>
              </MiniField>
            </div>
            <div style={{ flex: "1 1 160px" }}>
              <MiniField label="Minimum (security floor)">
                <select style={adminSelectStyle} value={minimumPerformanceMode} onChange={(e) => setMinimumPerformanceMode(e.target.value)} disabled={isReadOnly}>
                  {VISION_PERFORMANCE_MODES.map((m) => <option key={m} value={m}>{m}</option>)}
                </select>
              </MiniField>
            </div>
          </div>
          <label style={{ display: "flex", alignItems: "center", gap: 8, fontFamily: "var(--font-body)", fontSize: 13, color: "#000", margin: "4px 0 20px" }}>
            <input type="checkbox" checked={dynamicEnergyMode} onChange={(e) => setDynamicEnergyMode(e.target.checked)} disabled={isReadOnly} />
            Allow automatic energy-based downgrading (never below the floor above)
          </label>
          <div style={{ display: "flex", justifyContent: "flex-end", gap: 10 }}>
            <ActionButton disabled={busy} readOnly={isReadOnly} onClick={saveModes}>Save modes</ActionButton>
          </div>
        </React.Fragment>
      )}
      <div style={{ display: "flex", justifyContent: "flex-end", marginTop: 18, borderTop: "1px solid rgba(0,0,0,0.1)", paddingTop: 18 }}>
        <ActionButton onClick={onCancel}>Close</ActionButton>
      </div>
    </ModalShell>
  );
}

// ---------------------------------------------------------------------
// Zones tab — pick an asset -> pick a camera slot -> draw/edit polygons
// on top of that camera's live snapshot. A camera's zones exist
// independent of whether a Vision AI node is currently assigned to that
// asset (a zone belongs to the camera's view, see migration's header
// comment), so this tab is asset/camera-driven, not node-driven.

function VisionZonesTab({ isReadOnly }) {
  const [serialQuery, setSerialQuery] = useState("");
  const [units, setUnits] = useState([]);
  const [selectedUnitId, setSelectedUnitId] = useState("");
  const [cameras, setCameras] = useState([]);
  const [selectedCameraId, setSelectedCameraId] = useState("");

  const searchUnits = (q) => {
    setSerialQuery(q);
    if (!q || q.length < 2) { setUnits([]); return; }
    fetch(`/api/admin/units?serial=${encodeURIComponent(q)}`, { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => setUnits(data.units || []))
      .catch(() => setUnits([]));
  };

  useEffect(() => {
    if (!selectedUnitId) { setCameras([]); setSelectedCameraId(""); return; }
    fetch(`/api/admin/units/${selectedUnitId}/cameras`, { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => setCameras((data.cameras || []).filter((c) => c.configured)))
      .catch(() => setCameras([]));
  }, [selectedUnitId]);

  const selectedCamera = cameras.find((c) => String(c.id) === selectedCameraId);

  return (
    <React.Fragment>
      <SectionLabel>Pick a camera</SectionLabel>
      <div style={{ display: "flex", gap: 12, flexWrap: "wrap", marginBottom: 24 }}>
        <div style={{ flex: "1 1 220px" }}>
          <MiniField label="Search asset by serial">
            <input style={adminInputStyle} value={serialQuery} onChange={(e) => searchUnits(e.target.value)}
              placeholder="Type a serial number…" />
          </MiniField>
          {units.length > 0 && (
            <div style={{ border: "1px solid rgba(0,0,0,0.15)", maxHeight: 160, overflowY: "auto" }}>
              {units.map((u) => (
                <div key={u.id} onClick={() => { setSelectedUnitId(String(u.id)); setSerialQuery(u.serial_number); setUnits([]); }}
                  style={{ padding: "8px 12px", cursor: "pointer", fontFamily: "var(--font-body)", fontSize: 13, borderBottom: "1px solid rgba(0,0,0,0.06)" }}
                  onMouseEnter={(e) => e.currentTarget.style.background = "rgba(0,0,0,0.04)"}
                  onMouseLeave={(e) => e.currentTarget.style.background = "none"}>
                  {u.serial_number}
                </div>
              ))}
            </div>
          )}
        </div>
        {selectedUnitId && cameras.length > 0 && (
          <div style={{ flex: "1 1 220px" }}>
            <MiniField label="Camera slot">
              <select style={adminSelectStyle} value={selectedCameraId} onChange={(e) => setSelectedCameraId(e.target.value)}>
                <option value="">— Select a camera —</option>
                {cameras.map((c) => <option key={c.id} value={c.id}>Slot {c.slot}{c.label ? ` — ${c.label}` : ""}</option>)}
              </select>
            </MiniField>
          </div>
        )}
        {selectedUnitId && cameras.length === 0 && (
          <EmptyNote>This asset has no configured cameras.</EmptyNote>
        )}
      </div>

      {selectedCamera && (
        <ZoneEditor key={selectedCamera.id} unitId={selectedUnitId} camera={selectedCamera} isReadOnly={isReadOnly} />
      )}
    </React.Fragment>
  );
}

function ZoneEditor({ unitId, camera, isReadOnly }) {
  const [zones, setZones] = useState([]);
  const [status, setStatus] = useState("loading");
  const [drawing, setDrawing] = useState(null); // { points: [[x,y],...] } while actively drawing
  const [pendingType, setPendingType] = useState("restricted");
  const [selectedZoneId, setSelectedZoneId] = useState(null);
  const [error, setError] = useState("");
  const imgRef = useRef(null);
  const [imgError, setImgError] = useState(false);
  const snapshotUrl = `/api/admin/units/${unitId}/cameras/${camera.slot}/snapshot?t=${camera.id}`;

  const load = () => {
    fetch(`/api/admin/unit-cameras/${camera.id}/vision-zones`, { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => { setZones(data.zones || []); setStatus("ready"); })
      .catch(() => setStatus("error"));
  };
  useEffect(load, [camera.id]); // eslint-disable-line react-hooks/exhaustive-deps

  const isLine = pendingType === "line_crossing";

  const handleImageClick = (e) => {
    if (isReadOnly) return;
    const rect = e.currentTarget.getBoundingClientRect();
    const x = Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width));
    const y = Math.min(1, Math.max(0, (e.clientY - rect.top) / rect.height));
    setDrawing((prev) => {
      const points = prev ? [...prev.points, [x, y]] : [[x, y]];
      // A line auto-completes at exactly 2 points -- no double-click needed.
      return { points };
    });
  };

  const finishDrawing = async () => {
    if (!drawing || drawing.points.length < (isLine ? 2 : 3)) return;
    setError("");
    const name = window.prompt("Name this zone:", VISION_ZONE_TYPES.find((t) => t.value === pendingType)?.label || "Zone");
    if (!name) return;
    try {
      const res = await fetch(`/api/admin/unit-cameras/${camera.id}/vision-zones`, {
        method: "POST", credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name, zoneType: pendingType, isLine, polygon: drawing.points }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) { setError(data.error || "Couldn't save zone."); return; }
      setDrawing(null);
      load();
    } catch {
      setError("Couldn't reach the server.");
    }
  };

  const deleteZone = async (zone) => {
    if (!window.confirm(`Delete zone "${zone.name}"?`)) return;
    await fetch(`/api/admin/vision-zones/${zone.id}`, { method: "DELETE", credentials: "same-origin" });
    if (selectedZoneId === zone.id) setSelectedZoneId(null);
    load();
  };

  const toggleZoneEnabled = async (zone) => {
    await fetch(`/api/admin/vision-zones/${zone.id}`, {
      method: "PUT", credentials: "same-origin",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ enabled: !zone.enabled }),
    });
    load();
  };

  // Renders each stored zone's polygon as an SVG overlay on top of the
  // <img> snapshot -- normalised 0-1 coordinates map directly onto a
  // 0-100% viewBox, so no pixel math is needed regardless of the
  // snapshot's actual resolution (same reasoning as the spec's own §23
  // rationale for storing zones normalised in the first place).
  const renderPolygon = (points, closed) => {
    const pts = points.map(([x, y]) => `${x * 100},${y * 100}`).join(" ");
    return closed ? <polygon points={pts} /> : <polyline points={pts} />;
  };

  return (
    <div>
      {error && <ModalError>{error}</ModalError>}
      {status === "loading" && (
        <div style={{ fontFamily: "var(--font-body)", fontSize: 13, color: "rgba(0,0,0,0.5)" }}>Loading zones…</div>
      )}

      {!isReadOnly && (
        <div style={{ display: "flex", gap: 12, alignItems: "center", marginBottom: 12, flexWrap: "wrap" }}>
          <select style={{ ...adminSelectStyle, width: 220 }} value={pendingType}
            onChange={(e) => { setPendingType(e.target.value); setDrawing(null); }}>
            {VISION_ZONE_TYPES.map((t) => <option key={t.value} value={t.value}>{t.label}</option>)}
          </select>
          <span style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.5)" }}>
            Click the image to place points{isLine ? " (2 points)" : " (3+ points, then Finish)"}.
          </span>
          {drawing && drawing.points.length > 0 && (
            <React.Fragment>
              <ActionButton onClick={finishDrawing} disabled={drawing.points.length < (isLine ? 2 : 3)}>
                Finish zone
              </ActionButton>
              <ActionButton danger onClick={() => setDrawing(null)}>Cancel</ActionButton>
            </React.Fragment>
          )}
        </div>
      )}

      <div style={{ position: "relative", maxWidth: 640, border: "1px solid rgba(0,0,0,0.15)", background: "#000" }}>
        {imgError ? (
          <div style={{ padding: 40, textAlign: "center", color: "rgba(255,255,255,0.6)", fontFamily: "var(--font-body)", fontSize: 13 }}>
            Couldn't load a live snapshot for this camera.
          </div>
        ) : (
          <img ref={imgRef} src={snapshotUrl} alt={`Camera slot ${camera.slot}`} onError={() => setImgError(true)}
            onClick={handleImageClick} draggable={false}
            style={{ display: "block", width: "100%", cursor: isReadOnly ? "default" : "crosshair", userSelect: "none" }} />
        )}
        <svg viewBox="0 0 100 100" preserveAspectRatio="none"
          style={{ position: "absolute", inset: 0, width: "100%", height: "100%", pointerEvents: "none" }}>
          <style>{`
            polygon, polyline { fill: rgba(232,146,11,0.18); stroke: #E8920A; stroke-width: 0.4; vector-effect: non-scaling-stroke; }
            polyline { fill: none; stroke: #E8920A; }
            .vision-zone-dim polygon, .vision-zone-dim polyline { opacity: 0.35; }
          `}</style>
          {zones.map((z) => (
            <g key={z.id} className={z.enabled ? "" : "vision-zone-dim"}>
              {renderPolygon(z.polygon, !z.isLine)}
            </g>
          ))}
          {drawing && drawing.points.length > 0 && (
            <polyline points={drawing.points.map(([x, y]) => `${x * 100},${y * 100}`).join(" ")}
              style={{ fill: "none", stroke: "#fff", strokeWidth: 0.5, vectorEffect: "non-scaling-stroke" }} />
          )}
        </svg>
      </div>

      {zones.length === 0 ? (
        <EmptyNote>No zones drawn on this camera yet.</EmptyNote>
      ) : (
        <div style={{ display: "flex", flexDirection: "column", gap: 8, marginTop: 16 }}>
          {zones.map((z) => (
            <div key={z.id} style={{
              display: "flex", justifyContent: "space-between", alignItems: "center",
              background: "rgba(0,0,0,0.03)", border: "1px solid rgba(0,0,0,0.1)", padding: "10px 14px",
            }}>
              <div style={{ fontFamily: "var(--font-body)", fontSize: 13, color: "#000" }}>
                {z.name} <span style={{ color: "rgba(0,0,0,0.45)" }}>· {VISION_ZONE_TYPES.find((t) => t.value === z.zoneType)?.label || z.zoneType}</span>
                {!z.enabled && <span style={{ marginLeft: 8, fontSize: 9, letterSpacing: "0.1em", textTransform: "uppercase", color: "rgba(190,40,40,0.8)" }}>Disabled</span>}
              </div>
              <div style={{ display: "flex", gap: 8 }}>
                <ActionButton readOnly={isReadOnly} onClick={() => toggleZoneEnabled(z)}>
                  {z.enabled ? "Disable" : "Enable"}
                </ActionButton>
                <ActionButton danger readOnly={isReadOnly} onClick={() => deleteZone(z)}>Delete</ActionButton>
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ---------------------------------------------------------------------
// Events tab

function VisionEventsTab() {
  const [events, setEvents] = useState([]);
  const [status, setStatus] = useState("loading");
  const [filter, setFilter] = useState("pending");
  const [viewingEvent, setViewingEvent] = useState(null);

  const load = () => {
    setStatus("loading");
    const qs = filter === "all" ? "" : `?feedback=${filter}`;
    fetch(`/api/admin/vision-events${qs}`, { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => { setEvents(data.events || []); setStatus("ready"); })
      .catch(() => setStatus("error"));
  };
  useEffect(load, [filter]); // eslint-disable-line react-hooks/exhaustive-deps

  return (
    <React.Fragment>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
        <SectionLabel>AI Events</SectionLabel>
        <select style={{ ...adminSelectStyle, width: 200 }} value={filter} onChange={(e) => setFilter(e.target.value)}>
          <option value="pending">Awaiting feedback</option>
          <option value="all">All events</option>
          <option value="true_alarm">Marked true alarm</option>
          <option value="false_alarm">Marked false alarm</option>
          <option value="unsure">Marked unsure</option>
        </select>
      </div>

      {status === "loading" && (
        <div style={{ fontFamily: "var(--font-body)", fontSize: 13, color: "rgba(0,0,0,0.5)" }}>Loading…</div>
      )}
      {status === "error" && <ModalError>Couldn't load events.</ModalError>}
      {status === "ready" && events.length === 0 && <EmptyNote>No events match this filter.</EmptyNote>}
      {status === "ready" && events.length > 0 && (
        <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
          {events.map((ev) => (
            <div key={ev.id} onClick={() => setViewingEvent(ev)} role="button" style={{
              background: "rgba(0,0,0,0.03)", border: "1px solid rgba(0,0,0,0.12)",
              padding: "12px 16px", display: "flex", justifyContent: "space-between",
              alignItems: "center", flexWrap: "wrap", gap: 10, cursor: "pointer",
            }}>
              <div>
                <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, fontWeight: 600, color: "#000" }}>
                  {ev.event_type.replace(/_/g, " ")}
                  {ev.severity === "high" && <span style={{ marginLeft: 8, color: "rgba(170,30,30,0.9)", fontSize: 10, letterSpacing: "0.08em", textTransform: "uppercase" }}>High</span>}
                </div>
                <div style={{ fontFamily: "var(--font-body)", fontSize: 12, color: "rgba(0,0,0,0.5)", marginTop: 3 }}>
                  {ev.asset_unit_name || `Asset #${ev.asset_unit_id}`} · {ev.object_class || "—"} · {ev.started_at}
                </div>
              </div>
              <div style={{ fontFamily: "var(--font-body)", fontSize: 11, textTransform: "uppercase", letterSpacing: "0.08em", color: ev.feedback ? "rgba(20,120,50,0.9)" : "rgba(0,0,0,0.4)" }}>
                {ev.feedback ? ev.feedback.replace(/_/g, " ") : "Pending review"}
              </div>
            </div>
          ))}
        </div>
      )}

      {viewingEvent && (
        <EventDetailModal eventId={viewingEvent.id} onCancel={() => setViewingEvent(null)} onChanged={load} />
      )}
    </React.Fragment>
  );
}

function EventDetailModal({ eventId, onCancel, onChanged }) {
  const [event, setEvent] = useState(null);
  const [busy, setBusy] = useState(false);
  const [reason, setReason] = useState("");
  const [error, setError] = useState("");

  const load = () => {
    fetch(`/api/admin/vision-events/${eventId}`, { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : Promise.reject()))
      .then((data) => setEvent(data.event))
      .catch(() => setError("Couldn't load this event."));
  };
  useEffect(load, [eventId]); // eslint-disable-line react-hooks/exhaustive-deps

  const submitFeedback = async (feedback) => {
    setBusy(true); setError("");
    try {
      const res = await fetch(`/api/admin/vision-events/${eventId}/feedback`, {
        method: "PUT", credentials: "same-origin",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ feedback, feedbackReason: reason || undefined }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) { setError(data.error || "Something went wrong."); return; }
      onChanged();
      onCancel();
    } finally { setBusy(false); }
  };

  return (
    <ModalShell title="Event detail" onCancel={onCancel} wide>
      {error && <ModalError>{error}</ModalError>}
      {!event ? (
        <div style={{ fontFamily: "var(--font-body)", fontSize: 13, color: "rgba(0,0,0,0.5)" }}>Loading…</div>
      ) : (
        <React.Fragment>
          <div style={{ fontFamily: "var(--font-body)", fontSize: 15, fontWeight: 600, color: "#000", marginBottom: 6 }}>
            {event.event_type.replace(/_/g, " ")}
          </div>
          <div style={{ fontFamily: "var(--font-body)", fontSize: 12.5, color: "rgba(0,0,0,0.55)", marginBottom: 20 }}>
            {event.asset_unit_name} · Node {event.node_serial} · {event.started_at}
            {event.object_class && ` · ${event.object_class}${event.object_confidence != null ? ` (${Math.round(event.object_confidence * 100)}%)` : ""}`}
          </div>

          {event.snapshot_r2_key && (
            <img src={`/api/admin/vision-events/${eventId}/evidence/snapshot`} alt="Event snapshot"
              style={{ width: "100%", maxHeight: 320, objectFit: "contain", background: "#000", marginBottom: 20 }} />
          )}

          <SectionLabel>Human feedback</SectionLabel>
          {event.feedback && (
            <div style={{ fontFamily: "var(--font-body)", fontSize: 13, color: "rgba(0,0,0,0.6)", marginBottom: 12 }}>
              Already marked <strong>{event.feedback.replace(/_/g, " ")}</strong>{event.feedback_reason ? ` — ${event.feedback_reason}` : ""}.
              You can change it below.
            </div>
          )}
          <MiniField label="Reason (optional)">
            <input style={adminInputStyle} value={reason} onChange={(e) => setReason(e.target.value)}
              placeholder="e.g. Authorised person, Animal, Vegetation…" />
          </MiniField>
          <div style={{ display: "flex", gap: 10, flexWrap: "wrap", marginTop: 10 }}>
            <ActionButton disabled={busy} onClick={() => submitFeedback("true_alarm")}>True alarm</ActionButton>
            <ActionButton disabled={busy} onClick={() => submitFeedback("false_alarm")}>False alarm</ActionButton>
            <ActionButton disabled={busy} onClick={() => submitFeedback("unsure")}>Unsure</ActionButton>
          </div>
        </React.Fragment>
      )}
      <div style={{ display: "flex", justifyContent: "flex-end", marginTop: 18, borderTop: "1px solid rgba(0,0,0,0.1)", paddingTop: 18 }}>
        <ActionButton onClick={onCancel}>Close</ActionButton>
      </div>
    </ModalShell>
  );
}

Object.assign(window, { AdminVisionAiPage, VisionAiEdgeManagement });
