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

// ---------- AUDIO ----------
let audioCtx = null;
function getAudioCtx() {
  if (!audioCtx) {
    try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
    catch(e) { return null; }
  }
  return audioCtx;
}
function playBeadTone(kind = "ave") {
  const ctx = getAudioCtx(); if (!ctx) return;
  if (ctx.state === "suspended") ctx.resume();
  const now = ctx.currentTime;
  const freqs = kind === "pater" ? [220, 330, 440] : kind === "cross" ? [196, 294, 392, 587] : kind === "medal" ? [261, 392, 523, 784] : [330, 494];
  freqs.forEach((f, i) => {
    const o = ctx.createOscillator();
    const g = ctx.createGain();
    o.type = "sine"; o.frequency.value = f;
    g.gain.setValueAtTime(0, now);
    g.gain.linearRampToValueAtTime(kind === "ave" ? 0.06 : 0.10, now + 0.01 + i*0.02);
    g.gain.exponentialRampToValueAtTime(0.0001, now + 1.6 + i*0.1);
    o.connect(g); g.connect(ctx.destination);
    o.start(now); o.stop(now + 1.8);
  });
}

// Ambient drone (organ-like)
let droneNodes = null;
function startDrone() {
  const ctx = getAudioCtx(); if (!ctx) return;
  if (ctx.state === "suspended") ctx.resume();
  if (droneNodes) return;
  droneNodes = [];
  const gainMaster = ctx.createGain();
  gainMaster.gain.value = 0;
  gainMaster.gain.linearRampToValueAtTime(0.025, ctx.currentTime + 3);
  gainMaster.connect(ctx.destination);
  [110, 164.81, 220].forEach((f, i) => {
    const o = ctx.createOscillator();
    o.type = i === 0 ? "sine" : "triangle";
    o.frequency.value = f;
    const g = ctx.createGain(); g.gain.value = 0.6 - i*0.18;
    // very slow LFO
    const lfo = ctx.createOscillator(); lfo.frequency.value = 0.1 + i*0.05;
    const lfoG = ctx.createGain(); lfoG.gain.value = 0.05;
    lfo.connect(lfoG); lfoG.connect(g.gain);
    o.connect(g); g.connect(gainMaster);
    o.start(); lfo.start();
    droneNodes.push(o, lfo);
  });
  droneNodes.master = gainMaster;
}
function stopDrone() {
  if (!droneNodes) return;
  const ctx = getAudioCtx();
  droneNodes.master.gain.linearRampToValueAtTime(0, ctx.currentTime + 1.5);
  setTimeout(() => {
    droneNodes.forEach(n => { try { n.stop(); } catch(e){} });
    droneNodes = null;
  }, 1600);
}
function chimeBell() {
  const ctx = getAudioCtx(); if (!ctx) return;
  if (ctx.state === "suspended") ctx.resume();
  const now = ctx.currentTime;
  [523.25, 659.25, 783.99, 1046.5].forEach((f, i) => {
    const o = ctx.createOscillator(); const g = ctx.createGain();
    o.type = "sine"; o.frequency.value = f;
    g.gain.setValueAtTime(0, now);
    g.gain.linearRampToValueAtTime(0.08, now + 0.01 + i*0.04);
    g.gain.exponentialRampToValueAtTime(0.0001, now + 3 + i*0.3);
    o.connect(g); g.connect(ctx.destination);
    o.start(now); o.stop(now + 3.5);
  });
}

// ---------- DAY → MYSTERY ----------
function suggestMysteryForToday() {
  // Sun=0 Mon=1 ... Sat=6
  const d = new Date().getDay();
  // Mon, Sat → Joyful; Tue, Fri → Sorrowful; Wed, Sun → Glorious; Thu → Luminous
  return ["glorious","joyful","sorrowful","glorious","luminous","sorrowful","joyful"][d];
}
const DAY_NAMES = ["主日","週一","週二","週三","週四","週五","週六"];

// ---------- LOCAL STORAGE ----------
const LS_KEY = "rosarium_state_v1";
function loadState() {
  try { return JSON.parse(localStorage.getItem(LS_KEY) || "{}"); } catch(e){ return {}; }
}
function saveState(s) {
  try { localStorage.setItem(LS_KEY, JSON.stringify(s)); } catch(e){}
}

