// talon-admin.jsx — admin mode UI: lock toggle, POI list, editor, export.
const { useState: aUS, useRef: aUR, useEffect: aUE } = React;

const CATEGORIES = ["city", "ruin", "landmark", "character", "region"];

// ── hidden lock toggle (bottom-right) ──────────────────────────
function AdminLock({ active, onToggle }) {
  return (
    <button className={"admin-lock" + (active ? " is-active" : "")} onClick={onToggle}
            title={active ? "Exit admin (Shift+A)" : "Admin mode (Shift+A)"} aria-label="Toggle admin mode">
      {active ? (
        <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"><rect x="5" y="11" width="14" height="9" rx="2" /><path d="M8 11V7a4 4 0 0 1 8 0" /></svg>
      ) : (
        <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"><rect x="5" y="11" width="14" height="9" rx="2" /><path d="M8 11V7a4 4 0 0 1 7.9-1" /></svg>
      )}
    </button>
  );
}

// ── small field helpers ────────────────────────────────────────
function Field({ label, children, hint }) {
  return (
    <label className="adm-field">
      <span className="adm-label">{label}</span>
      {children}
      {hint && <span className="adm-hint">{hint}</span>}
    </label>
  );
}

// ── POI editor ─────────────────────────────────────────────────
function PoiEditor({ region, pois, poi, isBase, edited, placing, onChange, onPickOnMap, onDelete, onRevert, onBack }) {
  const update = (patch) => onChange({ ...poi, ...patch });
  const heroFileRef = aUR(null);

  const setBlock = (i, patch) => {
    const body = poi.body.map((b, j) => (j === i ? { ...b, ...patch } : b));
    update({ body });
  };
  const addBlock = () => update({ body: [...(poi.body || []), { h: null, p: "" }] });
  const removeBlock = (i) => update({ body: poi.body.filter((_, j) => j !== i) });
  const moveBlock = (i, dir) => {
    const body = [...poi.body]; const j = i + dir;
    if (j < 0 || j >= body.length) return;
    [body[i], body[j]] = [body[j], body[i]]; update({ body });
  };

  const onHeroFile = (e) => {
    const f = e.target.files && e.target.files[0];
    if (!f) return;
    const rd = new FileReader();
    rd.onload = () => update({ hero: rd.result });
    rd.readAsDataURL(f);
  };

  const others = pois.filter((p) => p.id !== poi.id);
  const toggleRelated = (id) => {
    const cur = poi.related || [];
    update({ related: cur.includes(id) ? cur.filter((x) => x !== id) : [...cur, id] });
  };

  return (
    <div className="adm-editor">
      <div className="adm-editor-head">
        <button className="adm-back" onClick={onBack}>
          <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 6l-6 6 6 6" /></svg>
          All points
        </button>
        <div className="adm-badges">
          {!isBase && <span className="adm-tag adm-tag-new">custom</span>}
          {isBase && edited && <span className="adm-tag adm-tag-edit">edited</span>}
        </div>
      </div>

      <div className="adm-editor-scroll">
        <Field label="Title">
          <input className="adm-input" value={poi.title || ""} onChange={(e) => update({ title: e.target.value })} />
        </Field>

        <div className="adm-grid2">
          <Field label="Category">
            <select className="adm-input adm-select" value={poi.category} onChange={(e) => update({ category: e.target.value })}>
              {CATEGORIES.map((c) => <option key={c} value={c}>{c}</option>)}
            </select>
          </Field>
          <Field label="Subtitle (optional)">
            <input className="adm-input" value={poi.subtitle || ""} placeholder="e.g. Warden of the North-East"
                   onChange={(e) => update({ subtitle: e.target.value || null })} />
          </Field>
        </div>

        <Field label="Tags" hint="comma separated">
          <input className="adm-input" value={(poi.tags || []).join(", ")}
                 onChange={(e) => update({ tags: e.target.value.split(",").map((s) => s.trim()).filter(Boolean) })} />
        </Field>

        <Field label="Coordinates" hint="0–1 fractions · or drag the marker on the map">
          <div className="adm-coords">
            <span className="adm-coord">x
              <input className="adm-input adm-num" type="number" min="0" max="1" step="0.001"
                     value={poi.coords.x}
                     onChange={(e) => update({ coords: { ...poi.coords, x: clampNum(e.target.value) } })} />
            </span>
            <span className="adm-coord">y
              <input className="adm-input adm-num" type="number" min="0" max="1" step="0.001"
                     value={poi.coords.y}
                     onChange={(e) => update({ coords: { ...poi.coords, y: clampNum(e.target.value) } })} />
            </span>
            <button className={"adm-btn adm-btn-ghost" + (placing ? " is-on" : "")} onClick={onPickOnMap}>
              {placing ? "Click the map…" : "Pick on map"}
            </button>
          </div>
        </Field>

        <div className="adm-section-label">Body</div>
        {(poi.body || []).map((blk, i) => (
          <div className="adm-block" key={i}>
            <div className="adm-block-head">
              <input className="adm-input adm-block-h" placeholder="Heading (optional)"
                     value={blk.h || ""} onChange={(e) => setBlock(i, { h: e.target.value || null })} />
              <div className="adm-block-tools">
                <button className="adm-icon" onClick={() => moveBlock(i, -1)} disabled={i === 0} aria-label="Move up">↑</button>
                <button className="adm-icon" onClick={() => moveBlock(i, 1)} disabled={i === poi.body.length - 1} aria-label="Move down">↓</button>
                <button className="adm-icon adm-icon-del" onClick={() => removeBlock(i)} disabled={poi.body.length <= 1} aria-label="Remove block">✕</button>
              </div>
            </div>
            <textarea className="adm-input adm-textarea" rows="3" placeholder="Paragraph text… (**bold**, _italic_)"
                      value={blk.p || ""} onChange={(e) => setBlock(i, { p: e.target.value })} />
          </div>
        ))}
        <button className="adm-btn adm-btn-ghost adm-add-block" onClick={addBlock}>+ Add paragraph</button>

        <Field label="Hero image (optional)" hint="paste a URL or upload — stored in your browser">
          <div className="adm-hero">
            {poi.hero ? (
              <div className="adm-hero-preview">
                <img src={poi.hero} alt="" />
                <button className="adm-btn adm-btn-ghost" onClick={() => update({ hero: null })}>Remove</button>
              </div>
            ) : (
              <div className="adm-hero-empty">No hero — a category glyph is shown instead.</div>
            )}
            <div className="adm-hero-inputs">
              <input className="adm-input" placeholder="https://…" value={typeof poi.hero === "string" && !poi.hero.startsWith("data:") ? poi.hero : ""}
                     onChange={(e) => update({ hero: e.target.value || null })} />
              <button className="adm-btn adm-btn-ghost" onClick={() => heroFileRef.current && heroFileRef.current.click()}>Upload</button>
              <input ref={heroFileRef} type="file" accept="image/*" style={{ display: "none" }} onChange={onHeroFile} />
            </div>
          </div>
        </Field>

        {others.length > 0 && (
          <>
            <div className="adm-section-label">Linked entries</div>
            <div className="adm-related">
              {others.map((p) => (
                <label key={p.id} className={"adm-related-row" + ((poi.related || []).includes(p.id) ? " is-on" : "")}>
                  <input type="checkbox" checked={(poi.related || []).includes(p.id)} onChange={() => toggleRelated(p.id)} />
                  <span>{p.title}</span>
                </label>
              ))}
            </div>
          </>
        )}
      </div>

      <div className="adm-editor-foot">
        {isBase && edited && <button className="adm-btn adm-btn-ghost" onClick={onRevert}>Revert</button>}
        <button className="adm-btn adm-btn-danger" onClick={onDelete}>{isBase ? "Hide POI" : "Delete"}</button>
        <span className="adm-saved">● Saved live</span>
      </div>
    </div>
  );
}
function clampNum(v) { const n = parseFloat(v); if (isNaN(n)) return 0; return +Math.min(1, Math.max(0, n)).toFixed(4); }

