/* global React, ReactDOM */
const { useState, useEffect, useRef, useMemo, useCallback } = React;

// ---------- Tweaks defaults ----------
const DEFAULT_TWEAKS = {
  density: 'normal',
  cardStyle: 'classic',
  showNicknames: true,
  showDates: true,
  lineStyle: 'elbow',
};

// ---------- Brand mark (a rice grain with roots) ----------
function RiceRootMark({ size = 22, stroke = 1.5 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 40 40" fill="none" aria-hidden="true">
      <ellipse cx="20" cy="9" rx="3" ry="6" fill="currentColor" transform="rotate(-12 20 9)"/>
      <path
        d="M20 15 V21 M20 21 C 15 25, 12 31, 10 37 M20 21 C 25 25, 28 31, 30 37 M20 21 V37"
        stroke="currentColor" strokeWidth={stroke} strokeLinecap="round" fill="none"
      />
    </svg>
  );
}

// ---------- Loading splash ----------
function Splash() {
  return (
    <div className="gate">
      <div style={{ textAlign: 'center', color: 'var(--ink-mute)' }}>
        <div style={{ color: 'var(--accent)', marginBottom: 14 }}>
          <RiceRootMark size={36} stroke={1.2}/>
        </div>
        <div style={{ fontFamily: 'var(--font-serif)', fontSize: 18, color: 'var(--ink-soft)' }}>
          digging through the family tree…
        </div>
      </div>
    </div>
  );
}

// ---------- Gate ----------
function Gate({ mode, onUnlock, onSetPassword, subtitle, backendMode }) {
  const [pw, setPw] = useState('');
  const [pw2, setPw2] = useState('');
  const [err, setErr] = useState('');
  const [busy, setBusy] = useState(false);
  const inputRef = useRef();

  useEffect(() => { inputRef.current && inputRef.current.focus(); }, [mode]);

  const submit = async (e) => {
    e.preventDefault();
    setErr('');
    setBusy(true);
    try {
      if (mode === 'set') {
        if (pw.length < 4) { setErr('Use at least 4 characters.'); setBusy(false); return; }
        if (pw !== pw2) { setErr('Passwords don\'t match.'); setBusy(false); return; }
        await onSetPassword(pw);
      } else {
        const ok = await onUnlock(pw);
        if (!ok) setErr('That isn\'t the family password.');
      }
    } catch (e) {
      setErr(e.message || 'Something went wrong.');
    } finally {
      setBusy(false);
    }
  };

  return (
    <div className="gate">
      <form className="gate-card" onSubmit={submit}>
        <div className="gate-mark">
          <RiceRootMark size={44} stroke={1.5}/>
        </div>
        <div className="gate-eyebrow">riceroot.net</div>
        <h1 className="gate-title">
          {mode === 'set' ? 'Plant the tree' : 'RiceRoot'}
        </h1>
        {mode === 'enter' && (
          <p className="gate-pitch">
            You probably wandered in because you married into the Rices, got adopted, just found out about
            a long-lost cousin, or heard a story about an uncle with a kid overseas. Welcome.
            <br/><br/>
            <span style={{ color: 'var(--ink)', fontWeight: 500 }}>
              Crowdsource the truth, one branch at a time.
            </span>
          </p>
        )}
        {mode === 'set' && (
          <p className="gate-pitch">
            Pick a password. Share it with anyone you'd trust to add a great-aunt, a long-lost cousin,
            or that uncle nobody talks about.
          </p>
        )}

        <div className="gate-field">
          <label className="gate-label">
            {mode === 'set' ? 'New password' : 'Family password'}
          </label>
          <input
            ref={inputRef}
            type="password"
            className="field-input"
            value={pw}
            onChange={(e) => setPw(e.target.value)}
            autoComplete={mode === 'set' ? 'new-password' : 'current-password'}
          />
        </div>
        {mode === 'set' && (
          <div className="gate-field">
            <label className="gate-label">Confirm password</label>
            <input
              type="password"
              className="field-input"
              value={pw2}
              onChange={(e) => setPw2(e.target.value)}
              autoComplete="new-password"
            />
          </div>
        )}

        <div className="gate-err">{err}</div>

        <button type="submit" className="btn btn-primary btn-block" disabled={busy || !pw}>
          {busy
            ? (mode === 'set' ? 'Planting…' : 'Unlocking…')
            : (mode === 'set' ? 'Plant the tree' : 'Open the tree')}
        </button>

        <p className="gate-hint">
          {mode === 'set'
            ? (backendMode === 'local'
                ? 'Preview mode — saved only in this browser. On riceroot.net it syncs to Cloudflare.'
                : 'Stored on Cloudflare. Anyone with the password can read and edit.')
            : subtitle}
        </p>
      </form>
    </div>
  );
}

