// Nocturnal Dog — Játékadatbázis SZERKESZTŐ
// Backend: Google Apps Script bound to a Google Sheet.
// Auth: server-side (salted iterated SHA-256), HMAC-signed session token.
const { useState, useEffect, useMemo, useRef, useCallback } = React;

// Fallback category for a brand-new game when the live list isn't known yet.
const DEFAULT_CATEGORY = "Bemelegítés";
const GROUP_SIZES = ["2-4", "5-10", "10+"];

const LS_API_URL = "nd_editor_api_url";
const SS_TOKEN   = "nd_editor_token";
const SS_USER    = "nd_editor_user";

// ───────────────────────── helpers ─────────────────────────
function getApiUrl() {
  return localStorage.getItem(LS_API_URL) || window.NDOG_API || "";
}

async function api(action, params = {}, opts = {}) {
  const url = getApiUrl();
  if (!url) throw new Error("Az API URL nincs beállítva.");
  const token = opts.token || sessionStorage.getItem(SS_TOKEN) || "";
  const body = JSON.stringify({ action, token, ...params });
  let res;
  try {
    res = await fetch(url, {
      method: "POST",
      // text/plain elkerüli a CORS preflight-et az Apps Script ellen
      headers: { "Content-Type": "text/plain;charset=utf-8" },
      body,
      redirect: "follow"
    });
  } catch (e) {
    throw new Error("Hálózati hiba — ellenőrizd az internetet és az API URL-t.");
  }
  if (!res.ok) throw new Error("Backend hiba: " + res.status);
  let data;
  try { data = await res.json(); }
  catch (e) { throw new Error("Érvénytelen válasz a backendtől (HTML-t kaptunk?). Ellenőrizd, hogy az Apps Script Web app-ként van publikálva, és hogy az URL a `/exec`-re végződik."); }
  if (!data.ok) {
    if (data.error === "invalid_token") {
      sessionStorage.removeItem(SS_TOKEN);
      sessionStorage.removeItem(SS_USER);
    }
    throw new Error(data.error || "Ismeretlen hiba");
  }
  return data;
}

function blankGame(id) {
  return {
    id: id || ("uj-jatek-" + Date.now().toString(36)),
    title: "",
    category: DEFAULT_CATEGORY,
    phase: 3,
    groupSize: "5-10",
    duration: 10,
    tags: [],
    summary: "",
    howTo: "",
    notes: ""
  };
}

function slugify(s) {
  return (s || "").toString().toLowerCase()
    .normalize("NFD").replace(/[\u0300-\u036f]/g, "")
    .replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "").slice(0, 48);
}

function timeAgo(ts) {
  if (!ts) return "";
  const s = Math.round((Date.now() - ts) / 1000);
  if (s < 5) return "épp most";
  if (s < 60) return s + " mp";
  if (s < 3600) return Math.round(s / 60) + " perc";
  return Math.round(s / 3600) + " óra";
}

function useToast() {
  const [toast, setToast] = useState(null);
  const show = useCallback((msg, kind = "success") => {
    setToast({ msg, kind, id: Date.now() });
    setTimeout(() => setToast(t => (t && t.id ? null : t)), 2800);
  }, []);
  const el = toast ? <div className={`ed-toast ${toast.kind}`}>{toast.msg}</div> : null;
  return [el, show];
}