// ---------- BEAD VISUAL ----------
function Bead({ pos, role, active, completed, onClick }) {
  if (!pos) return null;
  const isPater = role === "pater";
  const r = isPater ? 24 : 15;
  const baseGrad = isPater ? "url(#woodDarkBase)" : "url(#woodLightBase)";
  const grainFilter = isPater ? "url(#woodGrainDark)" : "url(#woodGrainLight)";
  const showAveGrain = role === "ave";
  const roseReliefPaths = showAveGrain ? [
    `M ${pos.x - 2.3} ${pos.y + 0.3}
     C ${pos.x - 0.8} ${pos.y - 1.8}, ${pos.x + 2.2} ${pos.y - 1.0}, ${pos.x + 1.3} ${pos.y + 1.1}
     C ${pos.x + 0.5} ${pos.y + 2.8}, ${pos.x - 2.0} ${pos.y + 2.1}, ${pos.x - 1.3} ${pos.y + 0.3}`,
    `M ${pos.x - 1.0} ${pos.y - 5.8}
     C ${pos.x + 2.8} ${pos.y - 7.2}, ${pos.x + 6.0} ${pos.y - 4.0}, ${pos.x + 3.2} ${pos.y - 1.3}`,
    `M ${pos.x + 4.0} ${pos.y - 2.0}
     C ${pos.x + 8.0} ${pos.y - 0.8}, ${pos.x + 7.4} ${pos.y + 4.4}, ${pos.x + 3.0} ${pos.y + 3.3}`,
    `M ${pos.x + 1.6} ${pos.y + 4.2}
     C ${pos.x - 0.9} ${pos.y + 7.6}, ${pos.x - 5.8} ${pos.y + 5.4}, ${pos.x - 3.5} ${pos.y + 1.8}`,
    `M ${pos.x - 4.0} ${pos.y + 1.2}
     C ${pos.x - 8.0} ${pos.y - 0.9}, ${pos.x - 5.8} ${pos.y - 5.8}, ${pos.x - 1.9} ${pos.y - 3.4}`,
  ] : [];
  return (
    <g className={`rosary-bead wood-bead ${active ? "is-active" : ""} ${completed ? "is-completed" : ""}`}
       onClick={onClick}
       style={{ cursor: "pointer", transformOrigin: `${pos.x}px ${pos.y}px` }}>
      {active && (
        <circle cx={pos.x} cy={pos.y} r={r * 3.2} fill="url(#haloGrad)" opacity="0.85"/>
      )}
      <circle cx={pos.x} cy={pos.y} r={r} fill={baseGrad} filter="url(#woodShadow)"/>
      <circle cx={pos.x} cy={pos.y} r={r} fill="#000"
              filter={grainFilter} opacity="0.7" pointerEvents="none"/>
      {showAveGrain && (
        <g pointerEvents="none" opacity="0.58">
          <path d={`M ${pos.x - 11} ${pos.y - 5}
                   C ${pos.x - 6} ${pos.y - 8}, ${pos.x + 5} ${pos.y - 8}, ${pos.x + 11} ${pos.y - 4}`}
                fill="none" stroke="#5a3415" strokeWidth="0.75" strokeLinecap="round" opacity="0.5"/>
          <path d={`M ${pos.x - 12} ${pos.y}
                   C ${pos.x - 5} ${pos.y + 3}, ${pos.x + 5} ${pos.y - 3}, ${pos.x + 12} ${pos.y + 1}`}
                fill="none" stroke="#8a5a25" strokeWidth="0.65" strokeLinecap="round" opacity="0.45"/>
          <path d={`M ${pos.x - 8} ${pos.y + 5}
                   C ${pos.x - 2} ${pos.y + 8}, ${pos.x + 6} ${pos.y + 6}, ${pos.x + 10} ${pos.y + 4}`}
                fill="none" stroke="#4a2810" strokeWidth="0.7" strokeLinecap="round" opacity="0.42"/>
          <ellipse cx={pos.x - 2.5} cy={pos.y + 1.5} rx="1.8" ry="0.7"
                   fill="#3d220d" opacity="0.28"/>
        </g>
      )}
      {showAveGrain && (
        <g pointerEvents="none" opacity={active ? "0.5" : "0.42"}>
          {roseReliefPaths.map((d, idx) => (
            <path key={`rose-shadow-${idx}`} d={d} fill="none"
                  stroke="#2f1a0a" strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round"
                  opacity="0.42" transform="translate(0.55 0.65)"/>
          ))}
          {roseReliefPaths.map((d, idx) => (
            <path key={`rose-highlight-${idx}`} d={d} fill="none"
                  stroke="#ffe0a6" strokeWidth="0.45" strokeLinecap="round" strokeLinejoin="round"
                  opacity="0.38" transform="translate(-0.4 -0.5)"/>
          ))}
          {roseReliefPaths.map((d, idx) => (
            <path key={`rose-cut-${idx}`} d={d} fill="none"
                  stroke="#5a3517" strokeWidth="0.55" strokeLinecap="round" strokeLinejoin="round"
                  opacity="0.5"/>
          ))}
        </g>
      )}
      <circle cx={pos.x} cy={pos.y} r={r} fill="url(#beadGloss)" pointerEvents="none"/>
      <circle cx={pos.x} cy={pos.y} r={r} fill="none"
              stroke={active ? "#ffe6a0" : isPater ? "#0e0703" : "#2a1808"}
              strokeWidth={active ? 1.6 : 0.8} opacity={active ? 0.95 : 0.85}/>
      {completed && !active && (
        <circle cx={pos.x} cy={pos.y} r={r * 0.7}
                fill="rgba(255,200,120,0.18)" pointerEvents="none"/>
      )}
    </g>
  );
}