// ---------- Connector SVG ----------
function Connectors({ family, layout, lineStyle }) {
  const { positions, cardW, cardH } = layout;
  const byId = useMemo(() => new Map(family.people.map(p => [p.id, p])), [family]);

  // Couple lines (mutual spouses on the same row)
  const drawnCouples = new Set();
  const coupleLines = [];
  for (const p of family.people) {
    for (const sid of (p.spouses || [])) {
      if (!byId.has(sid)) continue;
      const key = [p.id, sid].sort().join('|');
      if (drawnCouples.has(key)) continue;
      drawnCouples.add(key);
      const a = positions.get(p.id);
      const b = positions.get(sid);
      if (!a || !b || a.gen !== b.gen) continue;
      const ax = a.x + cardW / 2;
      const bx = b.x + cardW / 2;
      const y = a.y + cardH / 2;
      // Detect a divorced/separated couple — they're far apart with another
      // spouse sitting between them on the same row. (Heuristic: if EITHER
      // person has another spouse drawn between them, mark it broken.)
      const aPerson = byId.get(p.id);
      const bPerson = byId.get(sid);
      const isBroken = (aPerson.spouses || []).length > 1 || (bPerson.spouses || []).length > 1;
      coupleLines.push({ x1: ax, x2: bx, y, key, broken: isBroken });
    }
  }

  // Parent→children family groups (children grouped by their parent set)
  const familyGroups = new Map();
  for (const p of family.people) {
    const parentsInTree = (p.parents || []).filter(pid => byId.has(pid));
    if (parentsInTree.length === 0) continue;
    const key = parentsInTree.slice().sort().join('|');
    if (!familyGroups.has(key)) {
      familyGroups.set(key, { parentIds: parentsInTree.slice().sort(), childIds: [] });
    }
    familyGroups.get(key).childIds.push(p.id);
  }

  const paths = [];
  for (const { parentIds, childIds } of familyGroups.values()) {
    const parentPositions = parentIds.map(pid => positions.get(pid)).filter(Boolean);
    if (parentPositions.length === 0 || childIds.length === 0) continue;
    const parentCenterX = parentPositions.reduce((s, p) => s + p.x + cardW / 2, 0) / parentPositions.length;
    // For a couple, the line should drop from the couple line (mid-card),
    // not from the card bottom. For a single parent, drop from card bottom.
    const parentY = parentPositions.length >= 2
      ? parentPositions[0].y + cardH / 2
      : parentPositions[0].y + cardH;
    const childPositions = childIds.map(cid => positions.get(cid)).filter(Boolean);
    if (childPositions.length === 0) continue;
    const childY = childPositions[0].y;

    // Detect "partial" parentage: a single parent who has a spouse NOT
    // listed as a parent for these kids (e.g. step-parent in a couple).
    // Draw those connections offset + dashed so they don't visually merge
    // with the standard couple-to-children bus.
    let isPartial = false;
    if (parentIds.length === 1) {
      const p = byId.get(parentIds[0]);
      if (p && (p.spouses || []).some(sid => byId.has(sid) && !parentIds.includes(sid))) {
        isPartial = true;
      }
    }
    const busY = childY - 28 - (isPartial ? 16 : 0);
    const cls = isPartial ? 'partial' : '';

    if (lineStyle === 'curve') {
      childPositions.forEach(cp => {
        const cx = cp.x + cardW / 2;
        paths.push({ d: `M ${parentCenterX} ${parentY} C ${parentCenterX} ${busY}, ${cx} ${busY}, ${cx} ${cp.y}`, cls });
      });
    } else if (lineStyle === 'straight') {
      childPositions.forEach(cp => {
        const cx = cp.x + cardW / 2;
        paths.push({ d: `M ${parentCenterX} ${parentY} L ${cx} ${cp.y}`, cls });
      });
    } else {
      // elbow — draw as one continuous path per child so butt-capped seams
      // don't leave visible gaps at the bus row (especially when parent and
      // child share an X column, which produced a degenerate zero-length
      // segment in the old two-path version).
      if (childPositions.length === 1) {
        const cp = childPositions[0];
        const cx = cp.x + cardW / 2;
        if (Math.abs(cx - parentCenterX) < 0.5) {
          // Straight drop — no elbow needed.
          paths.push({ d: `M ${parentCenterX} ${parentY} L ${cx} ${cp.y}`, cls });
        } else {
          paths.push({ d: `M ${parentCenterX} ${parentY} L ${parentCenterX} ${busY} L ${cx} ${busY} L ${cx} ${cp.y}`, cls });
        }
      } else {
        // Multi-child: parent drop into bus, full bus across, then a drop per child.
        // Drawn as one path-per-child so each child stub is continuous with the bus
        // from above (no seam at the join above each child's column).
        const xs = childPositions.map(cp => cp.x + cardW / 2);
        const minX = Math.min(...xs, parentCenterX);
        const maxX = Math.max(...xs, parentCenterX);
        // Parent drop + full bus
        paths.push({ d: `M ${parentCenterX} ${parentY} L ${parentCenterX} ${busY} M ${minX} ${busY} L ${maxX} ${busY}`, cls });
        // Each child stub from the bus down
        childPositions.forEach(cp => {
          const cx = cp.x + cardW / 2;
          paths.push({ d: `M ${cx} ${busY} L ${cx} ${cp.y}`, cls });
        });
      }
    }
  }

  return (
    <svg className="connector-layer" style={{ width: layout.width, height: layout.height }}>
      {coupleLines.map(c => (
        <g key={c.key}>
          <line className="connector couple" x1={c.x1} y1={c.y} x2={c.x2} y2={c.y}/>
          {c.broken && Math.abs(c.x2 - c.x1) > cardW * 1.5 && (() => {
            const mx = (c.x1 + c.x2) / 2;
            return (
              <g className="divorce-x" transform={`translate(${mx} ${c.y})`}>
                <line x1="-10" y1="-10" x2="10" y2="10"/>
                <line x1="-10" y1="10"  x2="10" y2="-10"/>
              </g>
            );
          })()}
        </g>
      ))}
      {paths.map((p, i) => (
        <path key={i} className={`connector ${p.cls || ''}`} d={p.d}/>
      ))}
    </svg>
  );
}

