// ===================== Crafting Buch — App (mit Live-Sync) =====================

const { useState, useEffect, useMemo, useCallback, useRef } = React;

// -------- Storage --------
const LS_BOOK    = "cb_book_v2";
const LS_VIEW    = "cb_view_v1";
const LS_CAT     = "cb_cat_v1";
const LS_TWEAKS  = "cb_tweaks_v1";

function loadLS(key, fallback) {
  try { const raw = localStorage.getItem(key); if (!raw) return fallback; return JSON.parse(raw); }
  catch (e) { return fallback; }
}
function saveLS(key, val) {
  try { localStorage.setItem(key, JSON.stringify(val)); } catch (e) {}
}

function initialBook() {
  const existing = loadLS(LS_BOOK, null);
  if (existing && existing.items && existing.recipes) return existing;
  return {
    items: window.CB_ITEMS,
    categories: window.CB_CATEGORIES,
    recipes: window.CB_RECIPES,
    locations: window.CB_LOCATIONS.map(l => ({ ...l })),
    stock: window.CB_DEFAULT_STOCK,
    updatedAt: Date.now(),
  };
}

// -------- Client-seitige Op-Anwendung (spiegelt server/ops.js) --------
function applyOpToBook(book, op, by) {
  const lb = by ? { id: by.id, name: by.name, avatar: by.avatar } : null;
  switch (op.kind) {
    case "item.upsert":
      book.items[op.id] = lb ? { ...op.data, lastEditedBy: lb } : op.data;
      break;
    case "item.delete":
      delete book.items[op.id];
      delete book.stock[op.id];
      break;
    case "category.upsert": {
      const idx = book.categories.findIndex(c => c.id === op.id);
      if (idx >= 0) book.categories[idx] = op.data;
      else book.categories.push(op.data);
      break;
    }
    case "category.delete":
      book.categories = book.categories.filter(c => c.id !== op.id);
      break;
    case "recipe.upsert": {
      const d = lb ? { ...op.data, lastEditedBy: lb } : op.data;
      const idx = book.recipes.findIndex(r => r.id === op.id);
      if (idx >= 0) book.recipes[idx] = d; else book.recipes.push(d);
      break;
    }
    case "recipe.delete":
      book.recipes = book.recipes.filter(r => r.id !== op.id);
      break;
    case "location.upsert": {
      const d = lb ? { ...op.data, lastEditedBy: lb } : op.data;
      const idx = book.locations.findIndex(l => l.id === op.id);
      if (idx >= 0) book.locations[idx] = d; else book.locations.push(d);
      break;
    }
    case "location.delete":
      book.locations = book.locations.filter(l => l.id !== op.id);
      break;
    case "stock.set":
      book.stock[op.itemId] = Math.max(0, op.qty);
      break;
    case "stock.delta":
      book.stock[op.itemId] = Math.max(0, (book.stock[op.itemId] || 0) + op.delta);
      break;
    case "recipe.craft": {
      const recipe = book.recipes.find(r => r.id === op.recipeId);
      if (!recipe) break;
      const canCraft = recipe.ingredients.every(ing => (book.stock[ing.item] || 0) >= ing.qty);
      if (!canCraft) break;
      recipe.ingredients.forEach(ing => {
        book.stock[ing.item] = Math.max(0, (book.stock[ing.item] || 0) - ing.qty);
      });
      book.stock[recipe.out.item] = (book.stock[recipe.out.item] || 0) + recipe.out.qty;
      break;
    }
    case "note.upsert": {
      if (!book.notes) book.notes = [];
      const d = lb ? { ...op.data, lastEditedBy: lb } : op.data;
      const idx = book.notes.findIndex(n => n.id === op.id);
      if (idx >= 0) book.notes[idx] = d; else book.notes.push(d);
      break;
    }
    case "note.delete":
      if (!book.notes) book.notes = [];
      book.notes = book.notes.filter(n => n.id !== op.id);
      break;
    case "_undo":
      break;
    default: break;
  }
  return book;
}

function opDescription(op, book) {
  switch (op.kind) {
    case "item.upsert":    return `hat „${op.data?.name || 'Item'}" bearbeitet`;
    case "item.delete":    return `hat ein Item gelöscht`;
    case "category.upsert": return `hat Kategorie „${op.data?.name || ''}" bearbeitet`;
    case "category.delete": return `hat eine Kategorie gelöscht`;
    case "recipe.upsert":  return `hat Rezept „${op.data?.name || ''}" bearbeitet`;
    case "recipe.delete":  return `hat ein Rezept gelöscht`;
    case "location.upsert": return `hat Ort „${op.data?.name || ''}" bearbeitet`;
    case "location.delete": return `hat einen Ort gelöscht`;
    case "stock.set": {
      const item = book?.items?.[op.itemId];
      return `hat ${item?.name || op.itemId} auf ${op.qty} gesetzt`;
    }
    case "stock.delta": {
      const item = book?.items?.[op.itemId];
      return `hat ${item?.name || op.itemId} um ${op.delta > 0 ? '+' : ''}${op.delta} geändert`;
    }
    case "recipe.craft": {
      const r = book?.recipes?.find(r => r.id === op.recipeId);
      return `hat „${r?.name || 'Rezept'}" hergestellt`;
    }
    case "note.upsert":  return `hat Notiz „${op.data?.title || ''}" bearbeitet`;
    case "note.delete":  return `hat eine Notiz gelöscht`;
    case "_undo": return `hat eine Aktion rückgängig gemacht`;
    default: return `hat eine Aktion ausgeführt`;
  }
}

function hasConflict(editor, op) {
  if (!editor?.data?.id) return false;
  const id = editor.data.id;
  const type = editor.type;
  const k = op.kind;
  if (type === "item")     return (k === "item.upsert"     || k === "item.delete")     && op.id === id;
  if (type === "recipe")   return (k === "recipe.upsert"   || k === "recipe.delete")   && op.id === id;
  if (type === "location") return (k === "location.upsert" || k === "location.delete") && op.id === id;
  if (type === "category") return (k === "category.upsert" || k === "category.delete") && op.id === id;
  return false;
}

// -------- SVG symbols --------
function CatSymbol({ name, size = 22 }) {
  const s = size;
  const stroke = "currentColor";
  const sw = 1.5;
  const symbols = {
    diamond: (<g fill="none" stroke={stroke} strokeWidth={sw}><path d={`M${s/2} 3 L${s-3} ${s/2} L${s/2} ${s-3} L3 ${s/2} Z`} /><path d={`M${s/2} 7 L${s-7} ${s/2} L${s/2} ${s-7} L7 ${s/2} Z`} /></g>),
    zigzag: (<g fill="none" stroke={stroke} strokeWidth={sw} strokeLinejoin="miter"><path d={`M2 ${s-5} L${s/4} 5 L${s/2} ${s-5} L${s*3/4} 5 L${s-2} ${s-5}`} /></g>),
    arrow: (<g fill="none" stroke={stroke} strokeWidth={sw}><path d={`M3 ${s/2} L${s-5} ${s/2}`} /><path d={`M${s-9} 4 L${s-3} ${s/2} L${s-9} ${s-4}`} /></g>),
    sun: (<g fill="none" stroke={stroke} strokeWidth={sw}><circle cx={s/2} cy={s/2} r="5" />{[0,1,2,3,4,5,6,7].map(i => { const a = (i/8)*Math.PI*2; const x1=s/2+Math.cos(a)*8, y1=s/2+Math.sin(a)*8, x2=s/2+Math.cos(a)*11, y2=s/2+Math.sin(a)*11; return <line key={i} x1={x1} y1={y1} x2={x2} y2={y2} />; })}</g>),
    feather: (<g fill="none" stroke={stroke} strokeWidth={sw} strokeLinecap="round"><path d={`M4 ${s-4} L${s-5} 5`} /><path d={`M7 ${s-8} L${s-7} 3`} /><path d={`M11 ${s-11} L${s-9} 4`} /><path d={`M${s-4} 6 L${s-9} 11`} opacity="0.6" /></g>),
  };
  return (<svg width={s} height={s} viewBox={`0 0 ${s} ${s}`} className="sym">{symbols[name] || symbols.diamond}</svg>);
}
window.CatSymbol = CatSymbol;

