/* global React, ReactDOM, SuguruEngine, SuguruI18n, SuguruGrid, SuguruStorage, SuguruKotoba, KotobaLoader */
const { useState, useEffect, useRef, useMemo, useCallback } = React;
const { loadStore, saveStore, fmtTime, uid, STORAGE_KEY } = SuguruStorage;

// ---- Helpers ----
const GRADE_LABEL_KEY = {
  'naked-single': 'tech_nakedSingle',
  'hidden-single': 'tech_hiddenSingle',
  'naked-pair': 'tech_nakedPair',
  'hidden-pair': 'tech_hiddenPair',
  'naked-triple': 'tech_nakedTriple',
  'hidden-triple': 'tech_hiddenTriple',
  'locked-candidate': 'tech_lockedCandidate',
};
const GRADE_DESC_KEY = {
  'naked-single': 'desc_nakedSingle',
  'hidden-single': 'desc_hiddenSingle',
  'naked-pair': 'desc_nakedPair',
  'hidden-pair': 'desc_hiddenPair',
  'naked-triple': 'desc_nakedTriple',
  'hidden-triple': 'desc_hiddenTriple',
  'locked-candidate': 'desc_lockedCandidate',
};
const GRADE_ORDER = ['naked-single','hidden-single','naked-pair','hidden-pair','naked-triple','hidden-triple','locked-candidate'];
function gradeLabel(t, grade) {
  const key = GRADE_LABEL_KEY[grade];
  return key ? (t[key] || grade) : grade;
}
function gradeDesc(t, grade) {
  const key = GRADE_DESC_KEY[grade];
  return key ? (t[key] || '') : '';
}

function deepCloneGrid(g) { return g.map(r => r.slice()); }
function makeEmptyNotes(R, C) {
  return Array.from({ length: R }, () => Array.from({ length: C }, () => new Set()));
}
function cloneNotes(notes) {
  return notes.map(row => row.map(s => new Set(s)));
}
function notesToJSON(notes) {
  return notes.map(row => row.map(s => [...s]));
}
function notesFromJSON(obj) {
  return obj.map(row => row.map(arr => new Set(arr)));
}
function isComplete(puzzle, userGrid, solution) {
  for (let r = 0; r < puzzle.length; r++)
    for (let c = 0; c < puzzle[0].length; c++) {
      const v = puzzle[r][c] || userGrid[r][c];
      if (v === 0 || v !== solution[r][c]) return false;
    }
  return true;
}

// ---- Panel primitives ----
function Segmented({ options, value, onChange }) {
  return (
    <div className="seg">
      {options.map(o => (
        <button key={o.value}
          className={`seg-btn ${o.value === value ? 'on' : ''}`}
          onClick={() => onChange(o.value)}>
          {o.label}
        </button>
      ))}
    </div>
  );
}

function Modal({ open, onClose, title, children, maxWidth = 520 }) {
  if (!open) return null;
  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" style={{ maxWidth }} onClick={e => e.stopPropagation()}>
        <div className="modal-head">
          <h3>{title}</h3>
          <button className="icon-btn" onClick={onClose}>✕</button>
        </div>
        <div className="modal-body">{children}</div>
      </div>
    </div>
  );
}

// ---- Confirm/alert — styled replacement for window.confirm/alert ----
// Module-level controller so any nested component can call confirmAsync/notify
// without prop drilling. ConfirmHost at the root mounts the handler.
let _confirmController = null;
function confirmAsync(message, opts = {}) {
  return new Promise(resolve => {
    if (_confirmController) _confirmController({ message, ok: opts.ok, cancel: opts.cancel, mode: 'confirm' }, resolve);
    else resolve(false);
  });
}
function notify(message, opts = {}) {
  return new Promise(resolve => {
    if (_confirmController) _confirmController({ message, ok: opts.ok, mode: 'alert' }, () => resolve());
    else resolve();
  });
}

function GlossaryModal({ open, onClose, t, highlight }) {
  return (
    <Modal open={open} onClose={onClose} title={t.glossaryTitle}>
      <p className="prose muted">{t.glossaryIntro}</p>
      <dl className="glossary-list">
        {GRADE_ORDER.map(g => (
          <div key={g} className={`glossary-item ${highlight === g ? 'is-current' : ''}`}>
            <dt>{gradeLabel(t, g)}</dt>
            <dd>{gradeDesc(t, g)}</dd>
          </div>
        ))}
      </dl>
    </Modal>
  );
}

function ConfirmHost({ t }) {
  const [state, setState] = useState(null);
  useEffect(() => {
    _confirmController = (payload, resolve) => setState({ ...payload, resolve });
    return () => { _confirmController = null; };
  }, []);
  if (!state) return null;
  const done = (ok) => { const r = state.resolve; setState(null); r(ok); };
  return (
    <div className="modal-backdrop" onClick={() => done(false)}>
      <div className="modal confirm-modal" style={{ maxWidth: 420 }} onClick={e => e.stopPropagation()}>
        <div className="modal-body">
          <p className="confirm-message">{state.message}</p>
          <div className="confirm-actions">
            {state.mode === 'confirm' && (
              <button className="ghost-btn" onClick={() => done(false)}>{state.cancel || t.cancel}</button>
            )}
            <button className="primary-btn" onClick={() => done(true)}>{state.ok || 'OK'}</button>
          </div>
        </div>
      </div>
    </div>
  );
}

