/* Single Screen drilldown — port of prototype single-screen.jsx,
   wired to /api/screens/:code (returns rich object with screen + zone +
   site + organisation + species). Many KPI fields (uptime, brightness,
   CPU, memory, temp, signal) and event history don't exist in the real
   DB; they show '—' or sensible static defaults. */

const SCS_THEMES = {
  reef:      { fg: '#5DD3D3', grad: 'linear-gradient(135deg, #1f5961 0%, #0d2a30 100%)' },
  jellyfish: { fg: '#C99BFF', grad: 'linear-gradient(135deg, #3a2454 0%, #15102a 100%)' },
  shark:     { fg: '#9DBCFF', grad: 'linear-gradient(135deg, #1a2748 0%, #0a1024 100%)' },
  pelagic:   { fg: '#7DC0E0', grad: 'linear-gradient(135deg, #14384c 0%, #08182a 100%)' },
  touchpool: { fg: '#F2C879', grad: 'linear-gradient(135deg, #4a3618 0%, #1f1608 100%)' },
  cold:      { fg: '#A6E3FA', grad: 'linear-gradient(135deg, #1a4456 0%, #0a1d28 100%)' },
};

function SCS_lastSeenLabel(iso) {
  if (!iso) return 'never';
  const ageMs = Date.now() - new Date(iso).getTime();
  const m = Math.round(ageMs / 60000);
  if (m < 1) return 'just now';
  if (m < 60) return `${m} min ago`;
  const h = Math.round(m / 60);
  if (h < 24) return `${h} h ago`;
  const d = Math.round(h / 24);
  return `${d} d ago`;
}

function SCS_status(iso) {
  if (!iso) return { lvl: 'never', label: 'Never seen', dot: 'var(--aq-text-faint)' };
  const ageMs = Date.now() - new Date(iso).getTime();
  if (ageMs < 5 * 60 * 1000) {
    return { lvl: 'online', label: `Online · last heartbeat ${SCS_lastSeenLabel(iso)}`, dot: 'var(--aq-success)' };
  }
  if (ageMs < 60 * 60 * 1000) {
    return { lvl: 'idle', label: `Idle · last heartbeat ${SCS_lastSeenLabel(iso)}`, dot: '#F2C879' };
  }
  return { lvl: 'offline', label: `Offline · last seen ${SCS_lastSeenLabel(iso)}`, dot: 'var(--aq-danger)' };
}

function SCS_Bar({ value, max = 100, color }) {
  const pct = value == null ? 0 : (value / max) * 100;
  return (
    <div className="scs-bar">
      <div className="scs-bar-fill" style={{ width: `${pct}%`, background: color }} />
    </div>
  );
}

/* Screen size presets — mirrors the list used by the Register Screen
   modal in displays.jsx. Kept inline here (rather than imported via
   window.*) so this file parses standalone and a future split of the
   bundle doesn't accidentally produce two different preset lists.
   Width/height are the native pixel grid; we normalise to landscape
   orientation for matching and swap on save if orient === 'portrait'.
   Order matters — first match wins when locating the preset for an
   existing screen. */
const EDIT_SIZE_PRESETS = [
  { id: '24',    label: '24" Full HD',       width: 1920, height: 1080 },
  { id: '32',    label: '32" Full HD',       width: 1920, height: 1080 },
  { id: '43',    label: '43" Full HD',       width: 1920, height: 1080 },
  { id: '55',    label: '55" 4K UHD',        width: 3840, height: 2160 },
  { id: '65',    label: '65" 4K UHD',        width: 3840, height: 2160 },
  { id: '75',    label: '75" 4K UHD',        width: 3840, height: 2160 },
  { id: '85',    label: '85" 4K UHD',        width: 3840, height: 2160 },
  { id: 'custom', label: 'Custom / unknown', width: null, height: null },
];

/* Resolve the screen's stored resolution to a preset id. Curator
   sees 7 named sizes plus "custom". We match on landscape-normalised
   dimensions because portrait flips them on save. We prefer the
   common defaults (43" Full HD, 55" 4K) when multiple presets share
   a resolution — these are by far the most common SKUs in practice. */
function resolvePresetId(screen) {
  if (!screen || !screen.resolution_width || !screen.resolution_height) return 'custom';
  const w = Math.max(screen.resolution_width, screen.resolution_height);
  const h = Math.min(screen.resolution_width, screen.resolution_height);
  // If the row recorded the explicit inches (POST endpoint accepts
  // display_size_inches), prefer that. Falls back to resolution match.
  const fromInches = screen.display_size_inches ? String(screen.display_size_inches) : null;
  if (fromInches && EDIT_SIZE_PRESETS.find((p) => p.id === fromInches)) return fromInches;
  if (w === 1920 && h === 1080) return '43';
  if (w === 3840 && h === 2160) return '55';
  return 'custom';
}

/* Edit-screen modal — name, orientation, size, touch, auto-rotate.
   PUT /api/screens/:id. Was previously name + orientation + rotate
   only; size and touch added per curator feedback ("inside edit do
   we have all features needed? Its missing screen size atleast,
   touch disabled or enabled"). The backend has accepted these via
   the allowed-columns list since Phase 47 — just wasn't surfaced. */