// ───────────────────────── SETUP WIZARD (API URL) ─────────────────────────
function SetupWizard({ onDone }) {
  const [url, setUrl] = useState(getApiUrl());
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");

  async function test() {
    setErr(""); setBusy(true);
    try {
      localStorage.setItem(LS_API_URL, url.trim());
      const r = await fetch(url.trim(), {
        method: "POST",
        headers: { "Content-Type": "text/plain;charset=utf-8" },
        body: JSON.stringify({ action: "ping" })
      });
      const data = await r.json();
      if (!data.ok) throw new Error("Nem várt válasz a backendtől.");
      onDone();
    } catch (e) {
      setErr("Nem sikerült csatlakozni: " + e.message);
    } finally {
      setBusy(false);
    }
  }

  return (
    <div className="ed-lock">
      <div className="ed-lock-card" style={{ width: "min(540px, 100%)" }}>
        <div className="ed-lock-brand">
          <img src="assets/nocturnal_logo.png" alt="" />
          <span>Nocturnal Dog</span>
        </div>
        <div className="ed-lock-eyebrow"><span className="dot"></span>Telepítés · 1/2</div>
        <h1>API <em>csatlakoztatása</em></h1>
        <p className="sub">
          Az adatok a Google Sheets-ben élnek. Add meg a Sheet-hez tartozó
          Apps Script <b>Web app</b> URL-jét. (Lásd <code>apps-script.gs</code> a projektben —
          az ottani telepítési útmutató szerint <code>setup()</code> + <code>addUser()</code> +
          <i>Deploy → Web app</i>.)
        </p>
        <label>Apps Script Web app URL</label>
        <input
          type="text"
          value={url}
          onChange={e => setUrl(e.target.value)}
          placeholder="https://script.google.com/macros/s/…/exec"
          style={{
            width: "100%", background: "var(--card)", border: "1px solid var(--border)",
            color: "var(--ink)", padding: "12px 14px", borderRadius: 8,
            fontFamily: "var(--mono)", fontSize: 12, marginBottom: 14
          }}
        />
        <div className="msg">{err}</div>
        <button
          className="btn-primary"
          onClick={test}
          disabled={busy || !url.trim().startsWith("https://")}
        >
          {busy ? "Csatlakozás…" : "Csatlakozás és tovább"}
        </button>
        <div className="footnote">
          <b>Mit jelent ez?</b> Ez az URL egy Google Apps Script, ami a Sheet-eddel
          van összekötve. Ez kezeli a bejelentkezést, az olvasást és a mentést.
          A jelszavakat <i>sózott, iterált SHA-256 hash-ként</i> tárolja, a teljes
          kommunikáció HTTPS-en zajlik.
        </div>
        <div className="ed-lock-back">
          <a href="/jatekok/">← vissza a játékadatbázishoz</a>
        </div>
      </div>
    </div>
  );
}

// ───────────────────────── LOGIN ─────────────────────────
function Login({ onLogin, onReconfig }) {
  const [u, setU] = useState("");
  const [p, setP] = useState("");
  const [err, setErr] = useState("");
  const [busy, setBusy] = useState(false);
  const [attempts, setAttempts] = useState(0);
  const ref = useRef(null);
  useEffect(() => { setTimeout(() => ref.current?.focus(), 60); }, []);

  async function submit(e) {
    e.preventDefault();
    setErr(""); setBusy(true);
    try {
      const r = await api("login", { username: u.trim(), password: p });
      sessionStorage.setItem(SS_TOKEN, r.token);
      sessionStorage.setItem(SS_USER, r.username);
      onLogin(r.username);
    } catch (e) {
      const next = attempts + 1;
      setAttempts(next);
      setErr(e.message === "invalid_credentials"
        ? `Hibás felhasználónév vagy jelszó. (${next})`
        : e.message);
      setP("");
    } finally {
      setBusy(false);
    }
  }

  return (
    <div className="ed-lock">
      <div className="ed-lock-card">
        <div className="ed-lock-brand">
          <img src="assets/nocturnal_logo.png" alt="" />
          <span>Nocturnal Dog</span>
        </div>
        <div className="ed-lock-eyebrow"><span className="dot"></span>Védett terület · Szerkesztő</div>
        <h1>Belépés a <em>szerkesztőbe</em></h1>
        <p className="sub">
          Csapattagi belépés. A munkamenet ennek a fülnek a bezárásáig,
          de legfeljebb 8 óráig él.
        </p>
        <form onSubmit={submit}>
          <label>Felhasználónév</label>
          <input
            ref={ref}
            type="text"
            value={u}
            onChange={e => setU(e.target.value)}
            autoCapitalize="off"
            autoComplete="username"
            style={{
              width: "100%", background: "var(--card)", border: "1px solid var(--border)",
              color: "var(--ink)", padding: "12px 14px", borderRadius: 10,
              fontFamily: "var(--mono)", fontSize: 14, marginBottom: 14
            }}
          />
          <label>Jelszó</label>
          <input
            type="password"
            value={p}
            onChange={e => setP(e.target.value)}
            className={err ? "error" : ""}
            autoComplete="current-password"
          />
          <div className="msg">{err}</div>
          <button type="submit" className="btn-primary" disabled={busy || !u.trim() || !p}>
            {busy ? "Bejelentkezés…" : "Belépés"}
          </button>
        </form>
        <div className="footnote">
          <b>Védelem:</b> a jelszó nincs a frontend-forráskódban — a Google Sheet-hez
          tartozó Apps Script ellenőrzi, sózott SHA-256 hash alapján.
          A munkamenet HMAC-aláírt token, 8 óra lejárattal.
          {" "}<a onClick={onReconfig}>API URL újrabeállítása</a>
        </div>
        <div className="ed-lock-back">
          <a href="/jatekok/">← vissza a játékadatbázishoz</a>
        </div>
      </div>
    </div>
  );
}