// ── Admin panel (list ⇄ editor) ────────────────────────────────
function AdminPanel({ region, pois, regions, editingId, isBaseFn, editedFn, placing,
                      onNew, onEdit, onChangePoi, onDelete, onRevert, onPickOnMap,
                      onData, dbMode, onClose,
                      onRegionCreate, onRegionUpdate, onRegionDelete }) {
  const editing = editingId ? pois.find((p) => p.id === editingId) : null;
  const [view, setView] = aUS("pois");
  const [editingRegionId, setEditingRegionId] = aUS(null);
  const editingRegion = editingRegionId ? (regions || []).find((r) => r.id === editingRegionId) : null;
  return (
    <aside className="admin-panel">
      <div className="admin-panel-head">
        <div className="admin-title">
          <span className="admin-kicker">Admin</span>
          <span className="admin-region">
            {view === "regions" ? "Regions"
             : view === "region-editor" ? (editingRegionId ? "Edit Region" : "New Region")
             : region.name}
          </span>
        </div>
        <button className="admin-exit" onClick={onClose} aria-label="Exit admin">
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><path d="M6 6l12 12M18 6L6 18" /></svg>
        </button>
      </div>

      {view === "region-editor" ? (
        <RegionEditor
          region={editingRegion}
          onCreate={onRegionCreate}
          onUpdate={onRegionUpdate}
          onDelete={onRegionDelete}
          onBack={() => setView("regions")}
        />
      ) : view === "regions" ? (
        <RegionScreen
          regions={regions || []}
          onEdit={(id) => { setEditingRegionId(id); setView("region-editor"); }}
          onCreate={() => { setEditingRegionId(null); setView("region-editor"); }}
          onDelete={onRegionDelete}
          onBack={() => setView("pois")}
        />
      ) : editing ? (
        <PoiEditor region={region} pois={pois} poi={editing}
                   isBase={isBaseFn(editing.id)} edited={editedFn(editing.id)} placing={placing}
                   onChange={onChangePoi} onPickOnMap={onPickOnMap}
                   onDelete={() => onDelete(editing)} onRevert={() => onRevert(editing)}
                   onBack={() => onEdit(null)} />
      ) : (
        <div className="admin-list-wrap">
          <button className="adm-btn adm-btn-primary admin-new" onClick={onNew}>
            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M12 5v14M5 12h14" /></svg>
            New point of interest
          </button>
          {placing && <div className="admin-placing-hint">Click anywhere on the map to drop the marker.</div>}
          <div className="admin-list">
            {pois.length === 0 && <div className="admin-empty">No points of interest yet.</div>}
            {pois.map((p) => {
              const Icon = iconFor(p.category);
              const base = isBaseFn(p.id), ed = editedFn(p.id);
              return (
                <div className="admin-row" key={p.id} onClick={() => onEdit(p.id)}>
                  <span className="admin-row-icon"><Icon size={15} /></span>
                  <span className="admin-row-title">{p.title}</span>
                  {!base && <span className="adm-tag adm-tag-new">custom</span>}
                  {base && ed && <span className="adm-tag adm-tag-edit">edited</span>}
                  <button className="admin-row-del" onClick={(e) => { e.stopPropagation(); onDelete(p); }} aria-label="Delete">
                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"><path d="M4 7h16M9 7V5h6v2M7 7l1 13h8l1-13" /></svg>
                  </button>
                </div>
              );
            })}
          </div>
          <div className="admin-foot">
            <button className="adm-btn adm-btn-ghost" onClick={onData}>Import / Export</button>
            <button className="adm-btn adm-btn-ghost" onClick={() => setView("regions")}>Regions</button>
            <span className={"db-badge db-" + (dbMode || "loading")} title="Storage engine">
              <span className="db-dot" />
              {dbMode === "sqlite" ? "SQLite" : dbMode === "fallback" ? "Local (fallback)" : "…"}
            </span>
          </div>
        </div>
      )}
    </aside>
  );
}