// ---- Generation screen (when no puzzle loaded) ----
function Welcome({ t, onGenerate, store, onLoad }) {
  const [rows, setRows] = useState(6);
  const [cols, setCols] = useState(6);
  const [diff, setDiff] = useState('medium');
  const [busy, setBusy] = useState(false);

  const go = async () => {
    setBusy(true);
    // Yield so spinner can show
    await new Promise(r => setTimeout(r, 40));
    try {
      onGenerate(rows, cols, diff);
    } finally {
      setBusy(false);
    }
  };

  const sizePresets = [
    { r: 5, c: 5, label: '5×5' },
    { r: 6, c: 6, label: '6×6' },
    { r: 7, c: 7, label: '7×7' },
    { r: 8, c: 8, label: '8×8' },
    { r: 9, c: 9, label: '9×9' },
    { r: 5, c: 8, label: '5×8' },
    { r: 6, c: 9, label: '6×9' },
    { r: 7, c: 11, label: '7×11' },
  ];

  return (
    <div className="welcome">
      <div className="welcome-inner">
        <div className="brush-seal" aria-hidden>数</div>
        <h1 className="title">{t.appName}</h1>
        <p className="tagline">{t.tagline}</p>

        <div className="card">
          <div className="field">
            <label>{t.size}</label>
            <div className="size-grid">
              {sizePresets.map(p => (
                <button key={p.label}
                  className={`chip ${rows === p.r && cols === p.c ? 'on' : ''}`}
                  onClick={() => { setRows(p.r); setCols(p.c); }}>
                  {p.label}
                </button>
              ))}
            </div>
            <div className="custom-size">
              <label>{t.rows}
                <input type="number" min={4} max={12} value={rows}
                  onChange={e => setRows(Math.max(4, Math.min(12, +e.target.value||6)))} />
              </label>
              <label>{t.cols}
                <input type="number" min={4} max={12} value={cols}
                  onChange={e => setCols(Math.max(4, Math.min(12, +e.target.value||6)))} />
              </label>
            </div>
          </div>

          <div className="field">
            <label>{t.difficulty}</label>
            <Segmented
              options={[
                { value: 'easy', label: t.easy },
                { value: 'medium', label: t.medium },
                { value: 'hard', label: t.hard },
                { value: 'expert', label: t.expert },
              ]}
              value={diff} onChange={setDiff} />
          </div>

          <button className="primary-btn" disabled={busy} onClick={go}>
            {busy ? t.generating : t.generate}
          </button>
        </div>

        {store.lastId && (
          <button className="link-btn" onClick={() => onLoad(store.lastId)}>
            ↩ {t.resume2}
          </button>
        )}
      </div>
    </div>
  );
}

// ---- Number pad (responsive) ----
function NumPad({ maxN, onNum, onErase, onNotes, notesOn, t, userValueCounts, disabled }) {
  const nums = Array.from({ length: maxN }, (_, i) => i + 1);
  return (
    <div className="numpad">
      <div className="numpad-row">
        {nums.map(n => (
          <button key={n} className={`num-btn ${userValueCounts[n] >= 999 ? 'done' : ''}`}
            disabled={disabled} onClick={() => onNum(n)}>
            <span className="num">{n}</span>
          </button>
        ))}
      </div>
      <div className="numpad-row small">
        <button className="util-btn" disabled={disabled} onClick={onErase}>
          <span className="glyph">⌫</span><span>{t.erase}</span>
        </button>
        <button className={`util-btn ${notesOn ? 'on' : ''}`} disabled={disabled} onClick={onNotes}>
          <span className="glyph">✎</span><span>{t.notes}</span>
        </button>
      </div>
    </div>
  );
}