// ---------- Canvas with pan/zoom ----------
function TreeCanvas({ family, layout, focusedId, onSelect, tweaks }) {
  const [tx, setTx] = useState(0);
  const [ty, setTy] = useState(0);
  const [scale, setScale] = useState(1);
  const [grabbing, setGrabbing] = useState(false);
  const wrapRef = useRef();
  const dragRef = useRef(null);
  const stateRef = useRef({ tx: 0, ty: 0, scale: 1 });
  stateRef.current = { tx, ty, scale };

  const lastFitKey = useRef('');
  useEffect(() => {
    const wrap = wrapRef.current;
    if (!wrap || !layout.width) return;
    const key = `${layout.width}x${layout.height}`;
    if (lastFitKey.current === key) return;
    lastFitKey.current = key;

    const w = wrap.clientWidth;
    const h = wrap.clientHeight;
    const pad = 80;
    const s = Math.min(1, Math.min((w - pad * 2) / layout.width, (h - pad * 2) / layout.height));
    setScale(s);
    setTx((w - layout.width * s) / 2);
    setTy((h - layout.height * s) / 2);
  }, [layout.width, layout.height]);

  const centerOnPerson = useCallback((id) => {
    const wrap = wrapRef.current;
    const pos = layout.positions.get(id);
    if (!wrap || !pos) return;
    const w = wrap.clientWidth;
    const h = wrap.clientHeight;
    const s = stateRef.current.scale;
    const targetCx = pos.x + layout.cardW / 2;
    const targetCy = pos.y + layout.cardH / 2;
    setTx(w / 2 - targetCx * s);
    setTy(h / 2 - targetCy * s);
  }, [layout]);

  const onPointerDown = (e) => {
    if (e.button !== 0 && e.pointerType !== 'touch') return;
    if (e.target.closest('.card')) return;
    setGrabbing(true);
    dragRef.current = { x: e.clientX, y: e.clientY, tx, ty, id: e.pointerId };
    e.currentTarget.setPointerCapture(e.pointerId);
  };
  const onPointerMove = (e) => {
    if (!dragRef.current || dragRef.current.id !== e.pointerId) return;
    const dx = e.clientX - dragRef.current.x;
    const dy = e.clientY - dragRef.current.y;
    setTx(dragRef.current.tx + dx);
    setTy(dragRef.current.ty + dy);
  };
  const onPointerUp = (e) => {
    if (dragRef.current && dragRef.current.id === e.pointerId) {
      dragRef.current = null;
      setGrabbing(false);
    }
  };

  useEffect(() => {
    const el = wrapRef.current;
    if (!el) return;
    const handler = (e) => {
      e.preventDefault();
      const wrap = wrapRef.current;
      if (!wrap) return;
      const rect = wrap.getBoundingClientRect();
      const cx = e.clientX - rect.left;
      const cy = e.clientY - rect.top;
      const dz = e.deltaY < 0 ? 1.1 : 1 / 1.1;
      const { scale: s, tx: cTx, ty: cTy } = stateRef.current;
      const newScale = Math.min(2, Math.max(0.2, s * dz));
      const actual = newScale / s;
      setScale(newScale);
      setTx(cx - (cx - cTx) * actual);
      setTy(cy - (cy - cTy) * actual);
    };
    el.addEventListener('wheel', handler, { passive: false });
    return () => el.removeEventListener('wheel', handler);
  }, []);

  const zoomBy = (factor) => {
    const wrap = wrapRef.current;
    if (!wrap) return;
    const cx = wrap.clientWidth / 2;
    const cy = wrap.clientHeight / 2;
    const newScale = Math.min(2, Math.max(0.2, scale * factor));
    const actual = newScale / scale;
    setScale(newScale);
    setTx(cx - (cx - tx) * actual);
    setTy(cy - (cy - ty) * actual);
  };

  const zoomReset = () => {
    const wrap = wrapRef.current;
    if (!wrap || !layout.width) return;
    const w = wrap.clientWidth;
    const h = wrap.clientHeight;
    const pad = 80;
    const s = Math.min(1, Math.min((w - pad * 2) / layout.width, (h - pad * 2) / layout.height));
    setScale(s);
    setTx((w - layout.width * s) / 2);
    setTy((h - layout.height * s) / 2);
  };

  useEffect(() => {
    if (focusedId) centerOnPerson(focusedId);
    // eslint-disable-next-line
  }, [focusedId]);

  return (
    <div
      ref={wrapRef}
      className={`canvas-wrap ${grabbing ? 'grabbing' : ''}`}
      onPointerDown={onPointerDown}
      onPointerMove={onPointerMove}
      onPointerUp={onPointerUp}
      onPointerCancel={onPointerUp}
    >
      <div
        className="canvas"
        style={{ transform: `translate(${tx}px, ${ty}px) scale(${scale})` }}
      >
        <Connectors family={family} layout={layout} lineStyle={tweaks.lineStyle}/>
        {family.people.map(p => {
          const pos = layout.positions.get(p.id);
          if (!pos) return null;
          return (
            <PersonCard
              key={p.id}
              person={p}
              pos={pos}
              cardW={layout.cardW}
              cardH={layout.cardH}
              density={tweaks.density}
              showNicknames={tweaks.showNicknames}
              showDates={tweaks.showDates}
              photoForward={tweaks.cardStyle === 'photo'}
              focused={focusedId === p.id}
              onClick={onSelect}
            />
          );
        })}
      </div>

      <div className="zoom-controls">
        <button title="Zoom in"  onClick={() => zoomBy(1.2)}><Icon name="plus"/></button>
        <button title="Reset"    onClick={zoomReset}><Icon name="home"/></button>
        <button title="Zoom out" onClick={() => zoomBy(1/1.2)}><Icon name="minus"/></button>
      </div>
    </div>
  );
}