// ── Data modal: Import / Export (JSON · SQLite · JS) ───────────
function DataModal({ regions, overrides, activeRegion, dbMode, onImport, onClose }) {
  const [tab, setTab] = aUS("export");
  const [fmt, setFmt] = aUS("json");         // json | sqlite | js
  const [copied, setCopied] = aUS(false);
  const [importText, setImportText] = aUS("");
  const [status, setStatus] = aUS(null);     // {kind:'ok'|'err', msg}
  const fileRef = aUR(null);
  const taRef = aUR(null);

  aUE(() => {
    const h = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h);
  }, [onClose]);

  const text = fmt === "json" ? exportSnapshotJSON(regions, overrides)
             : fmt === "js" ? exportRegionsText(regions, overrides) : "";
  const filename = fmt === "json" ? "talon-map.json" : "talon-pois.js";

  const copy = () => {
    try { navigator.clipboard.writeText(text); } catch (e) { if (taRef.current) { taRef.current.select(); document.execCommand && document.execCommand("copy"); } }
    setCopied(true); setTimeout(() => setCopied(false), 1500);
  };
  const downloadText = () => {
    const blob = new Blob([text], { type: fmt === "json" ? "application/json" : "text/plain" });
    triggerDownload(blob, filename);
  };
  const downloadSqlite = async () => {
    try {
      if (window.TalonDB && window.TalonDB.isServer) {
        const res = await fetch('/api/export/sqlite');
        if (!res.ok) throw new Error('HTTP ' + res.status);
        const bytes = await res.arrayBuffer();
        triggerDownload(new Blob([bytes], { type: 'application/vnd.sqlite3' }), 'talon-map.sqlite');
      } else {
        const bytes = window.TalonDB && window.TalonDB.exportSqlite();
        if (!bytes) { setStatus({ kind: 'err', msg: 'SQLite engine unavailable in this session.' }); return; }
        triggerDownload(new Blob([bytes], { type: 'application/vnd.sqlite3' }), 'talon-map.sqlite');
      }
    } catch (e) {
      setStatus({ kind: 'err', msg: 'Export failed: ' + e.message });
    }
  };

  const applyJsonText = (raw) => {
    let parsed;
    try { parsed = JSON.parse(raw); } catch (e) { setStatus({ kind: "err", msg: "Invalid JSON: " + e.message }); return; }
    const res = importSnapshot(overrides, regions, parsed, activeRegion);
    if (res.error) { setStatus({ kind: "err", msg: res.error }); return; }
    onImport(res.overrides);
    const parts = [`Imported ${res.applied.length} region(s): ${res.applied.join(", ") || "—"}`];
    if (res.skipped.length) parts.push(`skipped: ${res.skipped.join(", ")}`);
    setStatus({ kind: "ok", msg: parts.join(" · ") });
  };

  const onFile = (e) => {
    const f = e.target.files && e.target.files[0];
    if (!f) return;
    const isSqlite = /\.(sqlite|db|sqlite3)$/i.test(f.name);
    const reader = new FileReader();
    reader.onload = async () => {
      if (isSqlite) {
        try {
          const ov = await window.TalonDB.importSqlite(reader.result);
          onImport(ov, true);
          setStatus({ kind: "ok", msg: `Loaded SQLite database "${f.name}".` });
        } catch (err) { setStatus({ kind: "err", msg: "Could not open .sqlite: " + err.message }); }
      } else {
        applyJsonText(reader.result);
      }
    };
    if (isSqlite) reader.readAsArrayBuffer(f); else reader.readAsText(f);
    e.target.value = "";
  };

  return (
    <div className="export-overlay" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="export-modal">
        <div className="export-head">
          <div>
            <h2>Import &amp; Export</h2>
            <p>Data is stored in a server-side <code>SQLite</code> database. Export as JSON or download the <code>.sqlite</code> file to move between machines.</p>
          </div>
          <button className="panel-close" onClick={onClose} aria-label="Close">
            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><path d="M6 6l12 12M18 6L6 18" /></svg>
          </button>
        </div>

        <div className="data-tabs">
          <button className={"data-tab" + (tab === "export" ? " is-on" : "")} onClick={() => { setTab("export"); setStatus(null); }}>Export</button>
          <button className={"data-tab" + (tab === "import" ? " is-on" : "")} onClick={() => { setTab("import"); setStatus(null); }}>Import</button>
        </div>

        {tab === "export" ? (
          <div className="data-body">
            <div className="seg">
              {[["json", "JSON"], ["sqlite", "SQLite file"], ["js", "talon-data.jsx"]].map(([v, label]) => (
                <button key={v} className={"seg-btn" + (fmt === v ? " is-on" : "")} onClick={() => setFmt(v)}>{label}</button>
              ))}
            </div>
            {fmt === "sqlite" ? (
              <div className="sqlite-card">
                <div className="sqlite-icon">
                  <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><ellipse cx="12" cy="5" rx="8" ry="3" /><path d="M4 5v14c0 1.66 3.58 3 8 3s8-1.34 8-3V5" /><path d="M4 12c0 1.66 3.58 3 8 3s8-1.34 8-3" /></svg>
                </div>
                <div>
                  <div className="sqlite-title">talon-map.sqlite</div>
                  <div className="sqlite-sub">A genuine SQLite database file — open it in any SQLite tool, or re-import it here / on another machine.</div>
                </div>
                <button className="adm-btn adm-btn-primary" onClick={downloadSqlite}>Download</button>
              </div>
            ) : (
              <>
                <textarea ref={taRef} className="export-text" readOnly value={text} spellCheck="false" />
                <div className="export-foot">
                  <button className="adm-btn adm-btn-primary" onClick={copy}>{copied ? "Copied ✓" : "Copy"}</button>
                  <button className="adm-btn adm-btn-ghost" onClick={downloadText}>Download {fmt === "json" ? ".json" : ".js"}</button>
                  {fmt === "js" && <span className="export-note">Paste the <code>pois</code> arrays into <code>talon-data.jsx</code> to ship permanently.</span>}
                </div>
              </>
            )}
          </div>
        ) : (
          <div className="data-body">
            <div className="import-drop" onClick={() => fileRef.current && fileRef.current.click()}>
              <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M12 16V4M7 9l5-5 5 5" /><path d="M5 16v2a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-2" /></svg>
              <div><strong>Choose a file</strong> — <code>.json</code> or <code>.sqlite</code></div>
              <input ref={fileRef} type="file" accept=".json,.sqlite,.db,.sqlite3,application/json" style={{ display: "none" }} onChange={onFile} />
            </div>
            <div className="import-or">or paste JSON</div>
            <textarea className="export-text import-text" value={importText} spellCheck="false"
                      placeholder='{ "regions": { "talon-mainland": { "pois": [ … ] } } }'
                      onChange={(e) => setImportText(e.target.value)} />
            <div className="export-foot">
              <button className="adm-btn adm-btn-primary" disabled={!importText.trim()} onClick={() => applyJsonText(importText)}>Apply JSON</button>
              <span className="export-note">Importing <strong>replaces</strong> the POIs of each region present in the data.</span>
            </div>
          </div>
        )}

        {status && <div className={"data-status " + (status.kind === "ok" ? "is-ok" : "is-err")}>{status.msg}</div>}
      </div>
    </div>
  );
}
function triggerDownload(blob, name) {
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a"); a.href = url; a.download = name; a.click();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

// ── Auth password modal ────────────────────────────────────────
function AuthModal({ onSuccess, onClose }) {
  const [password, setPassword] = aUS('');
  const [error, setError] = aUS('');
  const [loading, setLoading] = aUS(false);
  const inputRef = aUR(null);

  aUE(() => { if (inputRef.current) inputRef.current.focus(); }, []);

  aUE(() => {
    const h = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', h);
    return () => window.removeEventListener('keydown', h);
  }, [onClose]);

  const submit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError('');
    try {
      const res = await fetch('/api/auth', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ password }),
      });
      const data = await res.json();
      if (!res.ok) { setError(data.error || 'Incorrect password'); return; }
      onSuccess(data.token);
    } catch (_) {
      setError('Could not reach server.');
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="auth-backdrop" onClick={onClose}>
      <div className="auth-modal" role="dialog" aria-modal="true" aria-labelledby="auth-modal-heading" onClick={(e) => e.stopPropagation()}>
        <h3 id="auth-modal-heading" className="auth-modal-title">Admin Access</h3>
        <form onSubmit={submit} style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
          <input
            ref={inputRef}
            className="adm-input"
            type="password"
            placeholder="Password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            disabled={loading}
          />
          {error && <p className="auth-modal-error">{error}</p>}
          <button className="adm-btn adm-btn-primary auth-modal-submit" type="submit" disabled={loading || !password}>
            {loading ? 'Checking…' : 'Unlock'}
          </button>
        </form>
      </div>
    </div>
  );
}