function EditScreenModal({ open, screen, zonesList, onClose, onSaved }) {
  const [name,    setName]    = useState('');
  /* Free-text one-liner shown in the on-TV slot picker during
     self-claim. Optional — empty falls back to the bare slot name. */
  const [locHint, setLocHint] = useState('');
  const [orient,  setOrient]  = useState('landscape');
  const [rotate,  setRotate]  = useState('12');
  const [sizeId,  setSizeId]  = useState('55');
  const [touch,   setTouch]   = useState(false);
  const [zoneId,  setZoneId]  = useState('');
  /* Zones available in the site this screen belongs to. Prefer the
     parent's zonesList prop (displays.jsx already has it loaded) but
     fall back to fetching ourselves so single-screen.jsx's edit
     button still works. Filtered to same-site only — moving a screen
     across sites is a separate flow not exposed here. */
  const [zoneChoices, setZoneChoices] = useState([]);
  const [busy,    setBusy]    = useState(false);
  const [err,     setErr]     = useState(null);

  useEffect(() => {
    if (!open || !screen) return;
    setName(screen.name || '');
    setLocHint(screen.location_hint || '');
    setOrient(screen.orientation || 'landscape');
    setRotate(String(screen.auto_rotate_seconds || 12));
    setSizeId(resolvePresetId(screen));
    setTouch(!!screen.is_touch);
    setZoneId(screen.zone_id || '');
    setBusy(false); setErr(null);
  }, [open, screen]);

  /* Resolve zone choices. If the parent passed zonesList (displays
     page does this), reuse it — saves a roundtrip. Otherwise fetch
     scoped to the screen's site so the curator can move the screen
     between zones in the same site without leaving the modal. */
  useEffect(() => {
    if (!open || !screen) return;
    if (zonesList && zonesList.length) {
      // Filter to same site as the current screen. Falls back to
      // the full list if site_id isn't present on the screen.
      const siteId = screen.site_id;
      setZoneChoices(siteId ? zonesList.filter((z) => z.site_id === siteId) : zonesList);
      return;
    }
    // Fallback: fetch directly. Only fires on the single-screen
    // drilldown surface because displays.jsx always passes zonesList.
    const siteId = screen.site_id;
    if (!siteId) { setZoneChoices([]); return; }
    let cancelled = false;
    apiFetch(`/api/zones?site_id=${encodeURIComponent(siteId)}`)
      .then((r) => { if (!cancelled) setZoneChoices(r.zones || []); })
      .catch(() => { if (!cancelled) setZoneChoices([]); });
    return () => { cancelled = true; };
  }, [open, screen, zonesList]);

  useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === 'Escape') onClose && onClose(); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [open, onClose]);

  async function save(e) {
    e && e.preventDefault();
    if (!name.trim()) { setErr('Screen name is required.'); return; }
    const rot = Number(rotate);
    if (!Number.isFinite(rot) || rot < 3 || rot > 600) {
      setErr('Auto-rotate must be a number between 3 and 600 seconds.');
      return;
    }
    // Resolve the size preset → resolution. Custom leaves the field
    // alone so we don't overwrite a curator-set native pixel grid
    // with a fallback. Portrait orientation swaps the dimensions so
    // the kiosk knows the panel is rotated.
    const preset = EDIT_SIZE_PRESETS.find((p) => p.id === sizeId) || EDIT_SIZE_PRESETS[0];
    let w = preset.width;
    let h = preset.height;
    if (orient === 'portrait' && w && h && w > h) { [w, h] = [h, w]; }

    setBusy(true); setErr(null);
    try {
      await apiFetch(`/api/screens/${screen.id}`, {
        method: 'PUT',
        body: JSON.stringify({
          name: name.trim(),
          orientation: orient,
          auto_rotate_seconds: rot,
          is_touch: touch ? 1 : 0,
          // Only send resolution when the preset isn't 'custom' — a
          // curator picking Custom probably has a non-stock panel and
          // we shouldn't clobber whatever was already recorded.
          ...(preset.id !== 'custom' && w && h
            ? { resolution_width: w, resolution_height: h }
            : {}),
          // v100: also persist the exact inch size. Resolution alone is
          // ambiguous (24/32/43 are all 1080p; 55/65/75/85 all 4K), so
          // without this the picker collapsed to 43"/55" on reopen and
          // looked like it hadn't saved. Mirrors the Register modal.
          ...(preset.id !== 'custom'
            ? { display_size_inches: parseInt(preset.id, 10) }
            : {}),
          // Only send zone_id when it differs from the current value
          // AND we actually have it on the screen. Avoids a no-op
          // PATCH that would trigger a meaningless audit log row.
          ...(zoneId && zoneId !== screen.zone_id
            ? { zone_id: zoneId }
            : {}),
        }),
      });
      window.toast && window.toast.success('Screen updated');
      onSaved && onSaved();
      onClose && onClose();
    } catch (e2) {
      setErr(e2.message);
    } finally {
      setBusy(false);
    }
  }

  if (!open || !screen) return null;
  return (
    <div
      onClick={(e) => { if (e.target === e.currentTarget) onClose && onClose(); }}
      style={{
        // zIndex bumped from 220 → 1000 to match the project's modal
        // convention (see styles.css line ~248: "modals typically
        // 1000+"). Under the old 220 the underlying tile snapshots
        // and their "ONLINE" status pills bled through the modal
        // backdrop and rendered on top of the form fields.
        // Bug filed 2026-05-12 by Emma Thompson (feedback id a8a36ad3-…).
        position: 'fixed', inset: 0, zIndex: 1000,
        background: 'rgba(6, 7, 10, 0.78)', backdropFilter: 'blur(6px)',
        display: 'grid', placeItems: 'center', padding: 24,
      }}
    >
      <form
        onSubmit={save}
        style={{
          width: 'min(480px, 100%)',
          background: 'var(--aq-surface)',
          border: '1px solid var(--aq-line)',
          borderRadius: 14, boxShadow: 'var(--aq-shadow-2)',
          display: 'flex', flexDirection: 'column', maxHeight: '82vh',
        }}
      >
        <header style={{
          padding: '14px 18px', borderBottom: '1px solid var(--aq-line)',
          display: 'flex', alignItems: 'center', gap: 10,
        }}>
          <div style={{ flex: 1, display: 'flex', alignItems: 'baseline', gap: 10, minWidth: 0 }}>
            <div style={{ fontFamily: 'var(--aq-ff-display)', fontSize: 14.5, color: 'var(--aq-text)', fontWeight: 500 }}>
              Edit screen
            </div>
            {/* Screen code in the header so the curator can confirm
                they're editing the right row when they have multiple
                modals open in different tabs. Code is the licensing
                key — read-only here; new pairings happen elsewhere. */}
            {(screen.screen_code || screen.code) && (
              <div
                title="Screen code — read-only licensing key"
                style={{
                  fontFamily: 'var(--aq-ff-mono)',
                  fontSize: 11, color: 'var(--aq-text-faint)',
                  letterSpacing: '0.06em',
                  textTransform: 'uppercase',
                  overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
                }}
              >· {screen.screen_code || screen.code}</div>
            )}
          </div>
          <button type="button" className="aq-icon-btn" onClick={onClose}><Icon name="close" size={13} /></button>
        </header>

        <div style={{ overflowY: 'auto', padding: 16, display: 'flex', flexDirection: 'column', gap: 14 }}>
          <label style={{ display: 'block' }}>
            <div style={{ fontSize: 11.5, color: 'var(--aq-text-dim)', marginBottom: 5 }}>Display name</div>
            <input
              type="text" required autoFocus
              value={name} onChange={(e) => setName(e.target.value)}
              style={{
                width: '100%', padding: '8px 10px', fontSize: 13,
                background: 'var(--aq-surface-2)', border: '1px solid var(--aq-line)',
                borderRadius: 7, color: 'var(--aq-text)', font: 'inherit', outline: 0,
              }}
            />
            {/* Helper note: this name gets read aloud in tank audio
                descriptions, so it needs to be human-friendly and
                memorable rather than an internal code. */}
            <div style={{
              fontSize: 11, color: 'var(--aq-text-faint)',
              marginTop: 5, lineHeight: 1.35,
            }}>
              Make it memorable — this name is spoken in the tank's audio descriptions.
            </div>
          </label>

          {/* Location hint — optional install-time aid. Shown in the
              on-TV slot picker during self-claim so the installer
              knows which physical tank this slot belongs to without
              memorising the floor plan. Empty by default; safe to
              leave blank. */}
          <label style={{ display: 'block' }}>
            <div style={{ fontSize: 11.5, color: 'var(--aq-text-dim)', marginBottom: 5 }}>Location hint <span style={{ color: 'var(--aq-text-faint)', fontWeight: 400 }}>· optional</span></div>
            <input
              type="text"
              value={locHint} onChange={(e) => setLocHint(e.target.value)}
              placeholder="e.g. west wall, beside the entry"
              style={{
                width: '100%', padding: '8px 10px', fontSize: 13,
                background: 'var(--aq-surface-2)', border: '1px solid var(--aq-line)',
                borderRadius: 7, color: 'var(--aq-text)', font: 'inherit', outline: 0,
              }}
            />
            <div style={{
              fontSize: 11, color: 'var(--aq-text-faint)',
              marginTop: 5, lineHeight: 1.35,
            }}>
              Shown only during install — helps the installer pick the right slot from the on-TV list.
            </div>
          </label>

          {/* Screen code — read-only serial. Surfaced here so the
              curator can see + copy it (it identifies the device for
              pairing, support, etc.) but never edit it. The server
              auto-generates this from the site slug; mutating it
              would break device pairings. Rendered as a disabled
              monospace input so it visually reads as "data, not a
              control", and stays selectable for copy. */}
          {(screen.screen_code || screen.code) && (
            <label style={{ display: 'block' }}>
              <div style={{ fontSize: 11.5, color: 'var(--aq-text-dim)', marginBottom: 5 }}>Screen code</div>
              <input
                type="text" readOnly
                value={screen.screen_code || screen.code}
                onFocus={(e) => e.currentTarget.select()}
                style={{
                  width: '100%', padding: '8px 10px', fontSize: 13,
                  background: 'var(--aq-surface)', border: '1px solid var(--aq-line)',
                  borderRadius: 7, color: 'var(--aq-text-dim)',
                  fontFamily: 'var(--aq-ff-mono)', letterSpacing: '0.04em',
                  outline: 0, cursor: 'text',
                }}
              />
              <div style={{
                fontSize: 11, color: 'var(--aq-text-faint)',
                marginTop: 5, lineHeight: 1.35,
              }}>
                Serial — identifies this device for pairing and support. Not editable.
              </div>
            </label>
          )}

          {/* Zone selector — only render when we know what zones the
              curator could pick from. On the single-screen drilldown
              we fetch lazily after open; if the fetch returns empty
              (rare — at minimum the screen's own zone should appear)
              we hide the field rather than show an empty dropdown. */}
          {zoneChoices.length > 0 && (
            <div>
              <div style={{ fontSize: 11.5, color: 'var(--aq-text-dim)', marginBottom: 5 }}>Zone</div>
              <select
                value={zoneId} onChange={(e) => setZoneId(e.target.value)}
                style={{
                  width: '100%', padding: '8px 10px', fontSize: 13,
                  background: 'var(--aq-surface-2)', border: '1px solid var(--aq-line)',
                  borderRadius: 7, color: 'var(--aq-text)', font: 'inherit', outline: 0,
                }}
              >
                {/* Defensive empty entry — covers the case where the
                    screen's current zone_id isn't in the fetched list
                    (e.g. retired zone). Curator can still pick a real
                    one without losing the form state. */}
                {!zoneChoices.find((z) => z.id === zoneId) && zoneId && (
                  <option value={zoneId}>(current zone)</option>
                )}
                {zoneChoices.map((z) => (
                  <option key={z.id} value={z.id}>{z.name}</option>
                ))}
              </select>
              <div style={{ fontSize: 10.5, color: 'var(--aq-text-faint)', marginTop: 4 }}>
                Moving the screen updates its theme + zone roster on next kiosk reload.
              </div>
            </div>
          )}

          <div>
            <div style={{ fontSize: 11.5, color: 'var(--aq-text-dim)', marginBottom: 5 }}>Orientation</div>
            <div style={{ display: 'flex', gap: 6 }}>
              {[['landscape', 'Landscape'], ['portrait', 'Portrait']].map(([id, label]) => (
                <button
                  type="button" key={id}
                  onClick={() => setOrient(id)}
                  style={{
                    flex: 1, padding: '8px 10px', borderRadius: 7,
                    background: orient === id ? 'var(--aq-accent)' : 'transparent',
                    color: orient === id ? '#06090f' : 'var(--aq-text-dim)',
                    border: `1px solid ${orient === id ? 'var(--aq-accent)' : 'var(--aq-line)'}`,
                    fontSize: 12, fontWeight: 500, cursor: 'pointer',
                    fontFamily: 'inherit',
                  }}
                >{label}</button>
              ))}
            </div>
          </div>

          {/* Screen size — same preset list as the Register modal so
              swapping a panel reads the same way in both flows. The
              kiosk uses resolution to set deviceScaleFactor and
              layout; getting this right matters for legibility of
              species cards on a 75" 4K panel vs a 32" Full HD. */}
          <div>
            <div style={{ fontSize: 11.5, color: 'var(--aq-text-dim)', marginBottom: 5 }}>Screen size</div>
            <select
              value={sizeId} onChange={(e) => setSizeId(e.target.value)}
              style={{
                width: '100%', padding: '8px 10px', fontSize: 13,
                background: 'var(--aq-surface-2)', border: '1px solid var(--aq-line)',
                borderRadius: 7, color: 'var(--aq-text)', font: 'inherit', outline: 0,
              }}
            >
              {EDIT_SIZE_PRESETS.map((p) => (
                <option key={p.id} value={p.id}>{p.label}</option>
              ))}
            </select>
            <div style={{ fontSize: 10.5, color: 'var(--aq-text-faint)', marginTop: 4 }}>
              {(() => {
                const p = EDIT_SIZE_PRESETS.find((x) => x.id === sizeId);
                if (!p || !p.width) return 'Resolution: unspecified — kiosk uses 1920×1080 fallback.';
                return `Resolution: ${p.width}×${p.height} (${orient === 'portrait' ? 'portrait — dimensions swap on save' : 'landscape'})`;
              })()}
            </div>
          </div>

          {/* Touch toggle — flips is_touch on the row. Kiosk reads
              this on boot to enable/disable the touchpool tap-to-
              focus interactions. A flat toggle (not radio) because
              there are only two states and the label "Enabled /
              Disabled" reads identically to the detail-panel kv-grid. */}
          <div>
            <div style={{ fontSize: 11.5, color: 'var(--aq-text-dim)', marginBottom: 5 }}>Touch input</div>
            <div style={{ display: 'flex', gap: 6 }}>
              {[[true, 'Enabled'], [false, 'Disabled']].map(([val, label]) => (
                <button
                  type="button" key={String(val)}
                  onClick={() => setTouch(val)}
                  style={{
                    flex: 1, padding: '8px 10px', borderRadius: 7,
                    background: touch === val ? 'var(--aq-accent)' : 'transparent',
                    color: touch === val ? 'var(--aq-accent-ink, #06090f)' : 'var(--aq-text-dim)',
                    border: `1px solid ${touch === val ? 'var(--aq-accent)' : 'var(--aq-line)'}`,
                    fontSize: 12, fontWeight: 500, cursor: 'pointer',
                    fontFamily: 'inherit',
                  }}
                >{label}</button>
              ))}
            </div>
            <div style={{ fontSize: 10.5, color: 'var(--aq-text-faint)', marginTop: 4 }}>
              Enable for touchpool / interactive panels so visitors can tap species cards.
            </div>
          </div>

          <label style={{ display: 'block' }}>
            <div style={{ fontSize: 11.5, color: 'var(--aq-text-dim)', marginBottom: 5 }}>
              Auto-rotate <span style={{ color: 'var(--aq-text-faint)' }}>(seconds per slide)</span>
            </div>
            <input
              type="number" min={3} max={600} step={1}
              value={rotate} onChange={(e) => setRotate(e.target.value)}
              style={{
                width: '100%', padding: '8px 10px', fontSize: 13,
                background: 'var(--aq-surface-2)', border: '1px solid var(--aq-line)',
                borderRadius: 7, color: 'var(--aq-text)', font: 'var(--aq-ff-mono)', outline: 0,
              }}
            />
          </label>

          {/* Tank audio tour (v82). Lives at the bottom of the modal as a
              collapsible disclosure so the simple-edit flow stays compact
              for curators only renaming or rotating a screen. See
              public/cms/tank-audio-section.jsx for the component and
              src/routes/screen-voiceover.js for the backend it drives. */}
          {window.TankAudioSection && screen && screen.id && (
            <window.TankAudioSection screen={screen} />
          )}

          {err && (
            <div style={{
              padding: '8px 10px', borderRadius: 6,
              background: 'color-mix(in srgb, var(--aq-danger) 12%, transparent)',
              color: 'var(--aq-danger)', fontSize: 12,
            }}>{err}</div>
          )}
        </div>

        <footer style={{
          padding: '10px 16px', borderTop: '1px solid var(--aq-line)',
          background: 'var(--aq-surface-2)',
          display: 'flex', justifyContent: 'flex-end', gap: 8,
        }}>
          <button type="button" className="x-btn ghost" onClick={onClose}>Cancel</button>
          <button type="submit" className="x-btn" disabled={busy || !name.trim()}>
            {busy ? 'Saving…' : 'Save changes'}
          </button>
        </footer>
      </form>
    </div>
  );
}