// ---------- Tweaks panel ----------
function TweaksPanel({ tweaks, onChange }) {
  const set = (k, v) => onChange({ ...tweaks, [k]: v });
  const Seg = ({ k, opts }) => (
    <div className="tweak-segment">
      {opts.map(([val, label]) => (
        <button key={val} className={tweaks[k] === val ? 'active' : ''} onClick={() => set(k, val)}>
          {label}
        </button>
      ))}
    </div>
  );
  const Toggle = ({ k }) => (
    <button className={`tweak-toggle ${tweaks[k] ? 'on' : ''}`}
      onClick={() => set(k, !tweaks[k])} aria-pressed={tweaks[k]}/>
  );
  return (
    <div className="tweaks">
      <div className="tweaks-header">
        <Icon name="sliders" size={14}/>
        <span className="tweaks-title">Display</span>
      </div>
      <div className="tweaks-body">
        <div className="tweak-row"><label>Card style</label>
          <Seg k="cardStyle" opts={[['classic', 'Classic'], ['photo', 'Photo']]}/>
        </div>
        <div className="tweak-row"><label>Density</label>
          <Seg k="density" opts={[['compact', 'S'], ['normal', 'M'], ['cozy', 'L']]}/>
        </div>
        <div className="tweak-row"><label>Lines</label>
          <Seg k="lineStyle" opts={[['elbow', 'Elbow'], ['curve', 'Curve'], ['straight', 'Line']]}/>
        </div>
        <div className="tweak-row"><label>Nicknames</label><Toggle k="showNicknames"/></div>
        <div className="tweak-row"><label>Dates</label><Toggle k="showDates"/></div>
      </div>
    </div>
  );
}