// ── Delete confirm modal ───────────────────────────────────────
function DeleteConfirmModal({ regionName, onConfirm, onClose }) {
  aUE(() => {
    const h = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", h);
    return () => window.removeEventListener("keydown", h);
  }, [onClose]);

  return (
    <div className="adm-confirm-backdrop" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="adm-confirm-modal" role="dialog" aria-modal="true" aria-labelledby="adm-confirm-heading">
        <h3 id="adm-confirm-heading" className="adm-confirm-title">Delete region?</h3>
        <p className="adm-confirm-body">
          Permanently delete <strong>{regionName}</strong> and all its POI data? This cannot be undone.
        </p>
        <div className="adm-confirm-actions">
          <button className="adm-btn adm-btn-danger" onClick={onConfirm}>Delete</button>
          <button className="adm-btn adm-btn-ghost" onClick={onClose}>Cancel</button>
        </div>
      </div>
    </div>
  );
}

// ── First-run setup screen ────────────────────────────────
function SetupScreen({ authRequired, onComplete }) {
  const [step, setStep] = aUS(authRequired ? 'password' : 'name');
  const [password, setPassword] = aUS('');
  const [name, setName] = aUS('');
  const [error, setError] = aUS('');
  const [loading, setLoading] = aUS(false);
  const inputRef = aUR(null);
  aUE(() => { if (inputRef.current) inputRef.current.focus(); }, [step]);

  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');

  const submitPassword = async (e) => {
    e.preventDefault();
    setLoading(true); setError('');
    try {
      const res = await fetch('/api/auth', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ password }),
      });
      const data = await res.json();
      if (!res.ok) { setError(data.error || 'Incorrect password'); return; }
      window.TalonDB.setToken(data.token);
      setStep('name');
    } catch (_) {
      setError('Could not reach server.');
    } finally {
      setLoading(false);
    }
  };

  const submitName = async (e) => {
    e.preventDefault();
    if (!slug) return;
    setLoading(true); setError('');
    try {
      const region = await window.TalonDB.createRegion({
        id: slug, name: name.trim(),
        treatment: 'placeholder', image: null,
        size: { width: 1920, height: 1080 },
        initialView: { x: 0.5, y: 0.5, zoom: 1 },
        minZoom: 0.5, maxZoom: 2.5,
      });
      onComplete(region);
    } catch (err) {
      setError(err.message || 'Failed to create region.');
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="setup-screen">
      <div className="setup-card">
        {step === 'password' ? (
          <>
            <h2 className="setup-title">Admin access required</h2>
            <form onSubmit={submitPassword} style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
              <input className="adm-input" type="password" placeholder="Admin password"
                     ref={inputRef} value={password} onChange={(e) => setPassword(e.target.value)}
                     disabled={loading} />
              {error && <p className="auth-modal-error">{error}</p>}
              <button className="adm-btn adm-btn-primary" type="submit" disabled={loading || !password}>
                {loading ? 'Checking…' : 'Unlock'}
              </button>
            </form>
          </>
        ) : (
          <>
            <h2 className="setup-title">Create your first region</h2>
            <form onSubmit={submitName} style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
              <input className="adm-input" type="text" placeholder="e.g. The Northern Wastes"
                     ref={inputRef} value={name} onChange={(e) => setName(e.target.value)}
                     disabled={loading} />
              {name && <div className="setup-slug-preview">ID: {slug || '—'}</div>}
              {error && <p className="auth-modal-error">{error}</p>}
              <button className="adm-btn adm-btn-primary" type="submit" disabled={loading || !slug}>
                {loading ? 'Creating…' : 'Create map'}
              </button>
            </form>
          </>
        )}
      </div>
    </div>
  );
}