// ───────────────────────── FORM PIECES ─────────────────────────
function Segmented({ value, options, onChange }) {
  return (
    <div className="ed-seg">
      {options.map(opt => {
        const v = typeof opt === "object" ? opt.value : opt;
        const l = typeof opt === "object" ? opt.label : opt;
        return (
          <button
            key={v}
            type="button"
            className={value === v ? "active" : ""}
            onClick={() => onChange(v)}
          >{l}</button>
        );
      })}
    </div>
  );
}

function TagEditor({ tags, onChange }) {
  const [val, setVal] = useState("");
  const add = () => {
    const t = val.trim().replace(/^#/, "");
    if (!t || tags.includes(t)) { setVal(""); return; }
    onChange([...tags, t]);
    setVal("");
  };
  const remove = (t) => onChange(tags.filter(x => x !== t));
  return (
    <div className="ed-tags">
      {tags.map(t => (
        <span className="ed-tag" key={t}>
          <b>#</b>{t}
          <button type="button" onClick={() => remove(t)} aria-label="töröl">×</button>
        </span>
      ))}
      <input
        type="text"
        placeholder={tags.length ? "új címke + Enter" : "címke (Enter)"}
        value={val}
        onChange={e => setVal(e.target.value)}
        onKeyDown={e => {
          if (e.key === "Enter" || e.key === ",") { e.preventDefault(); add(); }
          if (e.key === "Backspace" && !val && tags.length) onChange(tags.slice(0, -1));
        }}
        onBlur={add}
      />
    </div>
  );
}

function GameForm({ game, onChange, onDelete, onSave, dirty, saving, saveError, lastSaved, isNew, categories }) {
  if (!game) {
    return (
      <div className="ed-form ed-form-empty">
        <h2>Válassz egy játékot</h2>
        <p>Bal oldalt válaszd ki, melyik játékot szeretnéd szerkeszteni — vagy adj hozzá egy újat.</p>
      </div>
    );
  }
  const set = (k, v) => onChange({ ...game, [k]: v });
  const errs = {};
  if (!game.id?.trim()) errs.id = "Az azonosító kötelező.";
  if (!game.title?.trim()) errs.title = "A cím kötelező.";
  if (!game.summary?.trim()) errs.summary = "Egy rövid összefoglaló segíti az áttekintést.";
  if (game.duration < 1 || game.duration > 999) errs.duration = "Reális percszámot adj.";
  const hasErrors = Object.keys(errs).length > 0;

  // ⌘/Ctrl+S to save
  const onKeyDown = (e) => {
    if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "s") {
      e.preventDefault();
      if (dirty && !hasErrors && !saving) onSave();
    }
  };

  let statusEl;
  if (saveError) statusEl = <span className="ed-sync err">⚠ {saveError}</span>;
  else if (saving) statusEl = <span className="ed-sync saving"><span className="spinner"></span>Mentés…</span>;
  else if (dirty) statusEl = <span className="ed-sync dirty">○ Nem mentett változások</span>;
  else if (lastSaved) statusEl = <span className="ed-sync saved">✓ Mentve · {timeAgo(lastSaved)}</span>;
  else statusEl = <span className="ed-sync">— Nincs változás</span>;

  return (
    <div className="ed-form" onKeyDown={onKeyDown}>
      <div className="ed-form-head">
        <div>
          <div className="crumb">
            Játék <span style={{ margin: "0 6px", opacity: 0.4 }}>/</span>
            {game.category || "—"}
          </div>
          <h2>{game.title || <span style={{ color: "var(--ink-faint)", fontStyle: "italic" }}>(névtelen játék)</span>}</h2>
        </div>
        <div className="ed-form-badges">
          {isNew && <span className="ed-badge new">új · mentésre vár</span>}
          {statusEl}
          <button
            className="ed-btn primary"
            onClick={onSave}
            disabled={!dirty || hasErrors || saving}
            title={hasErrors ? "Javítsd a hibákat a mentéshez" : "Mentés (⌘/Ctrl+S)"}
          >
            {saving ? "Mentés…" : isNew ? "Létrehozás" : "Mentés"}
          </button>
        </div>
      </div>

      <div className={`ed-fld ${errs.id ? "invalid" : ""}`}>
        <label>Azonosító (id) <span className="hint" style={{ marginLeft: 6 }}>— url-kompatibilis, egyedi; átírható, a sor nem duplázódik</span></label>
        <input type="text" value={game.id} onChange={e => set("id", slugify(e.target.value))} />
        {errs.id && <div className="err">{errs.id}</div>}
      </div>

      <div className={`ed-fld ${errs.title ? "invalid" : ""}`}>
        <label>Cím *</label>
        <input type="text" value={game.title} onChange={e => set("title", e.target.value)} />
        {errs.title && <div className="err">{errs.title}</div>}
      </div>

      <div className={`ed-fld ${errs.summary ? "invalid" : ""}`}>
        <label>Egysoros összefoglaló *</label>
        <textarea
          value={game.summary}
          onChange={e => set("summary", e.target.value)}
          placeholder="1 mondat, ami egy ismeretlen játékvezetőnek elmondja a lényeget."
        />
        {errs.summary && <div className="err">{errs.summary}</div>}
      </div>

      <div className="ed-fld-row">
        <div className="ed-fld">
          <label>Kategória</label>
          <select value={game.category} onChange={e => set("category", e.target.value)}>
            {(categories && categories.length ? categories : [game.category])
              .map(c => <option key={c} value={c}>{c}</option>)}
          </select>
        </div>
        <div className="ed-fld">
          <label>Létszám</label>
          <Segmented value={game.groupSize} options={GROUP_SIZES} onChange={v => set("groupSize", v)} />
        </div>
      </div>

      <div className="ed-fld-row">
        <div className={`ed-fld ${errs.duration ? "invalid" : ""}`}>
          <label>Időtartam (perc)</label>
          <input type="number" min="1" max="999" value={game.duration}
            onChange={e => set("duration", parseInt(e.target.value, 10) || 0)} />
          {errs.duration && <div className="err">{errs.duration}</div>}
        </div>
        <div className="ed-fld">
          <label>Mikor játszd</label>
          <Segmented value={game.phase}
            options={[1,2,3,4,5].map(n => ({ value: n, label: String(n) }))}
            onChange={v => set("phase", v)} />
          <div className="hint">1 = workshop eleje, 5 = később (bemelegített csoport kell)</div>
        </div>
      </div>

      <div className="ed-fld">
        <label>Címkék</label>
        <TagEditor tags={game.tags || []} onChange={v => set("tags", v)} />
      </div>

      <div className="ed-fld">
        <label>Hogyan játszd</label>
        <textarea className="tall" value={game.howTo}
          onChange={e => set("howTo", e.target.value)}
          placeholder="A vezetői instrukció pontról-pontra." />
      </div>

      <div className="ed-fld">
        <label>Vezetői jegyzet (opcionális)</label>
        <textarea value={game.notes || ""} onChange={e => set("notes", e.target.value)}
          placeholder="Tippek, tapasztalatok, mikor működik, mikor nem." />
      </div>

      <div className="ed-form-foot">
        <button className="ed-btn danger" onClick={onDelete}>
          Játék törlése
        </button>
        <div className="spacer"></div>
        <button
          className="ed-btn primary"
          onClick={onSave}
          disabled={!dirty || hasErrors || saving}
        >
          {saving ? "Mentés…" : isNew ? "Létrehozás" : "Mentés"}
        </button>
      </div>
    </div>
  );
}