// ---------- ROSARY VISUAL ----------
function Rosary({ geom, seq, currentIdx, completed, onBeadClick, mysteryHue }) {
  if (!geom) return null;
  const hue = mysteryHue ?? 45;

  return (
    <svg viewBox="140 160 830 1040"
         className="rosary-svg"
         preserveAspectRatio="xMidYMid meet">
      <defs>
        {/* Dark wood base — Our Father / large beads (rosewood/walnut) */}
        <radialGradient id="woodDarkBase" cx="0.32" cy="0.28" r="0.85">
          <stop offset="0%"  stopColor="#7a4220"/>
          <stop offset="35%" stopColor="#4a2210"/>
          <stop offset="80%" stopColor="#241008"/>
          <stop offset="100%" stopColor="#0e0503"/>
        </radialGradient>
        {/* Light wood base — Hail Mary / small beads (sandalwood) */}
        <radialGradient id="woodLightBase" cx="0.32" cy="0.28" r="0.85">
          <stop offset="0%"  stopColor="#f5d8a8"/>
          <stop offset="40%" stopColor="#d09c6e"/>
          <stop offset="85%" stopColor="#7a4f24"/>
          <stop offset="100%" stopColor="#36200e"/>
        </radialGradient>
        {/* Realistic grain via stretched turbulence — like tree rings */}
        <filter id="woodGrainDark" x="-10%" y="-10%" width="120%" height="120%">
          <feTurbulence type="turbulence" baseFrequency="0.022 0.55"
                        numOctaves="3" seed="7" stitchTiles="stitch" result="noise"/>
          <feColorMatrix in="noise" values="
             0 0 0 0 0.18
             0 0 0 0 0.09
             0 0 0 0 0.04
             0 0 0 0.7 0" result="tinted"/>
          <feComposite in="tinted" in2="SourceGraphic" operator="in"/>
        </filter>
        <filter id="woodGrainLight" x="-10%" y="-10%" width="120%" height="120%">
          <feTurbulence type="turbulence" baseFrequency="0.020 0.72"
                        numOctaves="4" seed="11" stitchTiles="stitch" result="noise"/>
          <feColorMatrix in="noise" values="
             0 0 0 0 0.38
             0 0 0 0 0.21
             0 0 0 0 0.07
             0 0 0 0.72 0" result="tinted"/>
          <feComposite in="tinted" in2="SourceGraphic" operator="in"/>
        </filter>
        {/* Crucifix grain — vertical orientation (frequencies swapped) */}
        <filter id="woodGrainCross" x="-10%" y="-10%" width="120%" height="120%">
          <feTurbulence type="turbulence" baseFrequency="0.55 0.022"
                        numOctaves="3" seed="3" stitchTiles="stitch" result="noise"/>
          <feColorMatrix in="noise" values="
             0 0 0 0 0.18
             0 0 0 0 0.09
             0 0 0 0 0.04
             0 0 0 0.6 0" result="tinted"/>
          <feComposite in="tinted" in2="SourceGraphic" operator="in"/>
        </filter>
        {/* Specular gloss highlight, light from upper-left */}
        <radialGradient id="beadGloss" cx="0.30" cy="0.25" r="0.40">
          <stop offset="0%"  stopColor="#fff0d2" stopOpacity="0.85"/>
          <stop offset="55%" stopColor="#fff0d2" stopOpacity="0.18"/>
          <stop offset="100%" stopColor="#fff0d2" stopOpacity="0"/>
        </radialGradient>
        <filter id="woodShadow" x="-30%" y="-30%" width="160%" height="170%">
          <feDropShadow dx="0.4" dy="1.8" stdDeviation="1.0"
                        floodColor="#0a0402" floodOpacity="0.6"/>
        </filter>
        {/* Medal kept brass-gold to contrast with wood */}
        <radialGradient id="medalGrad" cx="0.5" cy="0.5" r="0.5">
          <stop offset="0%" stopColor="#fff7d8"/>
          <stop offset="60%" stopColor="#d9b25a"/>
          <stop offset="100%" stopColor="#6e4d18"/>
        </radialGradient>
        <radialGradient id="haloGrad" cx="0.5" cy="0.5" r="0.5">
          <stop offset="0%" stopColor={`oklch(0.92 0.18 ${hue})`} stopOpacity="0.85"/>
          <stop offset="60%" stopColor={`oklch(0.78 0.14 ${hue})`} stopOpacity="0.3"/>
          <stop offset="100%" stopColor={`oklch(0.5 0.1 ${hue})`} stopOpacity="0"/>
        </radialGradient>
        <filter id="medalGlow" x="-50%" y="-50%" width="200%" height="200%">
          <feGaussianBlur stdDeviation="3" result="b"/>
          <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
        </filter>
      </defs>

      {/* CORD — loop (warm wood-dark thread) */}
      <path d={geom.loopPathD} fill="none"
            stroke="#3a2410" strokeWidth="2.2" strokeOpacity="0.55"
            strokeLinecap="round"/>
      <path d={geom.loopPathD} fill="none"
            stroke="#7a5320" strokeWidth="0.9" strokeOpacity="0.6"
            strokeLinecap="round"/>
      {/* CORD — pendant */}
      <path d={geom.pendantPathD} fill="none"
            stroke="#3a2410" strokeWidth="2.2" strokeOpacity="0.55"
            strokeLinecap="round"/>
      <path d={geom.pendantPathD} fill="none"
            stroke="#7a5320" strokeWidth="0.9" strokeOpacity="0.6"
            strokeLinecap="round"/>

      {/* Render beads from seqPositions; ghost-* steps share their bead with
          a neighbouring real one and are skipped here. The active highlight is
          driven by position match so a single pater stays lit through the
          gb → fatima → announce → of cluster. Active beads render last so
          nearby medal/ave elements cannot cover the current bead. */}
      {(() => {
        return window.getRosaryRenderItems(geom.seqPositions, currentIdx, {}).map((item) => {
          const i = item.index;
          const pos = item.pos;
          const isActive = item.isActive;
          const isCompleted = !isActive && completed.has(i);
          if (pos.kind === "cross") {
            return (
              <g key={i}
                 className="rosary-special-bead"
                 onClick={() => onBeadClick(i)}
                 style={{ cursor: "pointer" }}>
                {isActive && (
                  <circle cx={pos.x} cy={pos.y + 4} r="44" fill="url(#haloGrad)" opacity="0.85"/>
                )}
                <g transform={`translate(${pos.x}, ${pos.y + 4})`} filter="url(#woodShadow)">
                  <rect x="-4" y="-14" width="8" height="48" rx="1.2" fill="url(#woodDarkBase)"/>
                  <rect x="-4" y="-14" width="8" height="48" rx="1.2" fill="#000"
                        filter="url(#woodGrainCross)" opacity="0.55"/>
                  <rect x="-17" y="-1" width="34" height="8" rx="1.2" fill="url(#woodDarkBase)"/>
                  <rect x="-17" y="-1" width="34" height="8" rx="1.2" fill="#000"
                        filter="url(#woodGrainCross)" opacity="0.55"/>
                  <rect x="-4" y="-14" width="8" height="48" rx="1.2" fill="none"
                        stroke={isActive ? "#ffe6a0" : "#1a0d05"} strokeWidth="0.6" opacity="0.7"/>
                  <rect x="-17" y="-1" width="34" height="8" rx="1.2" fill="none"
                        stroke={isActive ? "#ffe6a0" : "#1a0d05"} strokeWidth="0.6" opacity="0.7"/>
                  <rect x="-7" y="-17" width="14" height="3.4" rx="0.5" fill="#c2924b"/>
                  <circle cx="0" cy="3" r="1.6" fill="#1a0d05" opacity="0.7"/>
                </g>
              </g>
            );
          }
          if (pos.kind === "medal") {
            return (
              <g key={i}
                 className="rosary-special-bead"
                 onClick={() => onBeadClick(i)}
                 style={{ cursor: "pointer" }}>
                {isActive && (
                  <circle cx={pos.x} cy={pos.y} r="50" fill="url(#haloGrad)" opacity="0.85"/>
                )}
                <circle cx={pos.x} cy={pos.y} r="22" fill="url(#medalGrad)"
                        stroke={isActive ? "#ffe6a0" : "#7a5320"} strokeWidth="1.2"
                        filter="url(#medalGlow)"/>
                <text x={pos.x} y={pos.y + 5} textAnchor="middle"
                      fontFamily="Cinzel, serif" fontSize="20" fill="#3a2410"
                      fontWeight="700" pointerEvents="none">M</text>
                <circle cx={pos.x} cy={pos.y} r="18" fill="none"
                        stroke="#7a5320" strokeWidth="0.4" strokeDasharray="1 1.5"/>
              </g>
            );
          }
          return (
            <Bead key={i} pos={pos} role={pos.kind === "pater" ? "pater" : pos.kind === "ave" ? "ave" : pos.kind}
                  active={isActive} completed={isCompleted}
                  onClick={() => onBeadClick(i)} />
          );
        });
      })()}
    </svg>
  );
}