// ── Region screen ──────────────────────────────────────────────
function RegionScreen({ regions, onEdit, onCreate, onDelete, onBack }) {
  const [pendingDelete, setPendingDelete] = aUS(null);

  return (
    <div className="admin-list-wrap">
      <button className="adm-btn adm-btn-primary admin-new" onClick={onCreate}>
        <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M12 5v14M5 12h14" /></svg>
        New region
      </button>
      <div className="admin-list">
        {regions.length === 0 && <div className="admin-empty">No regions.</div>}
        {regions.map((r) => (
          <div key={r.id} className="adm-region-row">
            <div className="region-thumb"
                 style={r.treatment === "painted" && r.image ? { backgroundImage: `url(${r.image})` } : {}}
                 data-placeholder={r.treatment !== "painted" || !r.image ? "true" : undefined} />
            <div className="region-row-info">
              <div className="region-row-name">{r.name}</div>
              <div className="region-row-meta">{(r.pois || []).length} POI{(r.pois || []).length !== 1 ? "s" : ""} · {r.treatment}</div>
            </div>
            <div className="region-row-actions">
              <button className="admin-row-del" onClick={() => onEdit(r.id)} aria-label="Edit region"
                      style={{ color: "var(--stone-400)" }}>
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" /><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" /></svg>
              </button>
              <button className="admin-row-del" onClick={() => setPendingDelete({ id: r.id, name: r.name })} aria-label="Delete region">
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"><path d="M4 7h16M9 7V5h6v2M7 7l1 13h8l1-13" /></svg>
              </button>
            </div>
          </div>
        ))}
      </div>
      <div className="admin-foot">
        <button className="adm-btn adm-btn-ghost" onClick={onBack}>← Back to POIs</button>
      </div>
      {pendingDelete !== null && (
        <DeleteConfirmModal
          regionName={pendingDelete.name}
          onConfirm={() => { onDelete(pendingDelete.id); setPendingDelete(null); }}
          onClose={() => setPendingDelete(null)}
        />
      )}
    </div>
  );
}