// ---------- Save indicator ----------
function SaveIndicator({ status }) {
  if (status === 'idle') return null;
  const label = {
    saving: 'Saving…',
    saved:  'Saved',
    error:  'Couldn\'t save',
  }[status];
  return (
    <div className={`save-indicator save-${status}`}>
      <span className="save-dot"/>
      <span>{label}</span>
    </div>
  );
}

// ---------- App ----------
function App() {
  const FD = window.FamilyData;

  const [phase, setPhase] = useState('loading'); // 'loading' | 'gate-set' | 'gate-enter' | 'unlocked'
  const [gateSubtitle] = useState(() => FD.pickSubtitle());
  const [backendMode, setBackendMode] = useState(null); // 'cloud' | 'local'
  const [family, setFamily] = useState({ people: [] });
  const [tweaks, setTweaks] = useState(() => ({ ...DEFAULT_TWEAKS, ...(FD.loadTweaks() || {}) }));
  const [showTweaks, setShowTweaks] = useState(false);

  const [selectedId, setSelectedId] = useState(null);
  const [focusedId, setFocusedId] = useState(null);
  const [editing, setEditing] = useState(null);
  const [showSettings, setShowSettings] = useState(false);
  const [searchQ, setSearchQ] = useState('');
  const [searchOpen, setSearchOpen] = useState(false);
  const [saveStatus, setSaveStatus] = useState('idle');

  // ---- Initial load: check state, fetch tree if authed
  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const mode = await FD.getBackendMode();
        if (!cancelled) setBackendMode(mode);
        const st = await FD.getState();
        if (cancelled) return;
        if (!st.initialized) { setPhase('gate-set'); return; }
        if (!st.authed)      { setPhase('gate-enter'); return; }
        const t = await FD.fetchTree();
        if (cancelled) return;
        setFamily(t);
        setPhase('unlocked');
      } catch (e) {
        if (!cancelled) setPhase('gate-enter');
      }
    })();
    return () => { cancelled = true; };
  }, []);

  // ---- Tweaks persist locally
  useEffect(() => { FD.saveTweaks(tweaks); }, [tweaks]);

  // ---- Debounced save of family to server
  const saveTimer = useRef(null);
  const dirtyRef = useRef(false);
  const inFlightRef = useRef(false);
  const latestFamilyRef = useRef(family);
  latestFamilyRef.current = family;

  const scheduleSave = useCallback(() => {
    dirtyRef.current = true;
    setSaveStatus('saving');
    if (saveTimer.current) clearTimeout(saveTimer.current);
    saveTimer.current = setTimeout(async () => {
      if (inFlightRef.current) {
        // re-schedule shortly
        scheduleSave();
        return;
      }
      inFlightRef.current = true;
      const snapshot = latestFamilyRef.current;
      try {
        await FD.saveTree(snapshot);
        if (!dirtyRef.current || latestFamilyRef.current === snapshot) {
          dirtyRef.current = false;
          setSaveStatus('saved');
          setTimeout(() => setSaveStatus(s => s === 'saved' ? 'idle' : s), 1400);
        }
      } catch (e) {
        if (e.status === 401) {
          setPhase('gate-enter');
          setSaveStatus('idle');
        } else {
          setSaveStatus('error');
        }
      } finally {
        inFlightRef.current = false;
      }
    }, 600);
  }, []);

  // Update family AND trigger a save
  const updateFamily = useCallback((updater) => {
    setFamily(prev => {
      const next = typeof updater === 'function' ? updater(prev) : updater;
      latestFamilyRef.current = next;
      scheduleSave();
      return next;
    });
  }, [scheduleSave]);

  // ---- Layout
  const layout = useMemo(() => {
    const cardH = tweaks.density === 'compact' ? 160
      : tweaks.cardStyle === 'photo' ? 260 : 200;
    const cardW = tweaks.density === 'compact' ? 144 : 168;
    return window.FamilyLayout.computeLayout(family.people, {
      cardW, cardH,
      gapX: tweaks.density === 'compact' ? 22 : 28,
      genGap: tweaks.density === 'compact' ? 80 : 110,
    });
  }, [family, tweaks.density, tweaks.cardStyle]);

  const selectedPerson = selectedId ? family.people.find(p => p.id === selectedId) : null;

  // ---- Search
  const searchResults = useMemo(() => {
    const q = searchQ.trim().toLowerCase();
    if (!q) return [];
    return family.people.filter(p =>
      (p.name || '').toLowerCase().includes(q) ||
      (p.nickname || '').toLowerCase().includes(q)
    ).slice(0, 8);
  }, [searchQ, family]);

  // ---- Handlers
  const handleSelect = (person) => {
    setSelectedId(person.id);
    setFocusedId(person.id);
  };
  const handleClosePanel = () => setSelectedId(null);
  const handleEdit = () => {
    if (!selectedPerson) return;
    setEditing({ person: selectedPerson, isNew: false });
  };

  const handleSave = (form) => {
    updateFamily(prev => {
      const idx = prev.people.findIndex(p => p.id === form.id);
      const oldPerson = idx >= 0 ? prev.people[idx] : null;

      let people;
      if (idx >= 0) {
        people = prev.people.slice();
        people[idx] = { ...prev.people[idx], ...form };
      } else {
        people = [...prev.people, form];
      }

      // ---- Bi-directional spouse sync (additions AND removals) ----
      const oldSpouses = new Set(oldPerson ? (oldPerson.spouses || []) : []);
      const newSpouses = new Set(form.spouses || []);
      const removed = [...oldSpouses].filter(s => !newSpouses.has(s));
      const added   = [...newSpouses].filter(s => !oldSpouses.has(s));

      if (removed.length || added.length) {
        people = people.map(p => {
          if (p.id === form.id) return p;
          if (!removed.includes(p.id) && !added.includes(p.id)) return p;
          const spouses = new Set(p.spouses || []);
          if (removed.includes(p.id)) spouses.delete(form.id);
          if (added.includes(p.id))   spouses.add(form.id);
          return { ...p, spouses: Array.from(spouses) };
        });
      }

      // Drop any spouse refs to people no longer in the tree
      const ids = new Set(people.map(p => p.id));
      people = people.map(p => {
        const filtered = (p.spouses || []).filter(id => ids.has(id));
        return filtered.length === (p.spouses || []).length ? p : { ...p, spouses: filtered };
      });

      return { ...prev, people };
    });
    setSelectedId(form.id);
    if (editing && editing.onSaveExtra) editing.onSaveExtra(form);
    setEditing(null);
  };

  const handleDelete = () => {
    if (!selectedPerson) return;
    const ok = confirm(`Delete ${selectedPerson.name}? This can't be undone.`);
    if (!ok) return;
    const id = selectedPerson.id;
    updateFamily(prev => ({
      ...prev,
      people: prev.people
        .filter(p => p.id !== id)
        .map(p => ({
          ...p,
          parents: (p.parents || []).filter(x => x !== id),
          spouses: (p.spouses || []).filter(x => x !== id),
        })),
    }));
    setSelectedId(null);
  };

  const handleAddRelative = (kind) => {
    if (!selectedPerson) return;
    const base = {
      id: FD.newId(),
      name: '', nickname: '', photo: null,
      birthDate: '', deathDate: '',
      location: '', notes: '',
      parents: [], spouses: [], color: null,
    };
    if (kind === 'parent') {
      setEditing({
        person: base,
        isNew: true,
        suggestedRelation: 'spouse',
        onSaveExtra: (newPerson) => {
          updateFamily(prev => ({
            ...prev,
            people: prev.people.map(p =>
              p.id === selectedPerson.id
                ? { ...p, parents: Array.from(new Set([...(p.parents || []), newPerson.id])).slice(0, 2) }
                : p
            ),
          }));
        }
      });
    } else if (kind === 'spouse') {
      base.spouses = [selectedPerson.id];
      setEditing({ person: base, isNew: true, suggestedRelation: 'parent' });
    } else if (kind === 'child') {
      base.parents = [selectedPerson.id, ...(selectedPerson.spouses || []).slice(0, 1)].filter(Boolean);
      setEditing({ person: base, isNew: true, suggestedRelation: 'parent' });
    } else if (kind === 'sibling') {
      base.parents = (selectedPerson.parents || []).slice();
      setEditing({ person: base, isNew: true, suggestedRelation: 'parent' });
    }
  };

  const handleAddNew = () => {
    setEditing({
      person: {
        id: FD.newId(),
        name: '', nickname: '', photo: null,
        birthDate: '', deathDate: '',
        location: '', notes: '',
        parents: [], spouses: [], color: null,
      },
      isNew: true,
      suggestedRelation: 'parent',
    });
  };

  // ---- Auth handlers
  const handleSetPassword = async (pw) => {
    await FD.setupPassword(pw);
    const tree = await FD.fetchTree();
    setFamily(tree);
    setPhase('unlocked');
  };
  const handleUnlock = async (pw) => {
    const ok = await FD.unlock(pw);
    if (!ok) return false;
    const tree = await FD.fetchTree();
    setFamily(tree);
    setPhase('unlocked');
    return true;
  };
  const handleLogout = async () => {
    setSelectedId(null);
    setShowSettings(false);
    await FD.lock();
    setPhase('gate-enter');
  };
  const handleReset = async () => {
    await FD.resetEverything();
    setShowSettings(false);
    setSelectedId(null);
    setPhase('gate-set');
  };

  // Keyboard
  useEffect(() => {
    const onKey = (e) => {
      if (e.key === 'Escape') {
        if (editing) setEditing(null);
        else if (showSettings) setShowSettings(false);
        else if (selectedId) setSelectedId(null);
        else if (showTweaks) setShowTweaks(false);
      }
      if (e.key === '/' && !e.target.closest('input, textarea')) {
        e.preventDefault();
        const inp = document.querySelector('.search-wrap input');
        inp && inp.focus();
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [editing, showSettings, selectedId, showTweaks]);

  // ---- Render
  if (phase === 'loading') return <Splash/>;
  if (phase === 'gate-set' || phase === 'gate-enter') {
    return <Gate
      mode={phase === 'gate-set' ? 'set' : 'enter'}
      onSetPassword={handleSetPassword}
      onUnlock={handleUnlock}
      subtitle={gateSubtitle}
      backendMode={backendMode}
    />;
  }

  return (
    <div className="app">
      {backendMode === 'local' && (
        <div className="preview-banner">
          <strong>Preview mode.</strong> No Cloudflare backend detected — your edits live only
          in this browser. Deploy to riceroot.net to share with the family.
        </div>
      )}
      <div className="topbar">
        <div className="brand">
          <span className="brand-mark"><RiceRootMark size={22} stroke={1.5}/></span>
          <span className="brand-name">RiceRoot</span>
          <span className="brand-dot">·</span>
          <span className="brand-tag">{family.people.length} branches and counting</span>
        </div>

        <div className="topbar-spacer"/>

        <SaveIndicator status={saveStatus}/>

        <div className="search-wrap">
          <Icon name="search"/>
          <input
            placeholder="Search names…  ( / )"
            value={searchQ}
            onChange={e => { setSearchQ(e.target.value); setSearchOpen(true); }}
            onFocus={() => setSearchOpen(true)}
            onBlur={() => setTimeout(() => setSearchOpen(false), 150)}
          />
          {searchOpen && searchQ && (
            <div className="search-results">
              {searchResults.length === 0
                ? <div className="search-empty">No one named that. Maybe add them?</div>
                : searchResults.map(p => (
                  <button key={p.id} className="search-result"
                    onMouseDown={(e) => { e.preventDefault(); handleSelect(p); setSearchQ(''); setSearchOpen(false); }}>
                    <Avatar person={p}/>
                    <div>
                      <div style={{ fontSize: 14 }}>{p.name}</div>
                      {p.nickname && <div style={{ fontSize: 12, color: 'var(--ink-mute)', fontStyle: 'italic' }}>“{p.nickname}”</div>}
                    </div>
                  </button>
                ))}
            </div>
          )}
        </div>

        <div className="topbar-actions">
          <button className="btn btn-ghost btn-icon" title="Display options" onClick={() => setShowTweaks(v => !v)}>
            <Icon name="sliders"/>
          </button>
          <button className="btn btn-ghost btn-icon" title="Settings" onClick={() => setShowSettings(true)}>
            <Icon name="settings"/>
          </button>
        </div>
      </div>

      {family.people.length === 0 ? (
        <div className="canvas-wrap" style={{ position: 'relative', flex: 1 }}>
          <EmptyState onAdd={handleAddNew}/>
        </div>
      ) : (
        <TreeCanvas
          family={family}
          layout={layout}
          focusedId={focusedId}
          onSelect={handleSelect}
          tweaks={tweaks}
        />
      )}

      <button className="add-fab" title="Add person" onClick={handleAddNew}>
        <Icon name="plus" size={20}/>
      </button>

      {showTweaks && (
        <TweaksPanel tweaks={tweaks} onChange={setTweaks}/>
      )}

      {selectedPerson && !editing && (
        <DetailPanel
          person={selectedPerson}
          family={family}
          onClose={handleClosePanel}
          onSelect={(p) => { setSelectedId(p.id); setFocusedId(p.id); }}
          onEdit={handleEdit}
          onDelete={handleDelete}
          onAddRelative={handleAddRelative}
        />
      )}

      {editing && (
        <EditPanel
          person={editing.person}
          family={family}
          isNew={editing.isNew}
          suggestedRelation={editing.suggestedRelation}
          onSave={handleSave}
          onCancel={() => setEditing(null)}
        />
      )}

      {showSettings && (
        <SettingsPanel
          onClose={() => setShowSettings(false)}
          onReset={handleReset}
          onLogout={handleLogout}
        />
      )}
    </div>
  );
}

// Mount
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App/>);