// ---- Main app ----
function App() {
  const [store, setStore] = useState(loadStore);
  const [lang, setLang] = useState(store.lang);
  const [theme, setTheme] = useState(store.theme);
  const [regionStyle, setRegionStyle] = useState(store.regionStyle);
  const t = SuguruI18n[lang] || SuguruI18n.en;

  // Current active puzzle record
  const [current, setCurrent] = useState(null);
  // Editable play state
  const [userGrid, setUserGrid] = useState(null);
  const [notes, setNotes] = useState(null);
  const [selected, setSelected] = useState(null);
  const [notesMode, setNotesMode] = useState(false);
  const [history, setHistory] = useState([]);
  const [future, setFuture] = useState([]);
  const [showMistakes, setShowMistakes] = useState(false);
  const [completed, setCompleted] = useState(false);
  const [timer, setTimer] = useState(0);
  const [paused, setPaused] = useState(false);
  const [busy, setBusy] = useState(false);

  const [showRules, setShowRules] = useState(false);
  const [showHistory, setShowHistory] = useState(false);
  const [showPrint, setShowPrint] = useState(false);
  const [showTweaks, setShowTweaks] = useState(false);
  const [showGlossary, setShowGlossary] = useState(false);
  const [editModeAvailable, setEditModeAvailable] = useState(false);

  // Persist top-level prefs
  useEffect(() => {
    const s = { ...store, lang, theme, regionStyle };
    setStore(s); saveStore(s);
    document.documentElement.setAttribute('data-theme', theme);
    document.documentElement.setAttribute('data-region-style', regionStyle);
    document.documentElement.lang = lang;
  // eslint-disable-next-line
  }, [lang, theme, regionStyle]);

  // Timer tick
  useEffect(() => {
    if (!current || paused || completed) return;
    const id = setInterval(() => setTimer(t => t + 1), 1000);
    return () => clearInterval(id);
  }, [current, paused, completed]);

  // Autosave progress to store
  useEffect(() => {
    if (!current) return;
    const rec = {
      id: current.id, createdAt: current.createdAt,
      rows: current.rows, cols: current.cols, difficulty: current.difficulty,
      puzzle: current.puzzle, regions: current.regions, solution: current.solution,
      grade: current.grade || null,
      techniques: current.techniques || [],
      userGrid, notes: notesToJSON(notes || makeEmptyNotes(current.rows, current.cols)),
      timer, completed, favorite: current.favorite || false,
    };
    const s = loadStore();
    const idx = s.history.findIndex(h => h.id === current.id);
    if (idx >= 0) s.history[idx] = rec; else s.history.unshift(rec);
    s.history = s.history.slice(0, 50);
    s.lastId = current.id;
    s.lang = lang; s.theme = theme; s.regionStyle = regionStyle;
    saveStore(s);
    setStore(s);
  // eslint-disable-next-line
  }, [userGrid, notes, completed, timer]);

  // Edit-mode protocol: register listener first, then announce
  useEffect(() => {
    function onMsg(e) {
      const d = e.data || {};
      if (d.type === '__activate_edit_mode') setShowTweaks(true);
      if (d.type === '__deactivate_edit_mode') setShowTweaks(false);
    }
    window.addEventListener('message', onMsg);
    try { window.parent.postMessage({ type: '__edit_mode_available' }, '*'); setEditModeAvailable(true); } catch(_) {}
    return () => window.removeEventListener('message', onMsg);
  }, []);

  // Check completion
  useEffect(() => {
    if (!current || !userGrid) return;
    if (isComplete(current.puzzle, userGrid, current.solution)) {
      if (!completed) {
        setCompleted(true);
        // Update stats
        const s = loadStore();
        const key = `${current.rows}x${current.cols}-${current.difficulty}`;
        const st = s.stats[key] || { solved: 0, totalTime: 0, best: null };
        st.solved++;
        st.totalTime += timer;
        st.best = st.best == null ? timer : Math.min(st.best, timer);
        s.stats[key] = st;
        saveStore(s); setStore(s);
      }
    }
  }, [userGrid, current]);

  // Keyboard
  useEffect(() => {
    function onKey(e) {
      if (!current || !selected) return;
      const [r, c] = selected;
      if (e.key >= '1' && e.key <= '9') {
        const n = +e.key;
        const size = current.regions.find(reg => reg.some(([rr, cc]) => rr === r && cc === c))?.length || 5;
        if (n <= size) inputNumber(n);
      } else if (e.key === 'Backspace' || e.key === 'Delete' || e.key === '0') {
        eraseCell();
      } else if (e.key === 'ArrowUp') setSelected([Math.max(0, r-1), c]);
      else if (e.key === 'ArrowDown') setSelected([Math.min(current.rows-1, r+1), c]);
      else if (e.key === 'ArrowLeft') setSelected([r, Math.max(0, c-1)]);
      else if (e.key === 'ArrowRight') setSelected([r, Math.min(current.cols-1, c+1)]);
      else if (e.key.toLowerCase() === 'n') setNotesMode(m => !m);
      else if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'z') { e.preventDefault(); undo(); }
      else if ((e.ctrlKey || e.metaKey) && (e.key.toLowerCase() === 'y' || (e.shiftKey && e.key.toLowerCase() === 'z'))) { e.preventDefault(); redo(); }
    }
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  // eslint-disable-next-line
  }, [current, selected, userGrid, notes, notesMode]);

  const pushHistory = () => {
    setHistory(h => [...h.slice(-50), { userGrid: deepCloneGrid(userGrid), notes: cloneNotes(notes) }]);
    setFuture([]);
  };

  const undo = () => {
    setHistory(h => {
      if (h.length === 0) return h;
      const prev = h[h.length - 1];
      setFuture(f => [...f, { userGrid: deepCloneGrid(userGrid), notes: cloneNotes(notes) }]);
      setUserGrid(prev.userGrid);
      setNotes(prev.notes);
      return h.slice(0, -1);
    });
  };
  const redo = () => {
    setFuture(f => {
      if (f.length === 0) return f;
      const nxt = f[f.length - 1];
      setHistory(h => [...h, { userGrid: deepCloneGrid(userGrid), notes: cloneNotes(notes) }]);
      setUserGrid(nxt.userGrid);
      setNotes(nxt.notes);
      return f.slice(0, -1);
    });
  };

  function inputNumber(n) {
    if (!selected || !current) return;
    const [r, c] = selected;
    if (current.puzzle[r][c] !== 0) return;
    pushHistory();
    if (notesMode) {
      const newNotes = cloneNotes(notes);
      if (newNotes[r][c].has(n)) newNotes[r][c].delete(n); else newNotes[r][c].add(n);
      setNotes(newNotes);
    } else {
      const ng = deepCloneGrid(userGrid);
      ng[r][c] = ng[r][c] === n ? 0 : n;
      setUserGrid(ng);
      // Clear notes for this cell
      const nn = cloneNotes(notes);
      nn[r][c] = new Set();
      setNotes(nn);
    }
  }

  function eraseCell() {
    if (!selected || !current) return;
    const [r, c] = selected;
    if (current.puzzle[r][c] !== 0) return;
    pushHistory();
    const ng = deepCloneGrid(userGrid);
    ng[r][c] = 0;
    const nn = cloneNotes(notes);
    nn[r][c] = new Set();
    setUserGrid(ng); setNotes(nn);
  }

  const genHandleRef = useRef(null);

  function startPuzzle(rows, cols, difficulty, attempt = 0) {
    // Clamp to the engine's practical envelope. Per-dimension [4,12] and a
    // 81-cell total cap (beyond that, solveHuman-based generation is too slow).
    rows = Math.max(4, Math.min(12, rows|0));
    cols = Math.max(4, Math.min(12, cols|0));
    if (rows * cols > 81) {
      notify(t.generationTooBig || 'Grid too large (max 81 cells).');
      return;
    }
    if (attempt === 0) setBusy(true);
    // Kick off generation on the Web Worker thread so the UI + Kotoba overlay
    // stay fully responsive even for heavy 9×8 expert puzzles.
    const handle = SuguruWorker.generate({ rows, cols, difficulty });
    genHandleRef.current = handle;
    handle.promise.then((p) => {
      if (genHandleRef.current !== handle) return; // cancelled / superseded
      const rec = {
        id: uid(),
        createdAt: Date.now(),
        rows, cols, difficulty,
        puzzle: p.puzzle, regions: p.regions, solution: p.solution,
        grade: p.grade || null,
        techniques: p.techniques || [],
        favorite: false,
      };
      setCurrent(rec);
      setUserGrid(Array.from({length: rows}, () => new Array(cols).fill(0)));
      setNotes(makeEmptyNotes(rows, cols));
      setHistory([]); setFuture([]);
      setSelected(null); setTimer(0); setPaused(false); setCompleted(false);
      setNotesMode(false); setShowMistakes(false);
      setBusy(false);
    }).catch((e) => {
      if (genHandleRef.current !== handle) return; // cancelled by user
      console.error(e);
      // Transparent retry once — large grids with min=2 occasionally fail on
      // a single partition seed but succeed on the next try.
      if (attempt < 1) {
        startPuzzle(rows, cols, difficulty, attempt + 1);
        return;
      }
      setBusy(false);
      notify(t.generationFailed || 'Generation failed, please try again.');
    });
  }

  function cancelGeneration() {
    if (genHandleRef.current) {
      const h = genHandleRef.current;
      genHandleRef.current = null;
      h.cancel();
    }
    setBusy(false);
  }

  function loadPuzzle(id) {
    const s = loadStore();
    const rec = s.history.find(h => h.id === id) || s.favorites.find(h => h.id === id);
    if (!rec) return;
    setCurrent({
      id: rec.id, createdAt: rec.createdAt,
      rows: rec.rows, cols: rec.cols, difficulty: rec.difficulty,
      puzzle: rec.puzzle, regions: rec.regions, solution: rec.solution,
      grade: rec.grade || null,
      techniques: rec.techniques || [],
      favorite: rec.favorite || false,
    });
    setUserGrid(rec.userGrid || Array.from({length: rec.rows}, () => new Array(rec.cols).fill(0)));
    setNotes(rec.notes ? notesFromJSON(rec.notes) : makeEmptyNotes(rec.rows, rec.cols));
    setTimer(rec.timer || 0);
    setCompleted(!!rec.completed);
    setHistory([]); setFuture([]);
    setSelected(null);
    setShowHistory(false);
  }

  function leavePuzzle() {
    setCurrent(null); setSelected(null);
  }

  async function restartPuzzle() {
    if (!current) return;
    if (!(await confirmAsync(t.confirmRestart))) return;
    setUserGrid(Array.from({length: current.rows}, () => new Array(current.cols).fill(0)));
    setNotes(makeEmptyNotes(current.rows, current.cols));
    setHistory([]); setFuture([]);
    setTimer(0); setCompleted(false);
  }

  function hint() {
    if (!current || !userGrid) return;
    // Find an empty cell, fill with solution value
    const empties = [];
    for (let r = 0; r < current.rows; r++) for (let c = 0; c < current.cols; c++) {
      if (current.puzzle[r][c] === 0 && userGrid[r][c] === 0) empties.push([r, c]);
    }
    if (empties.length === 0) return;
    const [r, c] = empties[Math.floor(Math.random() * empties.length)];
    pushHistory();
    const ng = deepCloneGrid(userGrid);
    ng[r][c] = current.solution[r][c];
    setUserGrid(ng);
    setSelected([r, c]);
  }

  async function solveAll() {
    if (!current) return;
    if (!(await confirmAsync(t.confirmSolve))) return;
    pushHistory();
    const ng = current.solution.map(r => r.slice());
    // Keep givens as-is (they are already in solution)
    setUserGrid(ng);
    setCompleted(true);
  }

  function toggleFavorite() {
    if (!current) return;
    const s = loadStore();
    const idx = s.favorites.findIndex(f => f.id === current.id);
    if (idx >= 0) {
      s.favorites.splice(idx, 1);
      setCurrent({ ...current, favorite: false });
    } else {
      const rec = {
        id: current.id, createdAt: current.createdAt,
        rows: current.rows, cols: current.cols, difficulty: current.difficulty,
        puzzle: current.puzzle, regions: current.regions, solution: current.solution,
        grade: current.grade || null,
        techniques: current.techniques || [],
        favorite: true,
      };
      s.favorites.unshift(rec);
      setCurrent({ ...current, favorite: true });
    }
    saveStore(s); setStore(s);
  }

  // Compute value counts for numpad fade
  const userValueCounts = useMemo(() => {
    const counts = {};
    if (!current || !userGrid) return counts;
    for (let r = 0; r < current.rows; r++) for (let c = 0; c < current.cols; c++) {
      const v = current.puzzle[r][c] || userGrid[r][c];
      if (v) counts[v] = (counts[v]||0)+1;
    }
    return counts;
  }, [userGrid, current]);

  const maxRegionSize = current ? Math.max(...current.regions.map(r => r.length)) : 5;

  // ---- Print view ----
  if (showPrint) {
    return <PrintView t={t} lang={lang} onClose={() => setShowPrint(false)} regionStyle={regionStyle} theme={theme} />;
  }

  if (!current) {
    return (
      <div className="app">
        <TopBar t={t} lang={lang} onLang={setLang} theme={theme} onTheme={setTheme}
          onRules={() => setShowRules(true)}
          onHistory={() => setShowHistory(true)}
          onPrint={() => setShowPrint(true)}
          onTweaks={() => setShowTweaks(v => !v)}
          minimal />
        <Welcome t={t} onGenerate={startPuzzle} store={store} onLoad={loadPuzzle} />
        <RulesModal open={showRules} onClose={() => setShowRules(false)} t={t} />
        <HistoryModal open={showHistory} onClose={() => setShowHistory(false)}
          t={t} onLoad={loadPuzzle} onUpdate={() => setStore(loadStore())} />
        {showTweaks && <TweaksPanel
          t={t} theme={theme} onTheme={setTheme}
          regionStyle={regionStyle} onRegionStyle={setRegionStyle}
          lang={lang} onLang={setLang}
        />}
        <KotobaLoader t={t} lang={lang} visible={busy} onSkip={cancelGeneration} />
        <ConfirmHost t={t} />
      </div>
    );
  }

  const size = `${current.rows}×${current.cols}`;
  const levelLabel = t[current.difficulty] || current.difficulty;

  return (
    <div className="app">
      <TopBar t={t} lang={lang} onLang={setLang} theme={theme} onTheme={setTheme}
        onRules={() => setShowRules(true)}
        onHistory={() => setShowHistory(true)}
        onPrint={() => setShowPrint(true)}
        onTweaks={() => setShowTweaks(v => !v)}
        onHome={leavePuzzle} />

      <div className="play-layout">
        <aside className="meta">
          <div className="meta-row">
            <span className="meta-label">{t.level}</span>
            <span className="meta-val">{levelLabel}</span>
          </div>
          <div className="meta-row">
            <span className="meta-label">{t.size}</span>
            <span className="meta-val">{size}</span>
          </div>
          {current.grade && (
            <button
              type="button"
              className="meta-row meta-row-btn"
              onClick={() => setShowGlossary(true)}
              title={t.glossaryTitle}
            >
              <span className="meta-label">{t.requires}</span>
              <span className="meta-val meta-val-link">{gradeLabel(t, current.grade)}</span>
            </button>
          )}
          <div className="meta-row">
            <span className="meta-label">{t.timer}</span>
            <span className="meta-val mono">{fmtTime(timer)}</span>
          </div>
          <div className="meta-actions">
            <button className="ghost-btn" onClick={() => setPaused(p => !p)}>{paused ? t.resume : t.pause}</button>
            <button className={`ghost-btn ${current.favorite ? 'on' : ''}`} onClick={toggleFavorite}>
              {current.favorite ? '★' : '☆'}
            </button>
          </div>
        </aside>

        <main className="play-main">
          {paused ? (
            <div className="paused-card">
              <div className="brush-seal small">休</div>
              <button className="primary-btn" onClick={() => setPaused(false)}>{t.resume}</button>
            </div>
          ) : (
            <SuguruGrid
              puzzle={current.puzzle}
              regions={current.regions}
              userGrid={userGrid}
              notes={notes}
              solution={current.solution}
              selected={selected}
              onSelect={(r, c) => setSelected([r, c])}
              showMistakes={showMistakes}
              regionStyle={regionStyle}
              theme={theme}
              completed={completed}
            />
          )}

          {completed && (
            <div className="complete-banner">
              <div className="brush-seal tiny">完</div>
              <div>
                <strong>{t.completed}</strong>
                <div className="mono">{fmtTime(timer)}</div>
              </div>
              <button className="primary-btn small" onClick={() => startPuzzle(current.rows, current.cols, current.difficulty)}>
                {t.newGame}
              </button>
            </div>
          )}

          <NumPad
            maxN={maxRegionSize}
            onNum={inputNumber}
            onErase={eraseCell}
            onNotes={() => setNotesMode(m => !m)}
            notesOn={notesMode}
            t={t}
            userValueCounts={userValueCounts}
            disabled={paused || completed}
          />

          <div className="controls-row">
            <button className="ctl-btn" onClick={undo} disabled={history.length === 0}>
              <span className="glyph">↶</span><span>{t.undo}</span>
            </button>
            <button className="ctl-btn" onClick={redo} disabled={future.length === 0}>
              <span className="glyph">↷</span><span>{t.redo}</span>
            </button>
            <button className="ctl-btn" onClick={hint} disabled={completed}>
              <span className="glyph">✦</span><span>{t.hint}</span>
            </button>
            <button className="ctl-btn" onClick={() => setShowMistakes(s => !s)} disabled={completed}>
              <span className="glyph">✓</span><span>{t.check}</span>
            </button>
            <button className="ctl-btn" onClick={restartPuzzle}>
              <span className="glyph">↺</span><span>{t.restart}</span>
            </button>
            <button className="ctl-btn" onClick={solveAll} disabled={completed}>
              <span className="glyph">◈</span><span>{t.solve}</span>
            </button>
            <button className="ctl-btn accent" onClick={leavePuzzle}>
              <span className="glyph">＋</span><span>{t.newGame}</span>
            </button>
          </div>
        </main>
      </div>

      <RulesModal open={showRules} onClose={() => setShowRules(false)} t={t} />
      <HistoryModal open={showHistory} onClose={() => setShowHistory(false)}
        t={t} onLoad={loadPuzzle} onUpdate={() => setStore(loadStore())} />
      {showTweaks && <TweaksPanel
        t={t} theme={theme} onTheme={setTheme}
        regionStyle={regionStyle} onRegionStyle={setRegionStyle}
        lang={lang} onLang={setLang}
      />}
      <KotobaLoader t={t} lang={lang} visible={busy} onSkip={cancelGeneration} />
      <GlossaryModal open={showGlossary} onClose={() => setShowGlossary(false)} t={t} highlight={current.grade} />
      <ConfirmHost t={t} />
    </div>
  );
}