// ---------- PRAYER PANEL (floating on top) ----------
function PrayerPanel({ step, mystery, currentIdx, total, language }) {
  if (!step) return null;
  const prayer = step.prayer ? window.PRAYERS[step.prayer] : null;
  const prayerBody = prayer ? (prayer[language] || prayer.modern) : null;
  const isAnnounce = step.kind === "announce";
  return (
    <div className="prayer-panel" key={currentIdx}>
      <div className="prayer-meta">
        <span className="prayer-meta-label">{step.label}</span>
        <span className="prayer-meta-progress">{currentIdx + 1} / {total}</span>
      </div>

      {isAnnounce && step.mystery && (
        <div className="announce-block">
          <div className="announce-decade">第 {["一","二","三","四","五"][step.decade]} 端</div>
          <h2 className="announce-title">{step.mystery.title}</h2>
          <div className="announce-sub">{step.mystery.subtitle}</div>
          <div className="announce-scripture">{step.mystery.scripture}</div>
          <div className="announce-fruit">默想此端，求得 <span>{step.mystery.fruit}</span> 之恩</div>
        </div>
      )}

      {prayer && (
        <div className="prayer-block">
          <div className="prayer-title">{prayer.title}</div>
          <div className="prayer-body">
            {prayerBody.split("\n").map((line, i) => (
              <div key={i} className="prayer-line" style={{animationDelay: `${i*120}ms`}}>{line}</div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

// ---------- MYSTERY PICKER ----------
function MysteryPicker({ current, onPick, suggested }) {
  const keys = ["joyful","luminous","sorrowful","glorious"];
  return (
    <div className="mystery-picker">
      {keys.map(k => {
        const m = window.MYSTERIES[k];
        const isCur = current === k;
        const isSug = suggested === k;
        return (
          <button key={k}
                  className={`mystery-chip ${isCur ? "is-current" : ""} ${isSug ? "is-suggested" : ""}`}
                  onClick={() => onPick(k)}
                  style={{ "--hue": m.hue }}>
            <span className="chip-name">{m.name}</span>
            <span className="chip-days">{m.days}</span>
            {isSug && <span className="chip-suggest">今日</span>}
          </button>
        );
      })}
    </div>
  );
}

function toneForStep(step) {
  if (!step) return "ave";
  if (step.bead === "crucifix") return "cross";
  if (step.bead === "pater" || step.kind === "of" || step.kind === "announce") return "pater";
  if (step.bead === "medal") return "medal";
  return "ave";
}

// ---------- MAIN APP ----------
function App() {
  const [tweaks, setTweaks] = useTweaks({
    beadStyle: "sandalwood",
    autoAdvance: false,
    autoSeconds: 12,
    audioBeads: true,
    audioDrone: true,
    background: "candlelight",
    language: "modern",
  });

  const suggested = useMemo(() => suggestMysteryForToday(), []);
  const persisted = useMemo(() => loadState(), []);
  const [mysteryKey, setMysteryKey] = useState(persisted.mysteryKey || suggested);
  const [currentIdx, setCurrentIdx] = useState(persisted.currentIdx || 0);
  const [completed, setCompleted] = useState(() => new Set(persisted.completed || []));
  const [history, setHistory] = useState(persisted.history || []); // array of ISO dates
  const [streak, setStreak] = useState(persisted.streak || 0);
  const [showCompleted, setShowCompleted] = useState(false);
  const [paused, setPaused] = useState(true);

  const seq = useMemo(() => window.buildBeadSequence(mysteryKey), [mysteryKey]);
  const mystery = window.MYSTERIES[mysteryKey];

  const geom = useMemo(() => {
    if (typeof document === "undefined") return null;
    return window.computeRosaryGeometry(seq, { width: 1100, height: 1300, cy: 440, rx: 380, ry: 380 });
  }, [seq]);

  // Reset progress only when user actively switches mystery (not on initial mount,
  // so persisted currentIdx survives a reload).
  const isFirstMount = useRef(true);
  useEffect(() => {
    if (isFirstMount.current) {
      isFirstMount.current = false;
      return;
    }
    setCurrentIdx(0);
    setCompleted(new Set());
  }, [mysteryKey]);

  // persist
  useEffect(() => {
    saveState({ mysteryKey, currentIdx, completed: [...completed], history, streak });
  }, [mysteryKey, currentIdx, completed, history, streak]);

  // Audio drone
  useEffect(() => {
    if (tweaks.audioDrone && !paused) startDrone();
    else stopDrone();
    return () => stopDrone();
  }, [tweaks.audioDrone, paused]);

  // Auto advance
  useEffect(() => {
    if (paused || !tweaks.autoAdvance) return;
    if (currentIdx >= seq.length - 1) return;
    const t = setTimeout(() => advance(), tweaks.autoSeconds * 1000);
    return () => clearTimeout(t);
  }, [paused, tweaks.autoAdvance, tweaks.autoSeconds, currentIdx, seq.length]);

  const advance = useCallback(() => {
    setCompleted(prev => {
      const next = new Set(prev); next.add(currentIdx); return next;
    });
    if (currentIdx >= seq.length - 1) {
      // finished
      finishRosary();
      return;
    }
    const nextIdx = currentIdx + 1;
    setCurrentIdx(nextIdx);
    if (tweaks.audioBeads) {
      playBeadTone(toneForStep(seq[nextIdx]));
    }
  }, [currentIdx, seq, tweaks.audioBeads]);

  const finishRosary = useCallback(() => {
    setCompleted(prev => { const n = new Set(prev); n.add(seq.length - 1); return n; });
    if (tweaks.audioBeads) chimeBell();
    setShowCompleted(true);
    // Update history + streak
    const today = new Date().toISOString().slice(0,10);
    setHistory(prev => {
      if (prev.includes(today)) return prev;
      const next = [...prev, today].sort();
      // streak calc
      let st = 1; const dates = next.slice().reverse();
      for (let i = 1; i < dates.length; i++) {
        const a = new Date(dates[i-1]); const b = new Date(dates[i]);
        const diff = (a - b) / (1000*60*60*24);
        if (Math.round(diff) === 1) st++; else break;
      }
      setStreak(st);
      return next;
    });
  }, [seq.length, tweaks.audioBeads]);

  const goBack = () => {
    if (currentIdx === 0) return;
    setCompleted(prev => { const n = new Set(prev); n.delete(currentIdx - 1); return n; });
    setCurrentIdx(currentIdx - 1);
  };

  const handleBeadClick = (idx) => {
    if (idx < 0 || idx >= seq.length) return;
    setCurrentIdx(idx);
    setPaused(false);
    if (tweaks.audioBeads) {
      playBeadTone(toneForStep(seq[idx]));
    }
  };

  const handleStart = () => {
    setPaused(false);
    if (currentIdx === 0 && completed.size === 0) {
      // first tap
      if (tweaks.audioBeads) playBeadTone("cross");
    }
  };

  const reset = () => {
    setCurrentIdx(0); setCompleted(new Set()); setShowCompleted(false); setPaused(true);
  };

  // Keyboard
  useEffect(() => {
    const onKey = (e) => {
      if (e.key === " " || e.key === "Enter" || e.key === "ArrowRight") { e.preventDefault(); advance(); setPaused(false); }
      else if (e.key === "ArrowLeft") { e.preventDefault(); goBack(); }
      else if (e.key.toLowerCase() === "p") setPaused(p => !p);
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [advance, currentIdx]);

  const step = seq[currentIdx];

  return (
    <div className={`app-root bg-${tweaks.background}`}>
      <div className="ambient-layer">
        <div className="candle candle-1"/>
        <div className="candle candle-2"/>
        <div className="candle candle-3"/>
        <div className="candle candle-4"/>
        <div className="incense"/>
        <div className="vignette"/>
      </div>

      <header className="app-header">
        <div className="brand">
          <span className="brand-mark">✦</span>
          <span className="brand-name">Rosarium</span>
          <span className="brand-tag">玫瑰經・默禱</span>
        </div>
        <div className="header-meta">
          <div className="meta-item"><span className="meta-label">今日</span><span className="meta-value">{DAY_NAMES[new Date().getDay()]}</span></div>
          <div className="meta-item"><span className="meta-label">連續</span><span className="meta-value">{streak} 天</span></div>
          <div className="meta-item"><span className="meta-label">總計</span><span className="meta-value">{history.length} 次</span></div>
        </div>
      </header>

      <MysteryPicker current={mysteryKey} onPick={setMysteryKey} suggested={suggested}/>

      <div className="stage">
        <div className="rosary-frame" style={{ "--hue": mystery.hue }}>
          <Rosary
            geom={geom}
            seq={seq}
            currentIdx={currentIdx}
            completed={completed}
            onBeadClick={handleBeadClick}
            mysteryHue={mystery.hue}
          />
        </div>

        <PrayerPanel step={step} mystery={mystery} currentIdx={currentIdx} total={seq.length} language={tweaks.language}/>

        {paused && currentIdx === 0 && completed.size === 0 && (
          <div className="welcome-overlay" onClick={handleStart}>
            <div className="welcome-inner">
              <div className="welcome-mark">✠</div>
              <div className="welcome-title">入靜</div>
              <div className="welcome-sub">默念 {mystery.name}</div>
              <div className="welcome-cta">點擊念珠或十字架以開始</div>
              <div className="welcome-hint">空白鍵 / → 前進　　← 返回　　P 暫停</div>
            </div>
          </div>
        )}

        {showCompleted && (
          <div className="welcome-overlay completed" onClick={() => setShowCompleted(false)}>
            <div className="welcome-inner">
              <div className="welcome-mark">✦</div>
              <div className="welcome-title">圓滿</div>
              <div className="welcome-sub">願主賜你平安</div>
              <div className="welcome-cta">已默想 {mystery.name}・連續 {streak} 天</div>
            </div>
          </div>
        )}
      </div>

      <footer className="app-footer">
        <button className="ctrl" onClick={goBack} title="上一個" aria-label="上一念">←</button>
        <button className="ctrl primary" onClick={() => { setPaused(false); advance(); }} title="下一個" aria-label="下一念">
          下一念 →
        </button>
        <button className="ctrl" onClick={() => setPaused(p => !p)} title="暫停/繼續" aria-label={paused ? "繼續" : "暫停"}>
          {paused ? "▶" : "❚❚"}
        </button>
        <button className="ctrl" onClick={reset} title="重新開始" aria-label="重新開始">↺</button>
      </footer>

      <TweaksPanel>
        <TweakSection title="念珠樣式">
          <TweakRadio label="木種" value={tweaks.beadStyle}
            options={[{value:"sandalwood", label:"檀香木"},{value:"rosewood", label:"紫檀木"},{value:"ebony", label:"烏木"}]}
            onChange={v => setTweaks("beadStyle", v)}/>
          <TweakRadio label="背景" value={tweaks.background}
            options={[{value:"candlelight", label:"燭光"},{value:"basilica", label:"教堂"},{value:"midnight", label:"午夜"}]}
            onChange={v => setTweaks("background", v)}/>
        </TweakSection>
        <TweakSection title="經文語言">
          <TweakRadio label="" value={tweaks.language}
            options={[{value:"modern", label:"白話文"},{value:"classical", label:"文言文"}]}
            onChange={v => setTweaks("language", v)}/>
        </TweakSection>
        <TweakSection title="念誦控制">
          <TweakToggle label="自動前進" value={tweaks.autoAdvance} onChange={v => setTweaks("autoAdvance", v)}/>
          <TweakSlider label="每念秒數" value={tweaks.autoSeconds} min={4} max={30} step={1} onChange={v => setTweaks("autoSeconds", v)}/>
        </TweakSection>
        <TweakSection title="音效">
          <TweakToggle label="念珠音" value={tweaks.audioBeads} onChange={v => setTweaks("audioBeads", v)}/>
          <TweakToggle label="環境聖樂" value={tweaks.audioDrone} onChange={v => setTweaks("audioDrone", v)}/>
        </TweakSection>
      </TweaksPanel>

      <BeadStyleDefs style={tweaks.beadStyle}/>
    </div>
  );
}

function BeadStyleDefs({ style }) {
  if (style === "rosewood") {
    return (
      <svg width="0" height="0" style={{position:"absolute"}}>
        <defs>
          <radialGradient id="woodDarkBase" cx="0.32" cy="0.28" r="0.85">
            <stop offset="0%"  stopColor="#9b3a28"/>
            <stop offset="40%" stopColor="#5e1d12"/>
            <stop offset="85%" stopColor="#2c0c08"/>
            <stop offset="100%" stopColor="#160604"/>
          </radialGradient>
          <radialGradient id="woodLightBase" cx="0.32" cy="0.28" r="0.85">
            <stop offset="0%"  stopColor="#e0a280"/>
            <stop offset="45%" stopColor="#a86a48"/>
            <stop offset="90%" stopColor="#5e2e1a"/>
            <stop offset="100%" stopColor="#2a1208"/>
          </radialGradient>
        </defs>
      </svg>
    );
  }
  if (style === "ebony") {
    return (
      <svg width="0" height="0" style={{position:"absolute"}}>
        <defs>
          <radialGradient id="woodDarkBase" cx="0.32" cy="0.28" r="0.85">
            <stop offset="0%"  stopColor="#3a2e26"/>
            <stop offset="40%" stopColor="#15100c"/>
            <stop offset="85%" stopColor="#070504"/>
            <stop offset="100%" stopColor="#000"/>
          </radialGradient>
          <radialGradient id="woodLightBase" cx="0.32" cy="0.28" r="0.85">
            <stop offset="0%"  stopColor="#a07a52"/>
            <stop offset="45%" stopColor="#5e3e22"/>
            <stop offset="90%" stopColor="#2a1808"/>
            <stop offset="100%" stopColor="#100804"/>
          </radialGradient>
        </defs>
      </svg>
    );
  }
  return null;
}

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