// ───────────────────────── MAIN EDITOR APP ─────────────────────────
function EditorApp({ username, onLogout, onReconfig }) {
  // `games` = last known server state (the committed truth).
  const [games, setGames] = useState([]);
  // The row currently being edited, identified by a STABLE key. For existing
  // rows this is the id at load time (rowKey); for new rows it's a synthetic
  // "new:" key so the id field can be edited freely before the first save.
  const [selectedKey, setSelectedKey] = useState(null);
  // `draft` = buffered, uncommitted edits for the selected row. null when the
  // selection is pristine (no local changes yet).
  const [draft, setDraft] = useState(null);
  const [saving, setSaving] = useState(false);
  const [saveError, setSaveError] = useState("");
  const [lastSaved, setLastSaved] = useState(null);

  const [loadState, setLoadState] = useState("loading"); // loading|ready|error
  const [loadError, setLoadError] = useState("");
  const [filterText, setFilterText] = useState("");
  const [toastEl, toast] = useToast();
  const [confirmDelete, setConfirmDelete] = useState(null);

  // New (never-saved) rows, keyed by their synthetic "new:" key -> blank game.
  const newRowsRef = useRef(new Map());

  // Stable key helpers.
  const keyOf = (g) => (newRowsRef.current.has(g.__key) ? g.__key : g.id);
  const isNewKey = (k) => typeof k === "string" && k.startsWith("new:");

  const reload = async () => {
    setLoadState("loading"); setLoadError("");
    try {
      const r = await api("list");
      setGames(r.games);
      newRowsRef.current.clear();
      setDraft(null); setSaveError(""); setLastSaved(null);
      setSelectedKey(prev => prev && r.games.some(g => g.id === prev) ? prev : (r.games[0]?.id || null));
      setLoadState("ready");
    } catch (e) {
      setLoadError(e.message);
      setLoadState("error");
    }
  };

  useEffect(() => { reload(); }, []);

  // The committed record for the current selection (server truth or new-row stub).
  const baseForKey = (k) => {
    if (k == null) return null;
    if (isNewKey(k)) return newRowsRef.current.get(k) || null;
    return games.find(g => g.id === k) || null;
  };
  const base = baseForKey(selectedKey);
  // What the form shows: the draft if editing, otherwise the committed record.
  const selected = draft || base;
  const dirty = draft != null;
  const isNew = isNewKey(selectedKey);

  // Buffer edits locally — NEVER hits the network on keystroke. For a new
  // (unsaved) row we also keep the buffer in newRowsRef so the in-progress
  // values survive switching away and back.
  const editDraft = (next) => {
    setDraft(next);
    if (isNewKey(selectedKey)) newRowsRef.current.set(selectedKey, next);
  };

  // Strip internal-only fields before sending to the backend.
  const cleanGame = (g) => { const c = { ...g }; delete c.__key; return c; };

  // Commit the buffered draft with an explicit Save.
  const saveDraft = async () => {
    if (!draft) return;
    if (!draft.id?.trim() || !draft.title?.trim()) {
      setSaveError("Hiányzó id vagy cím."); return;
    }
    setSaving(true); setSaveError("");
    try {
      if (isNew) {
        const saved = cleanGame(draft);
        await api("create", { game: saved });
        newRowsRef.current.delete(selectedKey);
        setGames(gs => [saved, ...gs]);
        setSelectedKey(saved.id); // re-key the selection to the real id
      } else {
        // Match the existing row by the stable key captured at selection time,
        // NOT by draft.id — so the id itself can be edited safely.
        const saved = cleanGame(draft);
        await api("update", { game: saved, rowKey: selectedKey });
        setGames(gs => gs.map(g => (g.id === selectedKey ? saved : g)));
        if (saved.id !== selectedKey) setSelectedKey(saved.id);
      }
      setDraft(null);
      setLastSaved(Date.now());
      toast("Mentve.");
    } catch (e) {
      if (e.message === "invalid_token") { onLogout(); return; }
      setSaveError(e.message);
      toast("Mentés sikertelen: " + e.message, "error");
    } finally {
      setSaving(false);
    }
  };

  // Selecting another row discards nothing silently: if there are unsaved
  // changes, ask the user what to do.
  const selectRow = (k) => {
    if (k === selectedKey) return;
    if (draft) {
      const go = window.confirm("Nem mentett változások vannak ezen a játékon. Elveted őket és átváltasz?");
      if (!go) return;
      if (isNew) newRowsRef.current.delete(selectedKey);
    }
    setSaveError("");
    setSelectedKey(k);
    // A new (unsaved) row carries its buffered draft; an existing row starts pristine.
    setDraft(isNewKey(k) ? (newRowsRef.current.get(k) || null) : null);
    setLastSaved(null);
  };

  const addGame = () => {
    if (draft && !window.confirm("Nem mentett változások vannak. Elveted őket és új játékot kezdesz?")) return;
    const k = "new:" + Date.now().toString(36);
    const g = { ...blankGame(), __key: k };
    newRowsRef.current.set(k, g);
    setDraft(null); setSaveError("");
    setSelectedKey(k);
    setDraft(g); // start dirty so the Save button is the only way in
    setLastSaved(null);
  };

  const askDelete = () => {
    if (selected) setConfirmDelete({ key: selectedKey, game: selected });
  };

  const reallyDelete = async () => {
    const target = confirmDelete;
    setConfirmDelete(null);
    if (!target) return;
    const { key, game: g } = target;
    // A new, never-saved row: just drop it locally.
    if (isNewKey(key)) {
      newRowsRef.current.delete(key);
      setDraft(null); setSaveError("");
      setSelectedKey(games[0]?.id || null);
      toast("Eldobva.");
      return;
    }
    try {
      await api("delete", { id: key });
      const remaining = games.filter(x => x.id !== key);
      setGames(remaining);
      setDraft(null); setSaveError("");
      setSelectedKey(remaining[0]?.id || null);
      toast(`Törölve: ${g.title || key}`);
    } catch (e) {
      toast("Törlés sikertelen: " + e.message, "error");
    }
  };

  // Sidebar list: committed games + any unsaved new rows on top.
  const listRows = useMemo(() => {
    const news = Array.from(newRowsRef.current.values());
    return [...news, ...games];
  }, [games, draft, selectedKey]);

  const filtered = useMemo(() => {
    const q = filterText.trim().toLowerCase();
    if (!q) return listRows;
    return listRows.filter(g => [g.title, g.category, g.summary, ...(g.tags || [])]
      .join(" ").toLowerCase().includes(q));
  }, [listRows, filterText]);

  // Category options for the form: distinct categories present in the live
  // data (so the editor matches whatever the sheet/import uses), plus the
  // current selection's value so it's never missing from its own dropdown.
  const categories = useMemo(() => {
    const s = new Set();
    games.forEach(g => { if (g.category) s.add(g.category); });
    if (selected?.category) s.add(selected.category);
    return Array.from(s).sort((a, b) => a.localeCompare(b, "hu"));
  }, [games, selected]);

  const pending = dirty ? 1 : 0;
  // Human-readable explanation for the header error pill. Special-case the
  // "backend not updated" situation, which otherwise shows a cryptic
  // unknown_action string.
  const errorHint = useMemo(() => {
    if (!saveError) return "";
    if (/unknown_action/i.test(saveError)) {
      return "A backend nincs frissítve — töltsd fel az új Code.js-t (clasp push vagy beillesztés).";
    }
    if (/invalid_token/i.test(saveError)) return "Lejárt a munkamenet — jelentkezz be újra.";
    return saveError;
  }, [saveError]);

  // Warn before leaving with unsaved changes.
  useEffect(() => {
    const h = (e) => {
      if (draft) { e.preventDefault(); e.returnValue = ""; }
    };
    window.addEventListener("beforeunload", h);
    return () => window.removeEventListener("beforeunload", h);
  }, [draft]);

  if (loadState === "loading") {
    return (
      <div className="ed-loading">
        <div className="spinner big"></div>
        <p>Adatok betöltése a Google Sheets-ből…</p>
      </div>
    );
  }

  if (loadState === "error") {
    return (
      <div className="ed-loading">
        <h2>Nem sikerült betölteni az adatokat</h2>
        <p style={{ color: "var(--crimson-soft)", marginBottom: 20 }}>{loadError}</p>
        <div style={{ display: "flex", gap: 8, justifyContent: "center" }}>
          <button className="ed-btn primary" onClick={reload}>↻ Újrapróbálás</button>
          <button className="ed-btn ghost" onClick={onReconfig}>API URL újrabeállítása</button>
        </div>
      </div>
    );
  }

  return (
    <div className="ed-page">
      <header className="ed-bar">
        <div className="ed-bar-inner">
          <a className="ed-brand" href="/">
            <img src="assets/nocturnal_logo.png" alt="" />
            <span>Nocturnal Dog</span>
          </a>
          <span className="ed-crumb">
            <span style={{ opacity: 0.4 }}>/</span> Játékadatbázis <span style={{ opacity: 0.4 }}>/</span>{" "}
            <span className="accent">Szerkesztő</span>
          </span>
          <div className="ed-status">
            <span className="pill"><b>{games.length}</b>játék</span>
            <span className={`pill ${pending > 0 ? "mod" : ""}`}>
              <span className={`live-dot ${pending > 0 ? "pulse" : "ok"}`}></span>
              {pending > 0 ? "nem mentett" : "szinkronban"}
            </span>
            {saveError && (
              <button
                type="button"
                className="pill del"
                title={errorHint + "  ·  Kattints az újrapróbáláshoz"}
                onClick={() => { if (dirty && !saving) saveDraft(); }}
                style={{ cursor: "pointer", maxWidth: 360, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
              >
                ⚠ {errorHint}
              </button>
            )}
          </div>
          <div className="ed-bar-actions">
            <span className="ed-user">
              <span className="dot"></span>
              {username}
            </span>
            <button className="ed-btn ghost" onClick={reload} title="Adatok újratöltése a Sheets-ből">↻ Frissítés</button>
            <button className="ed-btn ghost" onClick={onLogout} title="Kijelentkezés">⎋ Kilépés</button>
          </div>
        </div>
      </header>

      <div className="ed-main">
        <aside className="ed-sidebar">
          <div className="ed-sb-head">
            <div className="ed-sb-search">
              <span className="icon">⌕</span>
              <input
                type="text"
                placeholder="Keresés…"
                value={filterText}
                onChange={e => setFilterText(e.target.value)}
              />
            </div>
          </div>

          <div className="ed-sb-list">
            {filtered.length === 0 ? (
              <div className="ed-sb-empty">Nincs találat.</div>
            ) : filtered.map(g => {
              const rowKey = keyOf(g);
              const isSel = rowKey === selectedKey;
              let dotClass = "";
              if (isSel && saveError) dotClass = "del";
              else if (isSel && dirty) dotClass = "mod";
              else if (isNewKey(rowKey)) dotClass = "new";
              // When this row is the selected, dirty one, show the draft's
              // values so the list reflects in-progress edits.
              const view = isSel && draft ? draft : g;
              return (
                <button
                  key={rowKey}
                  className={`ed-sb-row ${isSel ? "active" : ""}`}
                  onClick={() => selectRow(rowKey)}
                >
                  <span className={`ed-sb-dot ${dotClass}`} />
                  <div className="ed-sb-meta">
                    <div className="ed-sb-title">{view.title || <i style={{ color: "var(--ink-faint)" }}>(névtelen)</i>}</div>
                    <div className="ed-sb-cat">{view.category} · {view.duration}′ · {view.groupSize}</div>
                  </div>
                  <span className="ed-sb-arrow">→</span>
                </button>
              );
            })}
          </div>

          <div className="ed-sb-add">
            <button className="ed-btn primary" onClick={addGame} style={{ width: "100%" }}>
              + Új játék hozzáadása
            </button>
          </div>
        </aside>

        <main>
          <GameForm
            game={selected}
            categories={categories}
            isNew={isNew}
            dirty={dirty}
            saving={saving}
            saveError={saveError}
            lastSaved={lastSaved}
            onChange={editDraft}
            onDelete={askDelete}
            onSave={saveDraft}
          />
        </main>
      </div>

      {confirmDelete && (
        <div className="ed-modal-back" onClick={() => setConfirmDelete(null)}>
          <div className="ed-modal" onClick={e => e.stopPropagation()} style={{ maxWidth: 460 }}>
            <div className="ed-modal-head">
              <h3>Játék törlése</h3>
              <button className="x" onClick={() => setConfirmDelete(null)}>×</button>
            </div>
            <div className="ed-modal-body">
              <p>Biztosan törlöd: <b style={{ color: "var(--ink)" }}>{confirmDelete.game?.title || confirmDelete.key}</b>?</p>
              <p className="warning">Ez azonnal eltávolítja a Google Sheets-ből. Nem visszavonható (a Sheets verziótörténetből még visszanyerhető).</p>
            </div>
            <div className="ed-modal-foot">
              <button className="ed-btn ghost" onClick={() => setConfirmDelete(null)}>Mégsem</button>
              <button className="ed-btn danger" onClick={reallyDelete}>Igen, törlés</button>
            </div>
          </div>
        </div>
      )}

      {toastEl}
    </div>
  );
}

// ───────────────────────── ROOT ─────────────────────────
function Root() {
  const [phase, setPhase] = useState(() => {
    if (!getApiUrl()) return "setup";
    if (sessionStorage.getItem(SS_TOKEN)) return "editor";
    return "login";
  });
  const [user, setUser] = useState(() => sessionStorage.getItem(SS_USER) || "");

  if (phase === "setup") {
    return <SetupWizard onDone={() => setPhase(sessionStorage.getItem(SS_TOKEN) ? "editor" : "login")} />;
  }
  if (phase === "login") {
    return <Login
      onLogin={(u) => { setUser(u); setPhase("editor"); }}
      onReconfig={() => setPhase("setup")}
    />;
  }
  return <EditorApp
    username={user}
    onLogout={() => {
      sessionStorage.removeItem(SS_TOKEN);
      sessionStorage.removeItem(SS_USER);
      setPhase("login");
    }}
    onReconfig={() => {
      sessionStorage.removeItem(SS_TOKEN);
      sessionStorage.removeItem(SS_USER);
      setPhase("setup");
    }}
  />;
}

ReactDOM.createRoot(document.getElementById("root")).render(<Root />);