// ---- TopBar ----
const LANG_CHOICES = [
  { code: 'en', label: 'English' },
  { code: 'fr', label: 'Français' },
  { code: 'nl', label: 'Nederlands' },
  { code: 'de', label: 'Deutsch' },
];

function LangPicker({ lang, onLang, t }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(() => {
    if (!open) return;
    function onDoc(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }
    function onKey(e) { if (e.key === 'Escape') setOpen(false); }
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('keydown', onKey);
    return () => { document.removeEventListener('mousedown', onDoc); document.removeEventListener('keydown', onKey); };
  }, [open]);

  return (
    <div className="lang-picker" ref={ref}>
      <button type="button" className="lang-picker-btn" onClick={() => setOpen(v => !v)}
        aria-haspopup="listbox" aria-expanded={open} title={t.language}>
        <span>{lang.toUpperCase()}</span>
        <svg className="lang-picker-caret" width="10" height="10" viewBox="0 0 10 10" aria-hidden>
          <path d="M2 3.5 L5 6.5 L8 3.5" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
        </svg>
      </button>
      {open && (
        <ul className="lang-picker-menu" role="listbox">
          {LANG_CHOICES.map(it => (
            <li key={it.code} role="option" aria-selected={lang === it.code}>
              <button type="button" className={`lang-picker-item ${lang === it.code ? 'is-active' : ''}`}
                onClick={() => { onLang(it.code); setOpen(false); }}>
                <span className="lang-code">{it.code.toUpperCase()}</span>
                <span className="lang-name">{it.label}</span>
              </button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

function TopBar({ t, lang, onLang, theme, onTheme, onRules, onHistory, onPrint, onHome, onTweaks, minimal }) {
  const [menuOpen, setMenuOpen] = useState(false);
  return (
    <header className="topbar">
      <div className="brand" onClick={onHome} style={{cursor: onHome ? 'pointer' : 'default'}}>
        <span className="brand-mark" aria-hidden>数</span>
        <span className="brand-name">{t.appName}</span>
      </div>
      <div className="topbar-right">
        <button className="icon-btn" onClick={onRules} title={t.rules}>?</button>
        <button className="icon-btn" onClick={onHistory} title={t.history}>☰</button>
        <button className="icon-btn" onClick={onPrint} title={t.print}>⎙</button>
        {onTweaks && <button className="icon-btn" onClick={onTweaks} title={t.tweaksTitle}>⚙︎</button>}
        <LangPicker lang={lang} onLang={onLang} t={t} />
      </div>
    </header>
  );
}

// ---- Modals ----
function RulesModal({ open, onClose, t }) {
  return (
    <Modal open={open} onClose={onClose} title={t.rules}>
      <p className="prose">{t.rulesBody}</p>
      <div className="rule-example">
        <MiniExample />
      </div>
      <p className="prose muted">{t.aboutBody}</p>
    </Modal>
  );
}

function MiniExample() {
  // Small demo grid
  const regions = [
    [[0,0],[0,1],[1,0]],
    [[0,2],[1,1],[1,2],[2,2]],
    [[2,0],[2,1]],
  ];
  const values = [[1,2,1],[2,3,2],[1,2,3]];
  return (
    <svg viewBox="0 0 172 172" className="mini-demo">
      {(() => {
        const CELL = 52, PAD = 8;
        const R = 3, C = 3;
        const rid = [[0,0,1],[0,1,1],[2,2,1]];
        const els = [];
        for (let r = 0; r < R; r++) for (let c = 0; c < C; c++) {
          els.push(<rect key={`b-${r}-${c}`} x={PAD+c*CELL} y={PAD+r*CELL} width={CELL} height={CELL}
            fill={rid[r][c]===0?'var(--wash-a)':rid[r][c]===1?'var(--wash-b)':'var(--wash-c)'} />);
          els.push(<text key={`t-${r}-${c}`} x={PAD+c*CELL+CELL/2} y={PAD+r*CELL+CELL/2+3}
            textAnchor="middle" dominantBaseline="middle" fontSize={CELL*0.48}
            fontFamily="'Shippori Mincho', 'Noto Serif JP', serif" fill="var(--ink-given)">{values[r][c]}</text>);
        }
        // Region borders
        const segs = [];
        for (let r = 0; r < R; r++) for (let c = 0; c < C; c++) {
          const x = PAD + c*CELL, y = PAD + r*CELL;
          if (r===0 || rid[r-1][c]!==rid[r][c]) segs.push(`M ${x} ${y} L ${x+CELL} ${y}`);
          if (c===0 || rid[r][c-1]!==rid[r][c]) segs.push(`M ${x} ${y} L ${x} ${y+CELL}`);
          if (r===R-1 || rid[r+1][c]!==rid[r][c]) segs.push(`M ${x} ${y+CELL} L ${x+CELL} ${y+CELL}`);
          if (c===C-1 || rid[r][c+1]!==rid[r][c]) segs.push(`M ${x+CELL} ${y} L ${x+CELL} ${y+CELL}`);
        }
        els.push(<path key="b" d={segs.join(' ')} stroke="var(--ink-border)" strokeWidth="3" fill="none"/>);
        return els;
      })()}
    </svg>
  );
}

function HistoryModal({ open, onClose, t, onLoad, onUpdate }) {
  const [tab, setTab] = useState('history');
  const s = loadStore();
  const items = tab === 'history' ? s.history : s.favorites;

  const del = async (id) => {
    if (!(await confirmAsync(t.confirmDelete))) return;
    const st = loadStore();
    if (tab === 'history') st.history = st.history.filter(h => h.id !== id);
    else st.favorites = st.favorites.filter(f => f.id !== id);
    saveStore(st);
    onUpdate();
  };

  // Stats
  const stats = s.stats || {};
  const statEntries = Object.entries(stats);

  return (
    <Modal open={open} onClose={onClose} title={t.history} maxWidth={640}>
      <div className="tab-row">
        <button className={`tab ${tab==='history'?'on':''}`} onClick={() => setTab('history')}>{t.history}</button>
        <button className={`tab ${tab==='favorites'?'on':''}`} onClick={() => setTab('favorites')}>{t.favorites}</button>
        <button className={`tab ${tab==='stats'?'on':''}`} onClick={() => setTab('stats')}>{t.stats}</button>
      </div>
      {tab !== 'stats' ? (
        items.length === 0 ? <p className="prose muted">{tab==='history'?t.noHistory:t.noFavorites}</p> :
        <ul className="list">
          {items.map(it => (
            <li key={it.id} className="list-item">
              <div className="list-mini">
                <MiniPreview rec={it} />
              </div>
              <div className="list-meta">
                <div className="list-title">{t[it.difficulty]} · {it.rows}×{it.cols}</div>
                <div className="list-sub mono">
                  {fmtTime(it.timer || 0)}{it.completed ? ' · ✓' : ''}{it.favorite ? ' · ★' : ''}
                </div>
              </div>
              <div className="list-actions">
                <button className="ghost-btn small" onClick={() => onLoad(it.id)}>{t.resume2}</button>
                <button className="ghost-btn small" onClick={() => del(it.id)}>{t.delete}</button>
              </div>
            </li>
          ))}
        </ul>
      ) : (
        <div className="stats">
          {statEntries.length === 0 ? <p className="prose muted">—</p> :
            <table className="stats-table">
              <thead><tr><th>{t.size}</th><th>{t.level}</th><th>{t.solved}</th><th>{t.bestTime}</th><th>{t.avgTime}</th></tr></thead>
              <tbody>
                {statEntries.map(([k, v]) => {
                  const [size, diff] = k.split('-');
                  return <tr key={k}>
                    <td className="mono">{size}</td>
                    <td>{t[diff] || diff}</td>
                    <td className="mono">{v.solved}</td>
                    <td className="mono">{fmtTime(v.best)}</td>
                    <td className="mono">{fmtTime(Math.round(v.totalTime/Math.max(1,v.solved)))}</td>
                  </tr>;
                })}
              </tbody>
            </table>
          }
        </div>
      )}
    </Modal>
  );
}

function MiniPreview({ rec }) {
  const R = rec.rows, C = rec.cols;
  const regionId = Array.from({length:R}, () => new Array(C).fill(-1));
  rec.regions.forEach((cells, i) => cells.forEach(([r,c]) => regionId[r][c] = i));
  const CELL = 8, PAD = 2;
  const segs = [];
  for (let r = 0; r < R; r++) for (let c = 0; c < C; c++) {
    const x = PAD + c*CELL, y = PAD + r*CELL;
    if (r===0 || regionId[r-1][c]!==regionId[r][c]) segs.push(`M ${x} ${y} L ${x+CELL} ${y}`);
    if (c===0 || regionId[r][c-1]!==regionId[r][c]) segs.push(`M ${x} ${y} L ${x} ${y+CELL}`);
    if (r===R-1 || regionId[r+1][c]!==regionId[r][c]) segs.push(`M ${x} ${y+CELL} L ${x+CELL} ${y+CELL}`);
    if (c===C-1 || regionId[r][c+1]!==regionId[r][c]) segs.push(`M ${x+CELL} ${y} L ${x+CELL} ${y+CELL}`);
  }
  return (
    <svg viewBox={`0 0 ${C*CELL+PAD*2} ${R*CELL+PAD*2}`} width="64" height="64" preserveAspectRatio="xMidYMid meet">
      <rect x={PAD} y={PAD} width={C*CELL} height={R*CELL} fill="var(--paper-inner)"/>
      <path d={segs.join(' ')} stroke="var(--ink-border)" strokeWidth="1" fill="none"/>
    </svg>
  );
}

// ---- Tweaks panel ----
function TweaksPanel({ t, theme, onTheme, regionStyle, onRegionStyle, lang, onLang }) {
  const update = (key, value) => {
    try { window.parent.postMessage({ type: '__edit_mode_set_keys', edits: { [key]: value } }, '*'); } catch(_) {}
  };
  return (
    <div className="tweaks-panel">
      <div className="tweaks-head">
        <strong>{t.tweaksTitle}</strong>
      </div>
      <div className="tweaks-body">
        <div className="tweak">
          <label>{t.theme}</label>
          <div className="seg">
            {['sumi','kori','yugure'].map(th => (
              <button key={th} className={`seg-btn ${theme===th?'on':''}`}
                onClick={() => { onTheme(th); update('theme', th); }}>{t[th]}</button>
            ))}
          </div>
        </div>
        <div className="tweak">
          <label>{t.regionStyle}</label>
          <div className="seg">
            <button className={`seg-btn ${regionStyle==='ink'?'on':''}`} onClick={() => { onRegionStyle('ink'); update('regionStyle','ink'); }}>{t.rsInk}</button>
            <button className={`seg-btn ${regionStyle==='wash'?'on':''}`} onClick={() => { onRegionStyle('wash'); update('regionStyle','wash'); }}>{t.rsWash}</button>
            <button className={`seg-btn ${regionStyle==='hybrid'?'on':''}`} onClick={() => { onRegionStyle('hybrid'); update('regionStyle','hybrid'); }}>{t.rsHybrid}</button>
          </div>
        </div>
        <div className="tweak">
          <label>{t.language}</label>
          <div className="seg">
            {['en','fr','nl','de'].map(l => (
              <button key={l} className={`seg-btn ${lang===l?'on':''}`} onClick={() => { onLang(l); update('lang', l); }}>{l.toUpperCase()}</button>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}

// ---- Print view ----
function PrintView({ t, lang, onClose, regionStyle, theme }) {
  const [count, setCount] = useState(4);
  const [rows, setRows] = useState(7);
  const [cols, setCols] = useState(7);
  const [diff, setDiff] = useState('medium');
  const [withSolutions, setWithSolutions] = useState(true);
  const [puzzles, setPuzzles] = useState([]);
  const [busy, setBusy] = useState(false);
  const [progress, setProgress] = useState({ done: 0, total: 0 });
  const cancelRef = useRef(false);

  const gen = async () => {
    cancelRef.current = false;
    setBusy(true); setPuzzles([]);
    setProgress({ done: 0, total: count });
    // Give the overlay one frame to paint before we start.
    await new Promise(r => setTimeout(r, 30));
    const arr = [];
    // Loop until we have `count` puzzles. Each slot retries up to 3 times on
    // failure before giving up. Large (≥81) Expert grids occasionally fail on a
    // single partition seed, but succeed on a retry with fresh randomness.
    const MAX_RETRIES = 3;
    for (let i = 0; i < count; i++) {
      if (cancelRef.current) break;
      let p = null;
      for (let attempt = 0; attempt < MAX_RETRIES && !p; attempt++) {
        if (cancelRef.current) break;
        const handle = SuguruWorker.generate({ rows, cols, difficulty: diff });
        cancelRef.currentHandle = handle;
        try {
          p = await handle.promise;
        } catch (err) {
          if (cancelRef.current) break;
          // retry — p stays null
        }
      }
      if (cancelRef.current) break;
      if (p) {
        arr.push(p);
        setProgress({ done: arr.length, total: count });
      }
      // If all retries failed, skip this slot but keep going — better to ship
      // 7/8 than to stall the whole batch.
    }
    cancelRef.currentHandle = null;
    setPuzzles(arr);
    setBusy(false);
  };

  const cancel = () => {
    cancelRef.current = true;
    if (cancelRef.currentHandle) cancelRef.currentHandle.cancel();
  };

  return (
    <div className="print-root">
      <header className="topbar noprint">
        <div className="brand">
          <span className="brand-mark">印</span>
          <span className="brand-name">{t.print}</span>
        </div>
        <div className="topbar-right">
          <button className="ghost-btn" onClick={onClose}>{t.close}</button>
        </div>
      </header>
      <div className="print-setup noprint">
        <div className="card">
          <div className="field">
            <label>{t.size}</label>
            <div className="custom-size">
              <label>{t.rows}<input type="number" min={4} max={12} value={rows} onChange={e => setRows(Math.max(4,Math.min(12,+e.target.value||7)))} /></label>
              <label>{t.cols}<input type="number" min={4} max={12} value={cols} onChange={e => setCols(Math.max(4,Math.min(12,+e.target.value||7)))} /></label>
            </div>
          </div>
          <div className="field">
            <label>{t.difficulty}</label>
            <Segmented value={diff} onChange={setDiff}
              options={[{value:'easy',label:t.easy},{value:'medium',label:t.medium},{value:'hard',label:t.hard},{value:'expert',label:t.expert}]}/>
          </div>
          <div className="field">
            <label>{t.puzzles}</label>
            <div className="seg">
              {[1,2,4,6,8].map(n => <button key={n} className={`seg-btn ${count===n?'on':''}`} onClick={() => setCount(n)}>{n}</button>)}
            </div>
          </div>
          <div className="field">
            <label><input type="checkbox" checked={withSolutions} onChange={e => setWithSolutions(e.target.checked)} /> {t.withSolutions}</label>
          </div>
          <div className="row">
            <button className="primary-btn" onClick={gen} disabled={busy}>{busy ? t.generating : t.generate}</button>
            <button className="ghost-btn" onClick={() => window.print()} disabled={puzzles.length===0 || busy}>⎙ {t.print}</button>
          </div>
          {busy && (
            <div className="print-progress">
              <div className="print-progress-bar">
                <div className="print-progress-fill" style={{width: `${progress.total ? (progress.done/progress.total)*100 : 0}%`}} />
              </div>
              <div className="print-progress-label muted small">
                {progress.done} / {progress.total}
              </div>
            </div>
          )}
          <p className="prose muted small">{t.printBatchHelp}</p>
        </div>
      </div>
      <KotobaLoader t={t} lang={lang} visible={busy} onSkip={cancel} progress={progress} />

      <div className="print-sheet">
        {puzzles.length === 0 && <div className="muted print-empty">—</div>}
        {puzzles.map((p, i) => (
          <div className="print-puzzle" key={i}>
            <div className="print-head">
              <span>{t.puzzle} {i+1}</span>
              <span>{t[p.difficulty]} · {p.rows}×{p.cols}</span>
            </div>
            <PrintGrid puzzle={p.puzzle} regions={p.regions} />
          </div>
        ))}
      </div>
      {withSolutions && puzzles.length > 0 && (
        <div className="solutions-page">
          <h2>{t.solutionsHeading || 'Solutions'}</h2>
          <div className="solutions-grid">
            {puzzles.map((p, i) => (
              <div className="print-puzzle small" key={`s${i}`}>
                <div className="print-head">
                  <span>{t.puzzle} {i+1}</span>
                </div>
                <PrintGrid puzzle={p.solution} regions={p.regions} compact />
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

function PrintGrid({ puzzle, regions, compact }) {
  const R = puzzle.length, C = puzzle[0].length;
  const CELL = compact ? 28 : 48;
  const PAD = 2;
  const regionId = Array.from({length:R}, () => new Array(C).fill(-1));
  regions.forEach((cells, i) => cells.forEach(([r,c]) => regionId[r][c] = i));

  const segs = [];
  for (let r = 0; r < R; r++) for (let c = 0; c < C; c++) {
    const x = PAD + c*CELL, y = PAD + r*CELL;
    if (r===0 || regionId[r-1][c]!==regionId[r][c]) segs.push(`M ${x} ${y} L ${x+CELL} ${y}`);
    if (c===0 || regionId[r][c-1]!==regionId[r][c]) segs.push(`M ${x} ${y} L ${x} ${y+CELL}`);
    if (r===R-1 || regionId[r+1][c]!==regionId[r][c]) segs.push(`M ${x} ${y+CELL} L ${x+CELL} ${y+CELL}`);
    if (c===C-1 || regionId[r][c+1]!==regionId[r][c]) segs.push(`M ${x+CELL} ${y} L ${x+CELL} ${y+CELL}`);
  }
  const thin = [];
  for (let r=1; r<R; r++) thin.push(`M ${PAD} ${PAD+r*CELL} L ${PAD+C*CELL} ${PAD+r*CELL}`);
  for (let c=1; c<C; c++) thin.push(`M ${PAD+c*CELL} ${PAD} L ${PAD+c*CELL} ${PAD+R*CELL}`);

  return (
    <svg viewBox={`0 0 ${C*CELL+PAD*2} ${R*CELL+PAD*2}`} className="print-grid">
      <rect x={PAD} y={PAD} width={C*CELL} height={R*CELL} fill="white" />
      <path d={thin.join(' ')} stroke="#bfbfbf" strokeWidth="0.6" fill="none" />
      <path d={segs.join(' ')} stroke="#111" strokeWidth={compact?1.6:2.2} fill="none"/>
      {puzzle.flatMap((row, r) => row.map((v, c) => v ? (
        <text key={`${r}-${c}`} x={PAD+c*CELL+CELL/2} y={PAD+r*CELL+CELL/2+(compact?2:3)}
          textAnchor="middle" dominantBaseline="middle" fontSize={CELL*(compact?0.55:0.5)}
          fontFamily="'Shippori Mincho', 'Noto Serif JP', Georgia, serif" fill="#111">{v}</text>
      ) : null))}
    </svg>
  );
}

window.SuguruApp = App;