// ── Region editor ──────────────────────────────────────────────
const REGION_DEFAULTS = {
  name: "New Region", treatment: "placeholder", image: null,
  size: { width: 1920, height: 1080 },
  initialView: { x: 0.5, y: 0.5, zoom: 1 },
  minZoom: 0.5, maxZoom: 2.5,
};

function RegionEditor({ region, onCreate, onUpdate, onDelete, onBack }) {
  const isCreate = !region;
  const [form, setForm] = aUS(() => isCreate ? { ...REGION_DEFAULTS } : {
    name: region.name, treatment: region.treatment, image: region.image,
    size: { ...region.size }, initialView: { ...region.initialView },
    minZoom: region.minZoom, maxZoom: region.maxZoom,
  });
  const [regionId, setRegionId] = aUS(isCreate ? "" : region.id);
  const [saving, setSaving] = aUS(false);
  const [error, setError] = aUS(null);
  const [confirmDelete, setConfirmDelete] = aUS(false);
  const fileRef = aUR(null);

  const update = (patch) => setForm((f) => ({ ...f, ...patch }));

  const handleCreate = async () => {
    if (!regionId.trim() || !form.name.trim()) return;
    setSaving(true); setError(null);
    try {
      await onCreate({ id: regionId.trim(), ...form });
      onBack();
    } catch (e) {
      setError(e.message || "Failed to create region.");
    } finally {
      setSaving(false);
    }
  };

  const saveField = async (patch) => {
    if (!isCreate) {
      try { await onUpdate(region.id, patch); }
      catch (e) { console.warn('[RegionEditor] update failed:', e.message); }
    }
  };

  const handleImageUpload = async (e) => {
    const file = e.target.files && e.target.files[0];
    if (!file || isCreate) return;
    try {
      const { path } = await window.TalonDB.uploadRegionImage(region.id, file);
      update({ image: path });
      await saveField({ image: path });
    } catch (e) {
      setError("Image upload failed: " + e.message);
    }
    e.target.value = "";
  };

  return (
    <div className="adm-editor">
      <div className="adm-editor-head">
        <button className="adm-back" onClick={onBack}>
          <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 6l-6 6 6 6" /></svg>
          All regions
        </button>
      </div>

      <div className="adm-editor-scroll">
        {isCreate && (
          <Field label="ID" hint="slug — a-z, 0-9, hyphens; cannot be changed later">
            <input className="adm-input" value={regionId} placeholder="e.g. northern-wastes"
                   onChange={(e) => setRegionId(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ""))} />
          </Field>
        )}

        <Field label="Name">
          <input className="adm-input" value={form.name}
                 onChange={(e) => update({ name: e.target.value })}
                 onBlur={() => saveField({ name: form.name })} />
        </Field>

        <Field label="Treatment">
          <div style={{ display: "flex", gap: "8px", marginTop: "4px" }}>
            {["painted", "placeholder"].map((t) => (
              <button key={t} className={"adm-btn adm-btn-ghost" + (form.treatment === t ? " is-on" : "")}
                      style={{ fontSize: "11px", padding: "3px 10px" }}
                      onClick={() => { update({ treatment: t }); saveField({ treatment: t }); }}>
                {t}
              </button>
            ))}
          </div>
        </Field>

        <Field label="Map image" hint={isCreate ? "Upload available after creation" : "path or upload"}>
          <div className="adm-hero">
            {form.image ? (
              <div className="adm-hero-preview">
                <img src={form.image} alt="" style={{ maxHeight: "60px", borderRadius: "4px" }} />
                <button className="adm-btn adm-btn-ghost" onClick={() => { update({ image: null }); saveField({ image: null }); }}>Remove</button>
              </div>
            ) : (
              <div className="adm-hero-empty">No image — placeholder stripes shown.</div>
            )}
            <div className="adm-hero-inputs">
              <input className="adm-input" placeholder="assets/…" value={form.image || ""}
                     onChange={(e) => update({ image: e.target.value || null })}
                     onBlur={() => saveField({ image: form.image })} />
              {!isCreate && (
                <>
                  <button className="adm-btn adm-btn-ghost" onClick={() => fileRef.current && fileRef.current.click()}>Upload</button>
                  <input ref={fileRef} type="file" accept="image/*" style={{ display: "none" }} onChange={handleImageUpload} />
                </>
              )}
            </div>
          </div>
        </Field>

        <div className="adm-section-label">Canvas size</div>
        <div className="adm-grid2">
          <Field label="Width (px)">
            <input className="adm-input adm-num" type="number" min="1" step="1" value={form.size.width}
                   onChange={(e) => update({ size: { ...form.size, width: Math.max(1, parseInt(e.target.value) || 1) } })}
                   onBlur={() => saveField({ size: form.size })} />
          </Field>
          <Field label="Height (px)">
            <input className="adm-input adm-num" type="number" min="1" step="1" value={form.size.height}
                   onChange={(e) => update({ size: { ...form.size, height: Math.max(1, parseInt(e.target.value) || 1) } })}
                   onBlur={() => saveField({ size: form.size })} />
          </Field>
        </div>

        <div className="adm-section-label">Initial view</div>
        <div className="adm-coords">
          {["x", "y", "zoom"].map((k) => (
            <span key={k} className="adm-coord">{k}
              <input className="adm-input adm-num" type="number" step="0.001"
                     min={k === "zoom" ? "0.01" : "0"} max={k === "zoom" ? undefined : "1"}
                     value={form.initialView[k]}
                     onChange={(e) => update({ initialView: { ...form.initialView, [k]: parseFloat(e.target.value) || 0 } })}
                     onBlur={() => saveField({ initialView: form.initialView })} />
            </span>
          ))}
        </div>

        <div className="adm-grid2" style={{ marginTop: "12px" }}>
          <Field label="Min zoom">
            <input className="adm-input adm-num" type="number" min="0.01" step="0.01" value={form.minZoom}
                   onChange={(e) => update({ minZoom: parseFloat(e.target.value) || 0.1 })}
                   onBlur={() => saveField({ minZoom: form.minZoom })} />
          </Field>
          <Field label="Max zoom">
            <input className="adm-input adm-num" type="number" min="0.01" step="0.01" value={form.maxZoom}
                   onChange={(e) => update({ maxZoom: parseFloat(e.target.value) || 1 })}
                   onBlur={() => saveField({ maxZoom: form.maxZoom })} />
          </Field>
        </div>

        {error && <p className="auth-modal-error" style={{ marginTop: "10px" }}>{error}</p>}
      </div>

      <div className="adm-editor-foot">
        {isCreate ? (
          <button className="adm-btn adm-btn-primary" onClick={handleCreate}
                  disabled={saving || !regionId.trim() || !form.name.trim()}>
            {saving ? "Creating…" : "Create region"}
          </button>
        ) : confirmDelete ? (
          <>
            <button className="adm-btn adm-btn-danger" onClick={() => { onDelete(region.id); onBack(); }}>Yes, delete</button>
            <button className="adm-btn adm-btn-ghost" onClick={() => setConfirmDelete(false)}>Cancel</button>
          </>
        ) : (
          <>
            <button className="adm-btn adm-btn-danger" onClick={() => setConfirmDelete(true)}>Delete region</button>
            <span className="adm-saved">● Saved live</span>
          </>
        )}
      </div>
    </div>
  );
}

Object.assign(window, { AdminLock, AdminPanel, DataModal, AuthModal, SetupScreen, DeleteConfirmModal, RegionScreen, RegionEditor, CATEGORIES });