function SingleScreenScreen({ param }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  /* Local, drag-reorderable copy of data.species. We mirror the
     server list into local state on load so we can optimistically
     reorder and PATCH sort_order on drop. */
  const [speciesOrder, setSpeciesOrder] = useState(null);
  const [dragId, setDragId] = useState(null);
  /* Modal-driven edit (replaces the legacy prompt-chain flow). */
  const [editOpen, setEditOpen] = useState(false);
  /* Live-vs-mock preview toggle. MUST live at the top of the
     component alongside the other useState calls — hoisted here
     to fix React error #310 ("rendered more hooks than during the
     previous render"). Previously this hook sat below the
     `if (loading) return …` / `if (error) return …` guards, so
     the loading render saw 5 hooks but the data-loaded render
     saw 6 — React aborts the entire screen the moment data lands.
     Hooks must be unconditional. */
  const [previewMode, setPreviewMode] = useState('live'); /* 'live' | 'mock' */

  useEffect(() => {
    if (!param) return;
    let cancelled = false;
    setLoading(true);
    apiFetch(`/api/screens/${encodeURIComponent(param)}`)
      .then((res) => {
        if (cancelled) return;
        setData(res);
        setSpeciesOrder(Array.isArray(res.species) ? res.species : []);
        setLoading(false);
      })
      .catch((err) => {
        if (cancelled) return;
        setError(err.message);
        setLoading(false);
      });
    return () => { cancelled = true; };
  }, [param]);

  if (loading) {
    return (
      <div className="scs-content">
        <div style={{ padding: 60, textAlign: 'center', color: 'var(--aq-text-faint)' }}>
          Loading screen…
        </div>
      </div>
    );
  }

  if (error || !data) {
    return (
      <div className="scs-content">
        <div className="x-err-stage" style={{ minHeight: 280 }}>
          <div className="x-err-card">
            <div className="x-err-code">Screen not found</div>
            <div className="x-err-glyph is-warn"><Icon name="alert" size={28} /></div>
            <h1 className="x-err-headline">{param || '—'}</h1>
            <p className="x-err-body">{error || 'No screen with this code.'}</p>
            <div className="x-err-actions">
              <button className="x-btn ghost" onClick={() => { window.location.hash = '#displays'; }}>
                Back to displays
              </button>
            </div>
          </div>
        </div>
      </div>
    );
  }

  const { screen, zone, site } = data;
  /* Prefer the locally-edited copy; fall back to the server payload
     while it lands. Any drag-reorder updates this list and PATCHes
     sort_order through to the API. */
  const species = speciesOrder || data.species || [];
  const theme = SCS_THEMES[zone.theme] || SCS_THEMES.reef;
  const featured = species.find((s) => s.is_featured) || species[0];

  /* Drag-reorder helpers — mirrored from FrameRow in campaign-editor
     (HTML5 native DnD). On drop we PATCH /api/screens/:id/species/:sid
     in parallel for each row and re-sync sort_order to its new index. */
  function speciesReorder(fromId, toId) {
    if (!fromId || fromId === toId) return;
    const cur = speciesOrder || [];
    const fromIdx = cur.findIndex((s) => s.id === fromId);
    const toIdx   = cur.findIndex((s) => s.id === toId);
    if (fromIdx < 0 || toIdx < 0) return;
    const next = cur.slice();
    const [moved] = next.splice(fromIdx, 1);
    next.splice(toIdx, 0, moved);
    setSpeciesOrder(next);
    /* Persist sort_order for every row (cheaper than computing the
       minimal subset, and matches FrameRow). Failures revert. */
    Promise.all(next.map((sp, i) => (
      apiFetch(`/api/screens/${screen.id}/species/${sp.id}`, {
        method: 'PATCH',
        body: JSON.stringify({ sort_order: i }),
      })
    ))).catch((err) => {
      window.toast && window.toast.danger(`Reorder failed: ${err.message}`);
      setSpeciesOrder(cur);
    });
  }
  /* Status derives from the screen's actual last_heartbeat — the real
     device's most recent ping. Audit (HIGH-32) flagged the old version
     which passed `new Date().toISOString()`, so every screen always
     rendered "just now" regardless of actual telemetry. Fall back to
     last_seen_at if last_heartbeat hasn't been recorded yet (e.g. on
     legacy pre-pairing screens, or while screens.js:261 heartbeat fix
     propagates). */
  /* Liveness audit (2026-06-12): dropped the last_seen_at fallback —
     it's the feed-fetch timestamp (snapshotter/preview-bumped), so a
     never-heartbeated screen read as alive whenever its feed was
     photographed. is_online wins when reported; heartbeat freshness
     covers rows that pre-date the flag. */
  const lastSeenIso = screen.last_heartbeat || null;
  let status = SCS_status(lastSeenIso);
  if (screen.is_online === true || screen.is_online === 1) {
    status = {
      lvl: 'online',
      label: `Online · last heartbeat ${lastSeenIso ? SCS_lastSeenLabel(lastSeenIso) : 'just now'}`,
      dot: 'var(--aq-success)',
    };
  } else if ((screen.is_online === false || screen.is_online === 0) && status.lvl === 'online') {
    /* Heartbeat looks fresh but the sweep flagged it offline — trust
       the flag; show the drop as idle rather than a healthy green. */
    status = {
      lvl: 'idle',
      label: `Idle · last heartbeat ${SCS_lastSeenLabel(lastSeenIso)}`,
      dot: 'var(--aq-warn)',
    };
  }
  const portrait = screen.orientation === 'portrait';
  /* "Live" preview mirrors the actual kiosk via /display/:code. The
     stylised SVG mode kept around as a fallback / design reference.
     previewMode hook is declared at the top of the component to
     keep hook order stable across loading/error/loaded renders. */

  return (
    <div className="scs-content">
      <header className="scs-header">
        <div className="scs-header-meta">
          <div className="scs-header-eyebrow">
            <span className="scs-id">{screen.code}</span>
            <span className="scs-divider" />
            <a
              href={zone.id ? `#zones/${zone.id}` : '#zones'}
              onClick={(e) => {
                e.preventDefault();
                window.location.hash = zone.id ? `#zones/${zone.id}` : '#zones';
              }}
              style={{ color: 'inherit', textDecoration: 'none', cursor: 'pointer' }}
            >{zone.name}</a>
            <span className="scs-divider" />
            <a
              href={site.name ? `#site/${(site.name || '').toLowerCase().replace(/\s+/g, '-')}` : '#'}
              onClick={(e) => {
                if (!site.name) { e.preventDefault(); return; }
                e.preventDefault();
                window.location.hash = `#site/${(site.name || '').toLowerCase().replace(/\s+/g, '-')}`;
              }}
              style={{ color: 'inherit', textDecoration: 'none', cursor: site.name ? 'pointer' : 'default' }}
            >{site.name}</a>
          </div>
          <h1>{screen.name}</h1>
          <div className="scs-header-status">
            <span className="scs-status-pill">
              <span className="scs-status-dot" style={{ background: status.dot }} />
              {status.label}
            </span>
            <span className="scs-meta">
              {screen.resolution.width}×{screen.resolution.height} · {portrait ? 'Portrait' : 'Landscape'} · {screen.template}
            </span>
          </div>
        </div>
        <div className="scs-header-actions">
          {Auth.canManageOrg() || Auth.isSiteManager() ? (
            <>
              <button
                className="scs-btn ghost"
                onClick={() => setEditOpen(true)}
                title="Edit name, orientation, or auto-rotate timing"
              ><Icon name="settings" size={13} />Edit</button>
              <button
                className="scs-btn ghost"
                style={{ color: 'var(--aq-danger)' }}
                onClick={async () => {
                  if (!window.confirm(`Delete "${screen.name}"? An admin can restore it.`)) return;
                  try {
                    await apiFetch(`/api/screens/${screen.id}`, { method: 'DELETE' });
                    window.location.hash = '#displays';
                  } catch (err) {
                    window.alert(`Delete failed: ${err.message}`);
                  }
                }}
              >Delete</button>
            </>
          ) : null}
          {(Auth.canManageOrg() || Auth.isSiteManager()) && (
            <button
              className="scs-btn ghost"
              onClick={async () => {
                if (!window.confirm('Request a restart? The screen reloads on its next heartbeat.')) return;
                try {
                  await apiFetch(`/api/screens/${screen.id}/restart`, { method: 'POST', body: JSON.stringify({}) });
                  window.toast && window.toast.success('Restart queued — the kiosk will reload on its next heartbeat');
                } catch (err) {
                  window.toast && window.toast.danger(`Restart failed: ${err.message}`);
                }
              }}
              title="Queue a restart on this kiosk"
            ><Icon name="alert" size={13} />Restart</button>
          )}
          <button
            className="scs-btn"
            onClick={() => window.open(`/display/preview.html?screen=${encodeURIComponent(screen.code)}`, '_blank')}
            title="Preview the kiosk at this screen's real size"
          ><Icon name="monitor" size={13} />Open kiosk</button>
        </div>
      </header>

      <section className="scs-toprow">
        <div className="scs-preview-card">
          <header className="scs-card-head">
            <div>
              <h2>Live preview</h2>
              <p>
                {previewMode === 'live'
                  ? <>Mirroring <span className="scs-mono">/display/{screen.code}</span> · auto-rotates every {screen.auto_rotate_seconds}s</>
                  : <>Stylised mock · auto-rotates every {screen.auto_rotate_seconds}s</>}
              </p>
            </div>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
              <div style={{
                display: 'flex', gap: 2, padding: 2,
                background: 'var(--aq-surface-2)',
                border: '1px solid var(--aq-line)',
                borderRadius: 6,
              }}>
                {[['live', 'Live'], ['mock', 'Mock']].map(([id, label]) => (
                  <button
                    key={id}
                    onClick={() => setPreviewMode(id)}
                    style={{
                      padding: '3px 9px', borderRadius: 4, border: 0,
                      background: previewMode === id ? 'var(--aq-surface-3, var(--aq-surface))' : 'transparent',
                      color: previewMode === id ? 'var(--aq-text)' : 'var(--aq-text-faint)',
                      fontFamily: 'var(--aq-ff-mono)', fontSize: 10,
                      letterSpacing: '0.06em', cursor: 'pointer',
                    }}
                  >{label}</button>
                ))}
              </div>
              <div className="scs-preview-tag">
                <span className="scs-status-dot" style={{ background: status.dot }} />
                {status.lvl === 'online' ? 'LIVE' : status.lvl.toUpperCase()}
              </div>
            </div>
          </header>
          <div className="scs-preview">
            <div className="scs-preview-frame">
              {previewMode === 'live' ? (
                /* Embed the actual kiosk renderer the device serves. The
                   /display/:code route is the same one a Pi/TV polls; the
                   curator sees exactly what plays in the gallery. */
                <iframe
                  src={`/display/${encodeURIComponent(screen.code)}`}
                  title={`Live preview — ${screen.code}`}
                  style={{
                    position: 'absolute', inset: 0, width: '100%', height: '100%',
                    border: 0, background: '#06090f',
                  }}
                />
              ) : (
                <div className="scs-preview-body" style={{ background: theme.grad }}>
                  <svg className="scs-preview-pattern" viewBox="0 0 600 340" preserveAspectRatio="none">
                    <path d="M0,220 Q150,180 300,210 T600,200 T900,205" stroke={theme.fg} strokeWidth="1.2" fill="none" opacity="0.4" />
                    <path d="M0,250 Q180,220 360,235 T720,230" stroke={theme.fg} strokeWidth="0.9" fill="none" opacity="0.28" />
                    <path d="M0,280 Q120,260 240,270 T480,268 T720,272" stroke={theme.fg} strokeWidth="0.7" fill="none" opacity="0.18" />
                    {[...Array(30)].map((_, i) => (
                      <circle key={i} cx={20 + i * 20} cy={40 + (i % 5) * 18} r={0.6 + (i % 3) * 0.5} fill={theme.fg} opacity={0.3 + (i % 3) * 0.12} />
                    ))}
                  </svg>
                  <div className="scs-preview-content">
                    <div className="scs-preview-eyebrow">Now playing · auto-rotation</div>
                    <div className="scs-preview-title">{featured ? featured.common_name : zone.name}</div>
                    <div className="scs-preview-sub">
                      {featured ? `${featured.scientific_name} · ${zone.name}` : 'Awaiting content'}
                    </div>
                  </div>
                </div>
              )}
            </div>
            <div className="scs-preview-footer">
              <div><span>Species pool</span><b>{species.length} in zone</b></div>
              <div><span>Auto-rotate</span><b>{screen.auto_rotate_seconds}s</b></div>
              <div><span>Touch</span><b>{screen.is_touch ? 'Enabled' : 'Disabled'}</b></div>
            </div>
          </div>
        </div>

        {/* KPI strip — most fields stub TODO until real telemetry wired */}
        <div className="scs-stats">
          <div className="scs-stat-card">
            <div className="scs-stat-label">Status</div>
            <div className="scs-stat-value is-ok" style={{ fontSize: 18, lineHeight: 1.3, textTransform: 'capitalize' }}>
              {status.lvl}
            </div>
            <div className="scs-stat-sub">{lastSeenIso ? SCS_lastSeenLabel(lastSeenIso) : 'never seen'}</div>
          </div>
          <div className="scs-stat-card">
            <div className="scs-stat-label">Brightness</div>
            <div className="scs-stat-value">—</div>
            <div className="scs-stat-sub">no telemetry yet</div>
          </div>
          <div className="scs-stat-card">
            <div className="scs-stat-label">CPU load</div>
            <div className="scs-stat-value">—</div>
            <SCS_Bar value={null} color="var(--aq-success)" />
          </div>
          <div className="scs-stat-card">
            <div className="scs-stat-label">Memory</div>
            <div className="scs-stat-value">—</div>
            <SCS_Bar value={null} color="var(--aq-success)" />
          </div>
          <div className="scs-stat-card">
            <div className="scs-stat-label">Player temp</div>
            <div className="scs-stat-value">—</div>
            <div className="scs-stat-sub">no telemetry yet</div>
          </div>
          <div className="scs-stat-card">
            <div className="scs-stat-label">Network</div>
            <div className="scs-stat-value is-ok">—</div>
            <div className="scs-stat-sub">connected</div>
          </div>
        </div>
      </section>

      {/* Hardware + species (replaces fake schedule + history) */}
      <div className="scs-bottom-row">
        <section className="scs-card">
          <header className="scs-card-head">
            <div>
              <h2>Hardware & network</h2>
              <p>Player diagnostics and identifiers</p>
            </div>
            <button className="scs-link">Run full diagnostics →</button>
          </header>
          <div className="scs-hw-grid">
            <div><span>Display name</span><b>{screen.name}</b></div>
            <div><span>Screen code</span><b className="scs-mono">{screen.code}</b></div>
            <div><span>Resolution</span><b>{screen.resolution.width}×{screen.resolution.height}</b></div>
            <div><span>Orientation</span><b style={{ textTransform: 'capitalize' }}>{screen.orientation}</b></div>
            <div><span>Template</span><b className="scs-mono">{screen.template}</b></div>
            <div><span>Touch</span><b>{screen.is_touch ? 'Enabled' : 'Disabled'}</b></div>
            <div><span>Auto-rotate</span><b>{screen.auto_rotate_seconds}s</b></div>
            <div><span>Zone</span><b>{zone.name}</b></div>
            <div><span>Site</span><b>{site.name}{site.city ? `, ${site.city}` : ''}</b></div>
            <div>
              <span>Display URL</span>
              <b className="scs-mono">
                <a
                  href={`/display/${encodeURIComponent(screen.code)}`}
                  target="_blank"
                  rel="noopener noreferrer"
                  style={{ color: 'inherit', textDecoration: 'none' }}
                  title="Open the live display in a new tab"
                >/display/{screen.code} ↗</a>
              </b>
            </div>
          </div>
        </section>

        <section className="scs-card">
          <header className="scs-card-head">
            <div>
              <h2>Species pool</h2>
              <p>{species.length} species cycling on this screen · drag to reorder</p>
            </div>
            <button
              className="scs-link"
              onClick={() => { window.location.hash = zone.id ? `#zones/${zone.id}` : '#zones'; }}
            >
              Manage zone →
            </button>
          </header>
          <ul className="scs-history">
            {species.slice(0, 8).map((sp) => (
              <li
                key={sp.id}
                className="scs-event scs-event-info"
                draggable
                onDragStart={(e) => {
                  setDragId(sp.id);
                  try { e.dataTransfer.effectAllowed = 'move'; } catch (_) {}
                }}
                onDragOver={(e) => { e.preventDefault(); try { e.dataTransfer.dropEffect = 'move'; } catch (_) {} }}
                onDrop={(e) => {
                  e.preventDefault();
                  speciesReorder(dragId, sp.id);
                  setDragId(null);
                }}
                onDragEnd={() => setDragId(null)}
                style={{
                  cursor: dragId === sp.id ? 'grabbing' : 'grab',
                  opacity: dragId === sp.id ? 0.55 : 1,
                }}
                title="Drag to reorder · click to open"
                onClick={(e) => {
                  /* Suppress click navigation if the user just dragged
                     this row — onDragEnd fires after onClick on some
                     browsers, so guard via the dragId state. */
                  if (dragId) return;
                  window.location.hash = `#species/${sp.id}`;
                }}
              >
                <span className="scs-event-mark" style={{ background: sp.is_featured ? 'var(--aq-accent)' : undefined }} />
                <span className="scs-event-time" style={{ width: 90 }}>
                  {sp.is_featured ? 'Featured' : 'In rotation'}
                </span>
                <span className="scs-event-msg">
                  <strong style={{ color: 'var(--aq-text)' }}>{sp.common_name}</strong>
                  <span style={{ color: 'var(--aq-text-faint)', fontStyle: 'italic' }}> · {sp.scientific_name}</span>
                </span>
              </li>
            ))}
            {species.length === 0 && (
              <li style={{ color: 'var(--aq-text-faint)', fontSize: 12, padding: 14 }}>
                No species assigned to this zone yet.
              </li>
            )}
          </ul>
        </section>
      </div>

      <EditScreenModal
        open={editOpen}
        screen={screen}
        onClose={() => setEditOpen(false)}
        onSaved={() => {
          /* Re-fetch the screen so the header + meta lines pick up the
             new name / orientation / rotate without a full page reload. */
          if (param) {
            apiFetch(`/api/screens/${encodeURIComponent(param)}`)
              .then((res) => setData(res))
              .catch(() => { /* surface keeps stale data — modal toast covers it */ });
          }
        }}
      />
    </div>
  );
}

window.SingleScreenScreen = SingleScreenScreen;
// Expose the edit modal so the Displays page can open it inline
// (right-hand detail panel's "Edit" button) instead of forcing
// the curator to navigate into the per-screen drilldown just to
// rename or rotate one.
window.EditScreenModal = EditScreenModal;