function ItemIcon({ item, size = 64 }) {
  if (!item) return null;
  return (<div className="ing-glyph" style={{ fontSize: size > 50 ? 18 : 13 }}><div style={{ width: size > 50 ? 28 : 22, height: 4, background: item.color, margin: "0 auto 4px", opacity: 0.85 }} />{item.glyph}</div>);
}

function ItemBadge({ item, size = 40 }) {
  if (!item) return (<div className="inv-icon" style={{ width: size, height: size, opacity: 0.4 }}><div style={{ fontFamily: "Special Elite, monospace", fontSize: 10 }}>?</div></div>);
  return (<div className="inv-icon" style={{ width: size, height: size }}><div style={{ textAlign: "center" }}><div style={{ width: size * 0.55, height: 3, background: item.color, margin: "0 auto 2px" }} /><div style={{ fontSize: 11, lineHeight: 1, letterSpacing: "0.04em" }}>{item.glyph}</div></div></div>);
}

const SearchIcon = () => (<svg className="icon" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5"><circle cx="6" cy="6" r="4.5" /><path d="M9.5 9.5 L13 13" /></svg>);
const EditIcon = () => (<svg width="13" height="13" viewBox="0 0 13 13" fill="none" stroke="currentColor" strokeWidth="1.4"><path d="M1 12 L1 9 L9 1 L12 4 L4 12 Z" /><path d="M7 3 L10 6" /></svg>);

const Compass = ({ size = 56 }) => (
  <svg className="compass" viewBox="0 0 56 56" fill="none" width={size} height={size}>
    <circle cx="28" cy="28" r="26" stroke="currentColor" strokeWidth="1" />
    <circle cx="28" cy="28" r="20" stroke="currentColor" strokeWidth="0.5" opacity="0.5" />
    <path d="M28 4 L32 28 L28 26 L24 28 Z" fill="currentColor" />
    <path d="M28 52 L24 28 L28 30 L32 28 Z" fill="currentColor" opacity="0.4" />
    <text x="28" y="14" fontSize="6" textAnchor="middle" fill="currentColor" fontFamily="IM Fell English SC">N</text>
    <text x="28" y="48" fontSize="6" textAnchor="middle" fill="currentColor" fontFamily="IM Fell English SC">S</text>
    <text x="48" y="30" fontSize="6" textAnchor="middle" fill="currentColor" fontFamily="IM Fell English SC">O</text>
    <text x="8"  y="30" fontSize="6" textAnchor="middle" fill="currentColor" fontFamily="IM Fell English SC">W</text>
  </svg>
);

// -------- Relative Zeit --------
function RelativeTime({ ts }) {
  const [label, setLabel] = useState("");
  useEffect(() => {
    const update = () => {
      const d = Date.now() - ts;
      if (d < 60000) setLabel("gerade eben");
      else if (d < 3600000) setLabel(`vor ${Math.floor(d/60000)} Min.`);
      else if (d < 86400000) setLabel(`vor ${Math.floor(d/3600000)} Std.`);
      else setLabel(`vor ${Math.floor(d/86400000)} T.`);
    };
    update();
    const id = setInterval(update, 30000);
    return () => clearInterval(id);
  }, [ts]);
  return <span>{label}</span>;
}

// -------- Login-Screen --------
function LoginScreen({ loading }) {
  return (
    <div className="login-screen">
      <div className="login-card">
        <img
          src="/logo.png"
          alt="Indianer Sachen Machen"
          className="login-logo"
        />
        <h2 style={{ fontFamily: "IM Fell English SC, serif", fontSize: 26, color: "#efe2c2", margin: "0 0 8px" }}>
          Das Crafting-Buch
        </h2>
        <p style={{ fontFamily: "Crimson Pro, serif", fontSize: 16, color: "rgba(239,226,194,0.65)", marginBottom: 32 }}>
          Frontier RP · Nur für Crew-Mitglieder
        </p>
        {loading ? (
          <div style={{ fontFamily: "Special Elite, monospace", fontSize: 12, color: "rgba(239,226,194,0.5)", letterSpacing: "0.2em" }}>
            WIRD GELADEN…
          </div>
        ) : (
          <a href="/auth/discord" className="discord-btn">
            <svg width="22" height="16" viewBox="0 0 71 55" fill="white" style={{ flexShrink: 0 }}>
              <path d="M60.1 4.9A58.5 58.5 0 0 0 45.6.4a40.6 40.6 0 0 0-1.8 3.7 54.2 54.2 0 0 0-16.3 0A40.7 40.7 0 0 0 25.7.4 58.4 58.4 0 0 0 11.1 5C1.6 19.2-1 33 .3 46.6a58.8 58.8 0 0 0 18 9.1 44 44 0 0 0 3.8-6.2 38.3 38.3 0 0 1-6-2.9c.5-.4 1-.7 1.5-1.1a41.9 41.9 0 0 0 35.9 0c.5.4 1 .8 1.5 1.1a38.4 38.4 0 0 1-6 2.9 43.9 43.9 0 0 0 3.8 6.2 58.7 58.7 0 0 0 18-9 58.6 58.6 0 0 0-8.7-40.8ZM23.7 38.4c-3.5 0-6.4-3.2-6.4-7.2s2.8-7.2 6.4-7.2 6.5 3.2 6.4 7.2c0 4-2.8 7.2-6.4 7.2Zm23.6 0c-3.5 0-6.4-3.2-6.4-7.2s2.8-7.2 6.4-7.2 6.5 3.2 6.4 7.2c0 4-2.8 7.2-6.4 7.2Z"/>
            </svg>
            Mit Discord anmelden
          </a>
        )}
      </div>
    </div>
  );
}

// -------- Verbindungsstatus --------
const STATUS_CFG = {
  connected:    { color: "#5ba35b", label: "Live" },
  connecting:   { color: "#c8842a", label: "Verbinde…" },
  reconnecting: { color: "#c8842a", label: "Verbinde…" },
  disconnected: { color: "#a83020", label: "Offline" },
};

function ConnectionStatus({ status, queueLength }) {
  const cfg = STATUS_CFG[status] || STATUS_CFG.disconnected;
  return (
    <div className="conn-status" title={status}>
      <span className="conn-dot" style={{ background: cfg.color }}></span>
      <span className="conn-label">{cfg.label}</span>
      {queueLength > 0 && (
        <span className="conn-queue" title={`${queueLength} Aktion(en) in der Warteschlange`}>
          ({queueLength})
        </span>
      )}
    </div>
  );
}

// -------- Online-Avatare --------
function OnlineAvatars({ online }) {
  if (!online || online.length === 0) return null;
  return (
    <div className="online-avatars">
      {online.slice(0, 8).map(u => (
        <div key={u.id} className={"avatar-wrap " + (u.status === "idle" ? "idle" : "active")}
          title={`${u.name}${u.status === "idle" ? " (inaktiv)" : ""}`}>
          {u.avatar
            ? <img src={u.avatar} alt={u.name} className="avatar-img" />
            : <div className="avatar-fallback">{u.name[0]?.toUpperCase()}</div>
          }
        </div>
      ))}
      {online.length > 8 && (
        <div className="avatar-wrap" title={`+${online.length - 8} weitere`}>
          <div className="avatar-fallback">+{online.length - 8}</div>
        </div>
      )}
    </div>
  );
}

// -------- Aktivitäten-Sidebar --------
function ActivitySidebar({ open, onClose, events, book, onUndo, authUserId }) {
  return (
    <div className={"activity-sidebar " + (open ? "open" : "")}>
      <div className="activity-header">
        <span>Aktivitäten</span>
        <button className="modal-close" onClick={onClose}>×</button>
      </div>
      <div className="activity-list">
        {events.length === 0 && (
          <div className="activity-empty">Noch keine Aktivitäten.</div>
        )}
        {events.map(env => {
          const isOwn = env.by?.id === authUserId;
          const isRecent = Date.now() - env.at < 5 * 60 * 1000;
          return (
            <div key={env.opId} className="activity-item">
              <div className="activity-avatar">
                {env.by?.avatar
                  ? <img src={env.by.avatar} alt={env.by.name} className="avatar-img" />
                  : <div className="avatar-fallback">{env.by?.name?.[0]?.toUpperCase() || "?"}</div>
                }
              </div>
              <div className="activity-content">
                <span className="activity-name">{env.by?.name || "?"}</span>
                {" "}
                <span className="activity-desc">{opDescription(env.op, book)}</span>
                <div className="activity-time"><RelativeTime ts={env.at} /></div>
              </div>
              {isOwn && isRecent && (
                <button className="btn ghost activity-undo"
                  onClick={() => onUndo(env.opId)}
                  title="Rückgängig">
                  ↩
                </button>
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ===================== RECIPES VIEW =====================
function RecipesView({ book, setStock, openEditor, dispatchOp }) {
  const { items, categories, recipes, stock } = book;
  const [search, setSearch] = useState("");
  const [activeCat, setActiveCat] = useState(() => loadLS(LS_CAT, categories[0]?.id || "gerben"));
  const [onlyCraftable, setOnlyCraftable] = useState(false);

  useEffect(() => { saveLS(LS_CAT, activeCat); }, [activeCat]);
  useEffect(() => {
    if (!categories.find(c => c.id === activeCat) && categories.length) setActiveCat(categories[0].id);
  }, [categories, activeCat]);

  const catCounts = useMemo(() => {
    const c = {};
    categories.forEach(cat => { c[cat.id] = recipes.filter(r => r.cat === cat.id).length; });
    return c;
  }, [recipes, categories]);

  const filtered = useMemo(() => {
    let list = recipes.filter(r => r.cat === activeCat);
    if (search.trim()) {
      const q = search.toLowerCase();
      list = list.filter(r => r.name.toLowerCase().includes(q) || r.ingredients.some(ing => items[ing.item]?.name.toLowerCase().includes(q)));
    }
    if (onlyCraftable) list = list.filter(r => r.ingredients.every(ing => (stock[ing.item] || 0) >= ing.qty));
    return list;
  }, [recipes, items, activeCat, search, onlyCraftable, stock]);

  const ingStatus = (ing) => {
    const have = stock[ing.item] || 0;
    if (have >= ing.qty) return "have";
    if (have > 0) return "partial";
    return "short";
  };

  const isCraftable = (r) => r.ingredients.every(ing => (stock[ing.item] || 0) >= ing.qty);

  const craft = (r) => {
    const preview = r.ingredients.map(ing => {
      const have = stock[ing.item] || 0;
      const after = have - ing.qty;
      return `${items[ing.item]?.name || ing.item}: ${have} → ${after}`;
    }).join("\n");
    const outItem = items[r.out.item];
    const outAfter = (stock[r.out.item] || 0) + r.out.qty;
    const msg = `${r.name} herstellen?\n\nVerbrauch:\n${preview}\n\nErtrag: ${outItem?.name || r.out.item}: +${r.out.qty} (→ ${outAfter})`;
    if (!window.confirm(msg)) return;
    if (dispatchOp) {
      dispatchOp({ kind: "recipe.craft", recipeId: r.id });
    } else {
      const newStock = { ...stock };
      r.ingredients.forEach(ing => { newStock[ing.item] = Math.max(0, (newStock[ing.item] || 0) - ing.qty); });
      newStock[r.out.item] = (newStock[r.out.item] || 0) + r.out.qty;
      setStock(newStock);
    }
  };

  const activeCategoryObj = categories.find(c => c.id === activeCat);

  return (
    <React.Fragment>
      <div className="page left">
        <div className="page-eyebrow">Kapitel I</div>
        <h2 className="page-heading">Kategorien</h2>
        <div className="page-rule"><span className="line"></span><span className="glyph">◆ ◆ ◆</span><span className="line"></span></div>

        <div className="cat-search">
          <SearchIcon />
          <input type="text" placeholder="Suche Rezept oder Zutat…" value={search} onChange={e => setSearch(e.target.value)} />
        </div>

        <div className="cat-toggle" onClick={() => setOnlyCraftable(!onlyCraftable)}>
          <div className={"box " + (onlyCraftable ? "on" : "")}></div>
          <span>Nur Herstellbar</span>
          <span style={{ marginLeft: "auto", opacity: 0.5 }}>(F)</span>
        </div>

        <ul className="cat-list">
          {categories.map(cat => (
            <li key={cat.id} className={"cat-item " + (cat.id === activeCat ? "active" : "")} onClick={() => setActiveCat(cat.id)}>
              <CatSymbol name={cat.symbol} />
              <span>{cat.name}</span>
              <span className="row-actions">
                <button onClick={e => { e.stopPropagation(); openEditor({ type: "category", data: cat }); }} title="Kategorie bearbeiten"><EditIcon /></button>
                <button onClick={e => { e.stopPropagation(); if (window.confirm(`Kategorie „${cat.name}" löschen?`)) dispatchOp({ kind: "category.delete", id: cat.id }); }} title="löschen" style={{ color: "var(--bad)" }}>✕</button>
              </span>
              <span className="count">{catCounts[cat.id]}</span>
            </li>
          ))}
        </ul>

        <button className="btn-add-row" style={{ width: "100%", marginTop: 10 }} onClick={() => openEditor({ type: "category", data: null })}>
          + Neue Kategorie
        </button>

        <div style={{ marginTop: 28 }}>
          <div className="ornament"><span className="line"></span><span>Legende</span><span className="line"></span></div>
          <div style={{ display: "flex", flexDirection: "column", gap: 8, fontFamily: "Special Elite, monospace", fontSize: 11, letterSpacing: "0.1em", color: "var(--ink-soft)" }}>
            <div style={{ display: "flex", gap: 8, alignItems: "center" }}><span style={{ width: 14, height: 14, background: "var(--ok-bg)", border: "1px solid var(--ok)" }}></span><span>VORHANDEN</span></div>
            <div style={{ display: "flex", gap: 8, alignItems: "center" }}><span style={{ width: 14, height: 14, background: "var(--warn-bg)", border: "1px solid var(--warn)" }}></span><span>TEILWEISE</span></div>
            <div style={{ display: "flex", gap: 8, alignItems: "center" }}><span style={{ width: 14, height: 14, background: "var(--bad-bg)", border: "1px solid var(--bad)" }}></span><span>FEHLT</span></div>
          </div>
        </div>
      </div>

      <div className="page right">
        <div className="page-eyebrow">{activeCategoryObj?.name || ""} · {filtered.length} Rezepte</div>
        <h2 className="page-heading">{activeCategoryObj?.name || "Rezepte"}</h2>
        <div className="page-rule"><span className="line"></span><span className="glyph">◇ ◇ ◇</span><span className="line"></span></div>

        <div className="section-actions">
          <div style={{ fontFamily: "Special Elite, monospace", fontSize: 11, color: "var(--ink-fade)", letterSpacing: "0.18em", textTransform: "uppercase" }}>
            {filtered.filter(r => isCraftable(r)).length} sofort herstellbar
          </div>
          <button className="btn" onClick={() => openEditor({ type: "recipe", data: null, defaults: { cat: activeCat } })}>+ Neues Rezept</button>
        </div>

        <div className="recipe-list">
          {filtered.length === 0 && <div className="empty">Keine Rezepte gefunden — leg eins an.</div>}
          {filtered.map(r => {
            const craftable = isCraftable(r);
            const outItem = items[r.out.item];
            return (
              <div key={r.id} className="recipe">
                {craftable && <div className="craftable-pill">Herstellbar</div>}
                <div className="recipe-head">
                  <div className="out-icon"><ItemBadge item={outItem} size={36} /></div>
                  <div className="recipe-name">{r.name}</div>
                  <div className="recipe-meta">
                    <span>{r.station}</span><span>{r.time}</span>
                    <span className="yield">+{r.out.qty}</span>
                  </div>
                  <span className="row-actions">
                    <button onClick={() => openEditor({ type: "recipe", data: r })} title="bearbeiten"><EditIcon /></button>
                    <button onClick={e => { e.stopPropagation(); if (window.confirm(`Rezept „${r.name}" löschen?`)) dispatchOp({ kind: "recipe.delete", id: r.id }); }} title="löschen" style={{ color: "var(--bad)" }}>✕</button>
                  </span>
                </div>
                {r.lastEditedBy && (
                  <div className="last-edited-by">
                    {r.lastEditedBy.avatar && <img src={r.lastEditedBy.avatar} alt="" className="avatar-img" style={{ width: 14, height: 14, borderRadius: "50%", marginRight: 4 }} />}
                    zuletzt: {r.lastEditedBy.name}
                  </div>
                )}
                <div className="recipe-body">
                  {r.ingredients.map((ing, idx) => {
                    const status = ingStatus(ing);
                    const have = stock[ing.item] || 0;
                    const item = items[ing.item];
                    return (
                      <div key={idx} className={"ing " + status}>
                        <div className="ing-badge">{ing.qty}<span style={{ opacity: 0.5 }}> | </span>{have}</div>
                        <div className="ing-slot"><ItemIcon item={item} /></div>
                        <div className="ing-name">{item?.name || "?"}</div>
                      </div>
                    );
                  })}
                  <div style={{ marginLeft: "auto", display: "flex", alignItems: "center", gap: 10 }}>
                    <span className="recipe-arrow">══▶</span>
                    <button className="btn" disabled={!craftable}
                      style={{ opacity: craftable ? 1 : 0.4, cursor: craftable ? "pointer" : "not-allowed", background: craftable ? "var(--terracotta)" : "var(--leather-deep)", borderColor: craftable ? "var(--terracotta-dim)" : "var(--leather-deep)" }}
                      onClick={() => craftable && craft(r)}>
                      Herstellen
                    </button>
                  </div>
                </div>
              </div>
            );
          })}
        </div>
      </div>
    </React.Fragment>
  );
}

// ===================== INVENTORY VIEW =====================
function InventoryView({ book, setStock, openEditor }) {
  const { items, stock } = book;
  const [search, setSearch] = useState("");
  const [filter, setFilter] = useState("alle");

  const itemGroups = useMemo(() => {
    const groups = { "Rohmaterial": [], "Verarbeitet": [], "Hilfsstoffe": [], "Produkte": [] };
    Object.entries(items).forEach(([id, item]) => {
      const g = item.group || "Rohmaterial";
      if (!groups[g]) groups[g] = [];
      groups[g].push(id);
    });
    Object.keys(groups).forEach(g => groups[g].sort((a, b) => items[a].name.localeCompare(items[b].name)));
    return groups;
  }, [items]);

  const adjust = (id, delta) => {
    const next = Math.max(0, (stock[id] || 0) + delta);
    setStock({ ...stock, [id]: next });
  };

  const set = (id, val) => {
    const n = parseInt(val, 10);
    setStock({ ...stock, [id]: isNaN(n) ? 0 : Math.max(0, n) });
  };

  const renderRow = (id) => {
    const qty = stock[id] || 0;
    const item = items[id];
    if (!item) return null;
    const q = search.trim().toLowerCase();
    if (q && !item.name.toLowerCase().includes(q)) return null;
    const threshold = item.minStock || 5;
    let level = qty === 0 ? "low" : qty < threshold ? "mid" : "full";
    if (filter === "leer" && qty > 0) return null;
    if (filter === "niedrig" && qty >= 5) return null;
    return (
      <div key={id} className={"inv-row " + level}>
        <ItemBadge item={item} size={40} />
        <div className="inv-name">{item.name}<span className="sub">{item.glyph}</span></div>
        {item.lastEditedBy && (
          <div className="last-edited-by" style={{ marginRight: 8, fontSize: 10 }}>
            {item.lastEditedBy.avatar && <img src={item.lastEditedBy.avatar} alt="" style={{ width: 12, height: 12, borderRadius: "50%", marginRight: 3, verticalAlign: "middle" }} />}
            {item.lastEditedBy.name}
          </div>
        )}
        <div style={{ display: "flex", gap: 6, alignItems: "center" }}>
          <div className="inv-qty">
            <button className="qty-btn" onClick={() => adjust(id, -1)}>−</button>
            <input className="qty-input" type="number" value={qty} min="0" onChange={e => set(id, e.target.value)} />
            <button className="qty-btn" onClick={() => adjust(id, 1)}>+</button>
          </div>
          <span className="row-actions">
            <button onClick={() => openEditor({ type: "item", data: { ...item, id } })} title="bearbeiten"><EditIcon /></button>
          </span>
        </div>
      </div>
    );
  };

  const total = Object.values(stock).reduce((a, b) => a + (b || 0), 0);
  const lowCount = Object.keys(items).filter(id => { const t = items[id]?.minStock || 5; return (stock[id] || 0) < t && (stock[id] || 0) > 0; }).length;
  const emptyCount = Object.keys(items).filter(id => (stock[id] || 0) === 0).length;
  const groupKeys = Object.keys(itemGroups);
  const half = Math.ceil(groupKeys.length / 2);

  const renderGroup = (name) => (
    <div key={name} style={{ marginBottom: 22 }}>
      <div className="ornament"><span className="line"></span><span>{name}</span><span className="line"></span></div>
      <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
        {itemGroups[name].length === 0 ? <div className="empty" style={{ padding: 14 }}>— leer —</div> : itemGroups[name].map(id => renderRow(id))}
      </div>
    </div>
  );

  return (
    <React.Fragment>
      <div className="page left">
        <div className="page-eyebrow">Kapitel II</div>
        <h2 className="page-heading">Lagerbestand</h2>
        <div className="page-rule"><span className="line"></span><span className="glyph">⌘ ⌘ ⌘</span><span className="line"></span></div>

        <div className="cat-search"><SearchIcon /><input type="text" placeholder="Item suchen…" value={search} onChange={e => setSearch(e.target.value)} /></div>

        <div className="summary-bar" style={{ flexDirection: "column", alignItems: "stretch" }}>
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
            <div className="stat"><span className="label">Gesamt-Items</span><span className="val">{total}</span></div>
            <div className="divider"></div>
            <div className="stat"><span className="label">Niedrig</span><span className="val accent">{lowCount}</span></div>
            <div className="divider"></div>
            <div className="stat"><span className="label">Leer</span><span className="val" style={{ color: "var(--bad)" }}>{emptyCount}</span></div>
          </div>
        </div>

        <div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
          {[{ id: "alle", label: "Alle" }, { id: "niedrig", label: "Niedrig" }, { id: "leer", label: "Leer" }].map(f => (
            <button key={f.id} className={"btn " + (filter === f.id ? "" : "ghost")} style={{ flex: 1 }} onClick={() => setFilter(f.id)}>{f.label}</button>
          ))}
        </div>

        <button className="btn-add-row" style={{ width: "100%", marginBottom: 18 }} onClick={() => openEditor({ type: "item", data: null })}>+ Neues Item</button>
        {groupKeys.slice(0, half).map(renderGroup)}
      </div>

      <div className="page right">
        <div className="page-eyebrow">Bestände nach Kategorie</div>
        <h2 className="page-heading">Vorratskammer</h2>
        <div className="page-rule"><span className="line"></span><span className="glyph">◇ ◇ ◇</span><span className="line"></span></div>
        {groupKeys.slice(half).map(renderGroup)}
      </div>
    </React.Fragment>
  );
}

// ===================== LOCATIONS VIEW =====================
function NotesView({ book, openEditor, dispatchOp, currentUser }) {
  const notes = book.notes || [];
  const [editingId, setEditingId] = useState(null);
  const [newTitle, setNewTitle] = useState("");
  const [newText, setNewText]   = useState("");
  const [editText, setEditText] = useState("");

  const startNew = () => {
    setEditingId("__new__");
    setNewTitle("");
    setNewText("");
  };

  const saveNew = () => {
    if (!newText.trim() && !newTitle.trim()) { setEditingId(null); return; }
    dispatchOp({ kind: "note.upsert", id: crypto.randomUUID(), data: { title: newTitle.trim(), text: newText.trim(), at: Date.now() } });
    setEditingId(null);
  };

  const startEdit = (note) => {
    setEditingId(note.id);
    setEditText(note.text);
  };

  const saveEdit = (note) => {
    dispatchOp({ kind: "note.upsert", id: note.id, data: { ...note, text: editText, at: Date.now() } });
    setEditingId(null);
  };

  const del = (note) => {
    if (window.confirm(`Notiz „${note.title || '…'}" löschen?`)) dispatchOp({ kind: "note.delete", id: note.id });
  };

  const fmt = (ts) => {
    if (!ts) return "";
    const d = new Date(ts);
    return d.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" }) + " · " +
           d.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
  };

  return (
    <React.Fragment>
      <div className="page left">
        <div className="page-eyebrow">Kapitel IV</div>
        <h2 className="page-heading">Notizen</h2>
        <div className="page-rule"><span className="line"></span><span className="glyph">✦ ✦ ✦</span><span className="line"></span></div>

        <button className="btn-add-row" style={{ width: "100%", marginBottom: 18 }} onClick={startNew}>+ Neue Notiz</button>

        {editingId === "__new__" && (
          <div className="note-card note-editing">
            <input className="note-title-input" placeholder="Titel (optional)" value={newTitle} onChange={e => setNewTitle(e.target.value)} />
            <textarea className="note-textarea" placeholder="Notiz schreiben…" value={newText} onChange={e => setNewText(e.target.value)} rows={5} autoFocus />
            <div className="note-actions">
              <button className="btn ghost" onClick={() => setEditingId(null)}>Abbrechen</button>
              <button className="btn" onClick={saveNew}>Speichern</button>
            </div>
          </div>
        )}

        {notes.length === 0 && editingId !== "__new__" && (
          <div className="empty">Noch keine Notizen — schreib die erste.</div>
        )}

        {notes.slice(0, Math.ceil(notes.length / 2)).map(note => (
          <div key={note.id} className="note-card">
            {note.title && <div className="note-title">{note.title}</div>}
            {editingId === note.id ? (
              <React.Fragment>
                <textarea className="note-textarea" value={editText} onChange={e => setEditText(e.target.value)} rows={4} autoFocus />
                <div className="note-actions">
                  <button className="btn ghost" onClick={() => setEditingId(null)}>Abbrechen</button>
                  <button className="btn" onClick={() => saveEdit(note)}>Speichern</button>
                </div>
              </React.Fragment>
            ) : (
              <React.Fragment>
                <div className="note-text">{note.text}</div>
                <div className="note-meta">
                  {note.lastEditedBy && <span className="note-author">{note.lastEditedBy.name}</span>}
                  <span className="note-time">{fmt(note.at)}</span>
                  <span className="note-btns">
                    <button onClick={() => startEdit(note)} title="bearbeiten"><EditIcon /></button>
                    <button onClick={() => del(note)} title="löschen" style={{ color: "var(--bad)" }}>✕</button>
                  </span>
                </div>
              </React.Fragment>
            )}
          </div>
        ))}
      </div>

      <div className="page right">
        <div className="page-eyebrow">Crew-Tagebuch · {notes.length} Einträge</div>
        <h2 className="page-heading">Tagebuch</h2>
        <div className="page-rule"><span className="line"></span><span className="glyph">◇ ◇ ◇</span><span className="line"></span></div>

        {notes.slice(Math.ceil(notes.length / 2)).map(note => (
          <div key={note.id} className="note-card">
            {note.title && <div className="note-title">{note.title}</div>}
            {editingId === note.id ? (
              <React.Fragment>
                <textarea className="note-textarea" value={editText} onChange={e => setEditText(e.target.value)} rows={4} autoFocus />
                <div className="note-actions">
                  <button className="btn ghost" onClick={() => setEditingId(null)}>Abbrechen</button>
                  <button className="btn" onClick={() => saveEdit(note)}>Speichern</button>
                </div>
              </React.Fragment>
            ) : (
              <React.Fragment>
                <div className="note-text">{note.text}</div>
                <div className="note-meta">
                  {note.lastEditedBy && <span className="note-author">{note.lastEditedBy.name}</span>}
                  <span className="note-time">{fmt(note.at)}</span>
                  <span className="note-btns">
                    <button onClick={() => startEdit(note)} title="bearbeiten"><EditIcon /></button>
                    <button onClick={() => del(note)} title="löschen" style={{ color: "var(--bad)" }}>✕</button>
                  </span>
                </div>
              </React.Fragment>
            )}
          </div>
        ))}

        {notes.length === 0 && (
          <div style={{ fontFamily: "IM Fell English:ital, serif", fontStyle: "italic", color: "var(--ink-fade)", textAlign: "center", marginTop: 60, fontSize: 18 }}>
            „Die Wildnis vergisst nichts —<br/>nur der Mensch."
          </div>
        )}
      </div>
    </React.Fragment>
  );
}

function LocationsView({ book, updateBook, openEditor, dispatchOp }) {
  const { locations } = book;
  const [selected, setSelected] = useState(null);
  const [placing, setPlacing] = useState(false);
  const [dragId, setDragId] = useState(null);
  const [isPanning, setIsPanning] = useState(false);
  const [view, setView] = useState({ scale: 1, tx: 0, ty: 0 });
  const mapRef = useRef(null);
  const viewRef = useRef({ scale: 1, tx: 0, ty: 0 });
  const panRef = useRef(null); // { startX, startY, startTx, startTy, moved }

  const clampView = (scale, tx, ty) => {
    if (!mapRef.current) return { scale, tx, ty };
    const { width: W, height: H } = mapRef.current.getBoundingClientRect();
    return {
      scale,
      tx: Math.min(0, Math.max(W * (1 - scale), tx)),
      ty: Math.min(0, Math.max(H * (1 - scale), ty)),
    };
  };

  const applyView = (next) => {
    viewRef.current = next;
    setView(next);
  };

  // Wheel-Zoom: passiv=false nötig, damit preventDefault greift
  useEffect(() => {
    const el = mapRef.current;
    if (!el) return;
    const onWheel = (e) => {
      e.preventDefault();
      const r = el.getBoundingClientRect();
      const mx = e.clientX - r.left;
      const my = e.clientY - r.top;
      const factor = e.deltaY < 0 ? 1.15 : 1 / 1.15;
      const { scale, tx, ty } = viewRef.current;
      const newScale = Math.max(1, Math.min(12, scale * factor));
      const ratio = newScale / scale;
      const next = clampView(newScale, mx - (mx - tx) * ratio, my - (my - ty) * ratio);
      applyView(next);
    };
    el.addEventListener("wheel", onWheel, { passive: false });
    return () => el.removeEventListener("wheel", onWheel);
  }, []);

  // Koordinaten in Karten-% umrechnen (berücksichtigt Zoom+Pan)
  const toMapCoords = (clientX, clientY) => {
    const r = mapRef.current.getBoundingClientRect();
    const { scale, tx, ty } = viewRef.current;
    const x = ((clientX - r.left - tx) / (r.width * scale)) * 100;
    const y = ((clientY - r.top - ty) / (r.height * scale)) * 100;
    return { x: Math.max(0, Math.min(100, x)), y: Math.max(0, Math.min(100, y)) };
  };

  // Pan-Geste auf der Karte
  const onMapPointerDown = (e) => {
    if (e.target.closest(".pin") || e.target.closest(".map-zoom-btns")) return;
    panRef.current = { startX: e.clientX, startY: e.clientY, startTx: viewRef.current.tx, startTy: viewRef.current.ty, moved: false };
    mapRef.current.setPointerCapture(e.pointerId);
    setIsPanning(true);
  };

  const onMapPointerMove = (e) => {
    if (!panRef.current) return;
    const dx = e.clientX - panRef.current.startX;
    const dy = e.clientY - panRef.current.startY;
    if (Math.abs(dx) > 4 || Math.abs(dy) > 4) panRef.current.moved = true;
    const { scale } = viewRef.current;
    const next = clampView(scale, panRef.current.startTx + dx, panRef.current.startTy + dy);
    applyView(next);
  };

  const onMapPointerUp = (e) => {
    if (!panRef.current) return;
    const moved = panRef.current.moved;
    panRef.current = null;
    setIsPanning(false);
    if (!moved && placing) {
      const { x, y } = toMapCoords(e.clientX, e.clientY);
      setPlacing(false);
      openEditor({ type: "location", data: null, defaults: { x, y } });
    }
  };

  // Pin-Drag (berücksichtigt Zoom)
  const onPinDown = (e, locId) => {
    e.stopPropagation();
    if (placing) return;
    setDragId(locId);
  };

  useEffect(() => {
    if (!dragId) return;
    const onMove = (e) => {
      if (!mapRef.current) return;
      const { x, y } = toMapCoords(e.clientX, e.clientY);
      updateBook({
        locations: book.locations.map(l => l.id === dragId
          ? { ...l, x: Math.max(0.5, Math.min(99.5, x)), y: Math.max(0.5, Math.min(99.5, y)) }
          : l)
      });
    };
    const onUp = () => {
      if (dispatchOp) {
        const loc = book.locations.find(l => l.id === dragId);
        if (loc) dispatchOp({ kind: "location.upsert", id: dragId, data: loc });
      }
      setDragId(null);
    };
    window.addEventListener("pointermove", onMove);
    window.addEventListener("pointerup", onUp);
    return () => { window.removeEventListener("pointermove", onMove); window.removeEventListener("pointerup", onUp); };
  }, [dragId, book.locations, updateBook, dispatchOp]);

  const zoomIn  = () => { const { scale, tx, ty } = viewRef.current; const next = clampView(Math.min(12, scale * 1.5), tx, ty); applyView(next); };
  const zoomOut = () => { const { scale, tx, ty } = viewRef.current; const next = clampView(Math.max(1, scale / 1.5), tx, ty); applyView(next); };
  const resetView = () => { const next = { scale: 1, tx: 0, ty: 0 }; applyView(next); };

  const dangerColor = (d) => d === "gefährlich" ? "#a83020" : d === "mittel" ? "#c8842a" : "#4a7236";

  const mapCursor = placing ? "crosshair" : isPanning ? "grabbing" : view.scale > 1 ? "grab" : "default";

  return (
    <React.Fragment>
      <div className="page left">
        <div className="page-eyebrow">Kapitel III</div>
        <h2 className="page-heading">Karte</h2>
        <div className="page-rule"><span className="line"></span><span className="glyph">✦ ✦ ✦</span><span className="line"></span></div>

        <div className="map-toolbar">
          <button className={"btn " + (placing ? "" : "ghost")} onClick={() => setPlacing(!placing)}>
            {placing ? "Klicken zum Setzen…" : "+ Ort platzieren"}
          </button>
          <button className="btn ghost" onClick={() => openEditor({ type: "location", data: null })}>+ Ohne Position</button>
          <span className="hint">Rad = Zoom · Ziehen = Verschieben</span>
        </div>

        <div
          ref={mapRef}
          className="big-map"
          style={{
            cursor: mapCursor,
            backgroundSize: `${view.scale * 100}% ${view.scale * 100}%`,
            backgroundPosition: `${view.tx}px ${view.ty}px`,
          }}
          onPointerDown={onMapPointerDown}
          onPointerMove={onMapPointerMove}
          onPointerUp={onMapPointerUp}
        >
          {/* Pins: in Container-Koordinaten positioniert */}
          {locations.map(loc => {
            if (loc.x == null || loc.y == null) return null;
            const isSel = selected === loc.id;
            const color = dangerColor(loc.danger);
            return (
              <div key={loc.id}
                className={"pin " + (dragId === loc.id ? "dragging" : "") + (isSel ? " selected" : "")}
                style={{
                  left: `calc(${loc.x * view.scale}% + ${view.tx}px)`,
                  top:  `calc(${loc.y * view.scale}% + ${view.ty}px)`,
                }}
                onPointerDown={(e) => onPinDown(e, loc.id)}
                onClick={(e) => { e.stopPropagation(); setSelected(isSel ? null : loc.id); }}>
                <svg width={isSel ? 32 : 24} height={isSel ? 40 : 30} viewBox="0 0 22 28" style={{ filter: "drop-shadow(0 2px 4px rgba(0,0,0,0.7))" }}>
                  <path d="M11 0 C5 0, 0 5, 0 11 C0 17, 11 28, 11 28 C11 28, 22 17, 22 11 C22 5, 17 0, 11 0 Z" fill={color} stroke="#2a1408" strokeWidth="1.5" />
                  <circle cx="11" cy="11" r="4" fill="#efe2c2" />
                </svg>
                {isSel && (
                  <div className="pin-popup" onClick={e => e.stopPropagation()}>
                    <div className="pin-popup-name">{loc.name}</div>
                    {loc.type && <div className="pin-popup-type">{loc.type}</div>}
                    {loc.desc && <div className="pin-popup-desc">{loc.desc}</div>}
                    {(loc.tags || []).length > 0 && (
                      <div className="pin-popup-tags">{loc.tags.map(t => <span key={t} className="loc-tag">{t}</span>)}</div>
                    )}
                    <span className={"danger-dot " + loc.danger} style={{ marginTop: 4, display: "inline-block" }}>{loc.danger}</span>
                    <div className="pin-popup-actions">
                      <button className="btn ghost" style={{ fontSize: 11, padding: "3px 8px" }} onClick={() => openEditor({ type: "location", data: loc })}>Bearbeiten</button>
                      <button className="btn ghost" style={{ fontSize: 11, padding: "3px 8px", color: "var(--bad)", borderColor: "var(--bad)" }}
                        onClick={() => { if (window.confirm(`„${loc.name}" löschen?`)) { dispatchOp({ kind: "location.delete", id: loc.id }); setSelected(null); } }}>Löschen</button>
                    </div>
                  </div>
                )}
                {!isSel && <div className="pin-tip">{loc.name}</div>}
              </div>
            );
          })}

          {/* Feste UI-Elemente */}
          <div className="legend">
            <div className="lg-row"><span className="lg-dot" style={{ background: "#4a7236" }}></span> Sicher</div>
            <div className="lg-row"><span className="lg-dot" style={{ background: "#c8842a" }}></span> Mittel</div>
            <div className="lg-row"><span className="lg-dot" style={{ background: "#a83020" }}></span> Gefährlich</div>
          </div>

          <Compass size={64} />

          <div className="map-zoom-btns">
            <button onClick={zoomIn}  title="Reinzoomen">+</button>
            <button onClick={zoomOut} title="Rauszoomen">−</button>
            <button onClick={resetView} title="Ansicht zurücksetzen" style={{ fontSize: 11 }}>↺</button>
            {view.scale > 1 && <span className="map-zoom-level">{Math.round(view.scale * 100)}%</span>}
          </div>
        </div>

        <div style={{ marginTop: 14, fontFamily: "Special Elite, monospace", fontSize: 11, color: "var(--ink-fade)", letterSpacing: "0.15em" }}>
          {locations.filter(l => l.x != null).length} ORTE GESETZT · {locations.filter(l => l.x == null).length} OHNE POSITION
        </div>
      </div>

      <div className="page right">
        <div className="page-eyebrow">Verzeichnis · {locations.length} Orte</div>
        <h2 className="page-heading">Orte & Reviere</h2>
        <div className="page-rule"><span className="line"></span><span className="glyph">✦ ✦ ✦</span><span className="line"></span></div>

        <div className="section-actions" style={{ justifyContent: "flex-end" }}>
          <button className="btn" onClick={() => openEditor({ type: "location", data: null })}>+ Neuer Ort</button>
        </div>

        <div className="loc-list">
          {locations.length === 0 && <div className="empty">Keine Orte angelegt — füge welche hinzu.</div>}
          {locations.map(loc => (
            <div key={loc.id} className="loc-card"
              style={{ outline: selected === loc.id ? "2px solid var(--terracotta)" : "none", outlineOffset: "-2px", cursor: "pointer" }}
              onClick={() => setSelected(selected === loc.id ? null : loc.id)}>
              <div className="loc-head">
                <span className="loc-name">{loc.name}</span>
                <span className="loc-type">{loc.type}</span>
                {loc.coords && <span className="loc-coords">{loc.coords}</span>}
                {loc.lastEditedBy && (
                  <span className="last-edited-by" style={{ fontSize: 10, opacity: 0.6 }}>
                    {loc.lastEditedBy.avatar && <img src={loc.lastEditedBy.avatar} alt="" style={{ width: 12, height: 12, borderRadius: "50%", marginRight: 3, verticalAlign: "middle" }} />}
                    {loc.lastEditedBy.name}
                  </span>
                )}
                <span className="row-actions">
                  <button onClick={(e) => { e.stopPropagation(); openEditor({ type: "location", data: loc }); }} title="bearbeiten"><EditIcon /></button>
                  <button onClick={(e) => { e.stopPropagation(); if (window.confirm(`„${loc.name}" löschen?`)) dispatchOp({ kind: "location.delete", id: loc.id }); }} title="löschen" style={{ color: "var(--bad)" }}>✕</button>
                </span>
              </div>
              {loc.desc && <div className="loc-desc">{loc.desc}</div>}
              <div className="loc-tags">
                {(loc.tags || []).map(t => <span key={t} className="loc-tag">{t}</span>)}
                <span className={"danger-dot " + loc.danger}>{loc.danger}</span>
              </div>
            </div>
          ))}
        </div>
      </div>
    </React.Fragment>
  );
}

// ===================== APP SHELL =====================

const DEFAULT_TWEAKS = /*EDITMODE-BEGIN*/{
  "accent": "terracotta",
  "leather": "warm"
}/*EDITMODE-END*/;

function App() {
  const [view, setView]           = useState(loadLS(LS_VIEW, "rezepte"));
  const [book, setBookState]      = useState(initialBook);
  const [editor, setEditor]       = useState(null);
  const [showActivity, setShowActivity] = useState(false);
  const [conflictInfo, setConflictInfo] = useState(null);
  const [toastNode, setToast]     = useToast();
  const [authLoading, setAuthLoading] = useState(true);
  const [authUser, setAuthUser]   = useState(null);
  const [showUserMenu, setShowUserMenu] = useState(false);
  const bookRef = useRef(book);

  const [tweaks, setTweak] = (typeof useTweaks === "function")
    ? useTweaks(DEFAULT_TWEAKS)
    : [DEFAULT_TWEAKS, () => {}];

  useEffect(() => { saveLS(LS_VIEW, view); }, [view]);
  useEffect(() => { bookRef.current = book; }, [book]);

  // ── Lokales Op-Apply ──────────────────────────────────────────────────────
  const applyOpLocal = useCallback((op, by) => {
    setBookState(prev => {
      const next = { ...applyOpToBook(JSON.parse(JSON.stringify(prev)), op, by), updatedAt: Date.now() };
      saveLS(LS_BOOK, next);
      return next;
    });
  }, []);

  // ── Full-Resync ────────────────────────────────────────────────────────────
  const forceSync = useCallback((freshBook, seq) => {
    const next = { ...freshBook, updatedAt: Date.now() };
    setBookState(next);
    saveLS(LS_BOOK, next);
    localStorage.setItem("cb_last_seq_v1", seq);
    setConflictInfo(null);
  }, []);

  // ── Sync-Client ────────────────────────────────────────────────────────────
  const sync = (typeof useSyncClient === "function") ? useSyncClient({
    onOp: useCallback((envelope) => {
      applyOpLocal(envelope.op, envelope.by);
      // Konflikt-Check: ist gerade ein Modal auf diesem Objekt offen?
      setEditor(prev => {
        if (prev && hasConflict(prev, envelope.op)) {
          setConflictInfo({ by: envelope.by, at: envelope.at, opId: envelope.opId });
        }
        return prev;
      });
    }, [applyOpLocal]),
    onForceSync: forceSync,
    onToast: setToast,
  }) : { status: "disconnected", user: null, online: [], events: [], send: () => {}, undo: () => {}, queueLength: 0 };

  // ── Auth-Check ─────────────────────────────────────────────────────────────
  useEffect(() => {
    fetch("/auth/me")
      .then(r => r.ok ? r.json() : null)
      .then(data => {
        setAuthUser(data?.user || null);
        setAuthLoading(false);
        if (data?.user) {
          fetch("/book")
            .then(r => r.ok ? r.json() : null)
            .then(d => { if (d?.book) forceSync(d.book, d.seq); })
            .catch(() => {});
        }
      })
      .catch(() => setAuthLoading(false));
  }, []);

  // ── Dispatch Op ───────────────────────────────────────────────────────────
  const dispatchOp = useCallback((op) => {
    const by = authUser || sync.user;
    applyOpLocal(op, by);
    if (by) sync.send(op);
  }, [applyOpLocal, authUser, sync]);

  // ── Nur-lokales Update (z.B. während Drag) ────────────────────────────────
  const updateBook = useCallback((patch) => {
    setBookState(prev => {
      const next = { ...prev, ...patch, updatedAt: Date.now() };
      saveLS(LS_BOOK, next);
      return next;
    });
  }, []);

  // ── setStock — diff → stock.set Ops ─────────────────────────────────────
  const setStock = useCallback((newStock) => {
    const currentStock = bookRef.current.stock;
    Object.entries(newStock).forEach(([itemId, qty]) => {
      if ((currentStock[itemId] ?? 0) !== qty) {
        dispatchOp({ kind: "stock.set", itemId, qty });
      }
    });
  }, [dispatchOp]);

  // ── Editor-Handler (alle über dispatchOp) ─────────────────────────────────
  const saveItem = (item) => {
    const { id, ...rest } = item;
    dispatchOp({ kind: "item.upsert", id, data: rest });
    setToast("Item gespeichert");
  };
  const deleteItem = (id) => {
    dispatchOp({ kind: "item.delete", id });
    setToast("Item gelöscht");
  };
  const saveCategory = (cat) => {
    dispatchOp({ kind: "category.upsert", id: cat.id, data: cat });
    setToast("Kategorie gespeichert");
  };
  const deleteCategory = (id) => {
    if (book.recipes.some(r => r.cat === id)) { setToast("Kategorie noch in Verwendung"); return; }
    dispatchOp({ kind: "category.delete", id });
    setToast("Kategorie gelöscht");
  };
  const createItemInline = (name) => {
    const id = "item_" + crypto.randomUUID().slice(0, 8);
    dispatchOp({ kind: "item.upsert", id, data: { id, name, glyph: name.slice(0, 3).toUpperCase(), color: "#8a9870", group: "Rohmaterial", minStock: 0 } });
    return id;
  };
  const saveRecipe = (recipe) => {
    dispatchOp({ kind: "recipe.upsert", id: recipe.id, data: recipe });
    setToast("Rezept gespeichert");
  };
  const deleteRecipe = (id) => {
    dispatchOp({ kind: "recipe.delete", id });
    setToast("Rezept gelöscht");
  };
  const saveLocation = (loc) => {
    dispatchOp({ kind: "location.upsert", id: loc.id, data: loc });
    setToast("Ort gespeichert");
  };
  const deleteLocation = (id) => {
    dispatchOp({ kind: "location.delete", id });
    setToast("Ort gelöscht");
  };
  const importBook = (data) => {
    updateBook({ items: data.items, categories: data.categories, recipes: data.recipes, locations: data.locations, stock: data.stock });
  };
  const handleUndo = async (opId) => {
    const result = await sync.undo(opId);
    if (!result?.error) {
      setToast("Rückgängig gemacht");
      // Book-Zustand neu laden
      fetch("/book").then(r => r.json()).then(d => { if (d?.book) forceSync(d.book, d.seq); }).catch(() => {});
    }
  };

  // ── findUndoOpId — letzter eigener Op für aktuelle Entity ────────────────
  const findUndoOpId = () => {
    if (!editor?.data?.id || !authUser) return null;
    const entityId = editor.data.id;
    const type = editor.type;
    for (const env of sync.events) {
      if (env.by?.id !== authUser.id) continue;
      const op = env.op;
      const opId = env.opId;
      if (type === "item"     && (op.kind === "item.upsert"     || op.kind === "item.delete")     && op.id === entityId) return opId;
      if (type === "recipe"   && (op.kind === "recipe.upsert"   || op.kind === "recipe.delete")   && op.id === entityId) return opId;
      if (type === "location" && (op.kind === "location.upsert" || op.kind === "location.delete") && op.id === entityId) return opId;
      if (type === "category" && (op.kind === "category.upsert" || op.kind === "category.delete") && op.id === entityId) return opId;
    }
    return null;
  };

  // ── Theme ─────────────────────────────────────────────────────────────────
  useEffect(() => {
    const root = document.documentElement;
    const accents = { terracotta: { primary: "#b8542a", dim: "#8e3e1c" }, turquoise: { primary: "#2d7a82", dim: "#1f5e64" }, ochre: { primary: "#c89234", dim: "#9c6f1c" }, sage: { primary: "#7a8862", dim: "#5a6840" } };
    const a = accents[tweaks.accent] || accents.terracotta;
    root.style.setProperty("--terracotta", a.primary);
    root.style.setProperty("--terracotta-dim", a.dim);
    const leathers = { warm: { l: "#4a2818", d: "#2a1408", m: "#6a3a20" }, dark: { l: "#2c1a10", d: "#150804", m: "#46271a" }, ash: { l: "#3a342a", d: "#1c1814", m: "#5a4c3a" } };
    const lth = leathers[tweaks.leather] || leathers.warm;
    root.style.setProperty("--leather", lth.l);
    root.style.setProperty("--leather-deep", lth.d);
    root.style.setProperty("--leather-mid", lth.m);
  }, [tweaks.accent, tweaks.leather]);

  // ── Auth-Screens ─────────────────────────────────────────────────────────
  if (authLoading) return <LoginScreen loading />;
  if (!authUser)   return <LoginScreen />;

  const undoOpId = findUndoOpId();
  const currentUser = authUser || sync.user;

  return (
    <div className="app" onClick={() => setShowUserMenu(false)}>
      <div className="book">
        <svg className="book-band top" viewBox="0 0 400 14" preserveAspectRatio="none">
          <defs><pattern id="sw-band" x="0" y="0" width="40" height="14" patternUnits="userSpaceOnUse"><path d="M 0 7 L 10 1 L 20 7 L 30 1 L 40 7 L 30 13 L 20 7 L 10 13 Z" fill="#d4a060" /><rect x="18" y="5" width="4" height="4" fill="#2a1408" /></pattern></defs>
          <rect width="400" height="14" fill="url(#sw-band)" />
        </svg>
        <svg className="book-band bot" viewBox="0 0 400 14" preserveAspectRatio="none">
          <rect width="400" height="14" fill="url(#sw-band)" />
        </svg>

        <div className="book-header">
          <div className="book-title">
            <svg width="36" height="36" viewBox="0 0 36 36" fill="none">
              <path d="M18 4 L31 11 L31 25 L18 32 L5 25 L5 11 Z" stroke="#d4a060" strokeWidth="1.5" fill="rgba(212, 160, 96, 0.1)" />
              <path d="M18 10 L25 14 L25 22 L18 26 L11 22 L11 14 Z" stroke="#d4a060" strokeWidth="1" fill="none" />
              <circle cx="18" cy="18" r="2" fill="#d4a060" />
            </svg>
            <div>
              <h1>Das Crafting-Buch</h1>
              <div className="sub">RDR · Roleplay · Frontier Manual</div>
            </div>
          </div>

          <div style={{ display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }}>
            {/* Verbindungsstatus */}
            <ConnectionStatus status={sync.status} queueLength={sync.queueLength} />

            {/* Online-Avatare */}
            <OnlineAvatars online={sync.online} />

            {/* Aktivitäten */}
            <button className="btn ghost" style={{ padding: "4px 10px", fontSize: 12 }}
              onClick={() => setShowActivity(s => !s)}
              title="Aktivitäten">
              ≡ {sync.events.length > 0 && <span style={{ marginLeft: 4, opacity: 0.7 }}>{sync.events.length}</span>}
            </button>

            <div className="nav-tabs">
              <button className={"nav-tab " + (view === "rezepte" ? "active" : "")} onClick={() => setView("rezepte")}>Rezepte</button>
              <button className={"nav-tab " + (view === "lager" ? "active" : "")} onClick={() => setView("lager")}>Lager</button>
              <button className={"nav-tab " + (view === "orte" ? "active" : "")} onClick={() => setView("orte")}>Orte</button>
              <button className={"nav-tab " + (view === "notizen" ? "active" : "")} onClick={() => setView("notizen")}>Notizen</button>
            </div>

            {/* User-Menü */}
            <div className="user-menu-wrap" onClick={e => e.stopPropagation()}>
              <button className="user-menu-btn" onClick={() => setShowUserMenu(s => !s)} title={currentUser?.name}>
                {currentUser?.avatar
                  ? <img src={currentUser.avatar} alt={currentUser.name} className="avatar-img" />
                  : <div className="avatar-fallback">{currentUser?.name?.[0]?.toUpperCase() || "?"}</div>
                }
              </button>
              {showUserMenu && (
                <div className="user-menu-dropdown">
                  <div className="user-menu-name">{currentUser?.name}</div>
                  <button className="btn ghost" style={{ width: "100%", marginTop: 8 }}
                    onClick={() => { fetch("/auth/logout", { method: "POST" }).then(() => setAuthUser(null)); }}>
                    Abmelden
                  </button>
                </div>
              )}
            </div>
          </div>
        </div>

        <div className="book-inner">
          {view === "rezepte"  && <RecipesView book={book} setStock={setStock} openEditor={setEditor} dispatchOp={dispatchOp} />}
          {view === "lager"    && <InventoryView book={book} setStock={setStock} openEditor={setEditor} />}
          {view === "orte"     && <LocationsView book={book} updateBook={updateBook} openEditor={setEditor} dispatchOp={dispatchOp} />}
          {view === "notizen"  && <NotesView book={book} openEditor={setEditor} dispatchOp={dispatchOp} currentUser={currentUser} />}
        </div>

        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 14, padding: "0 10px", fontFamily: "Special Elite, monospace", fontSize: 10, letterSpacing: "0.2em", textTransform: "uppercase", color: "rgba(232, 210, 170, 0.5)" }}>
          <span>Ed. 1886 · Vol. I · Stand {new Date(book.updatedAt).toLocaleDateString("de-DE")}</span>
          <span>Seite {view === "rezepte" ? "I" : view === "lager" ? "II" : view === "orte" ? "III" : "IV"} / IV</span>
        </div>
      </div>

      {/* Aktivitäten-Sidebar */}
      <ActivitySidebar
        open={showActivity}
        onClose={() => setShowActivity(false)}
        events={sync.events}
        book={book}
        onUndo={handleUndo}
        authUserId={currentUser?.id}
      />
      {showActivity && <div className="activity-overlay" onClick={() => setShowActivity(false)} />}

      {/* Modals */}
      {editor?.type === "item" && (
        <ItemEditor item={editor.data} onSave={saveItem} onDelete={deleteItem} onClose={() => { setEditor(null); setConflictInfo(null); }}
          conflictWarning={conflictInfo} undoOpId={undoOpId} onUndo={undoOpId ? handleUndo : null} />
      )}
      {editor?.type === "category" && (
        <CategoryEditor category={editor.data} onSave={saveCategory} onDelete={deleteCategory} onClose={() => { setEditor(null); setConflictInfo(null); }}
          conflictWarning={conflictInfo} />
      )}
      {editor?.type === "recipe" && (
        <RecipeEditor
          recipe={editor.data ? editor.data : (editor.defaults ? { ...editor.defaults } : null)}
          items={book.items} categories={book.categories}
          onSave={saveRecipe} onDelete={deleteRecipe} onClose={() => { setEditor(null); setConflictInfo(null); }}
          conflictWarning={conflictInfo} undoOpId={undoOpId} onUndo={undoOpId ? handleUndo : null}
          onCreateItem={createItemInline} />
      )}
      {editor?.type === "location" && (
        <LocationEditor
          location={editor.data ? editor.data : (editor.defaults ? { ...editor.defaults } : null)}
          onSave={saveLocation} onDelete={deleteLocation} onClose={() => { setEditor(null); setConflictInfo(null); }}
          conflictWarning={conflictInfo} undoOpId={undoOpId} onUndo={undoOpId ? handleUndo : null} />
      )}
      {/* Toast */}
      {toastNode}

      {/* Tweaks Panel */}
      {window.TweaksPanel && (
        <TweaksPanel title="Tweaks">
          <TweakSection title="Akzentfarbe">
            <TweakSelect label="Farbe" value={tweaks.accent} onChange={v => setTweak("accent", v)}
              options={[{ value: "terracotta", label: "Terrakotta" }, { value: "turquoise", label: "Türkis" }, { value: "ochre", label: "Ocker" }, { value: "sage", label: "Salbei" }]} />
          </TweakSection>
          <TweakSection title="Einband">
            <TweakRadio label="Leder" value={tweaks.leather} onChange={v => setTweak("leather", v)}
              options={[{ value: "warm", label: "Warm" }, { value: "dark", label: "Dunkel" }, { value: "ash", label: "Asche" }]} />
          </TweakSection>
        </TweaksPanel>
      )}
    </div>
  );
}

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