/* Devonew — app shell, intro, state, tweaks */
const { useState, useEffect, useRef } = React;
const C = window.DEVO_CONTENT;

/* ---------- contexto multi-tenant + multi-membro ----------
   A igreja-demo (comunidadevida) é a vitrine: um membro só, com dados ricos.
   Qualquer outra igreja é real → CADA membro tem o seu acesso, exige sessão
   (login em /acesso) e os dados sincronizam com o servidor (/api/state, D1);
   o localStorage vira cache local para abrir offline (PWA). */
const _tenant = (window.DEVO_TENANT || '').toLowerCase();
const CHURCH_IS_DEMO = !_tenant || _tenant === 'comunidadevida';
const IS_DEMO_MEMBER = CHURCH_IS_DEMO;
const _titleCase = (s) => (s || '').replace(/-/g, ' ').replace(/\b\w/g, (ch) => ch.toUpperCase()).trim();
const _firstOf = (n) => (n || '').trim().split(/\s+/)[0] || 'Membro';
const _siteSaved = (() => { try { return JSON.parse(localStorage.getItem('devonew_site') || 'null'); } catch { return null; } })();
const _apiChurch = window.DEVO_CHURCH || null; // identidade real da igreja (D1), injetada pelo worker
// membro logado: confirmado pelo boot via /api/me; o localStorage guarda o
// último login como cache para o modo offline. Na demo é ignorado.
let CURRENT_MEMBER = (() => { try { return JSON.parse(localStorage.getItem('devonew_current_member') || 'null'); } catch { return null; } })();
let MEMBER_KEY = 'demo';
let _mprefix = '';
let ME = null;
let CHURCH_NAME = IS_DEMO_MEMBER
  ? C.church
  : ((_siteSaved && _siteSaved.name) || (_apiChurch && _apiChurch.name) || _titleCase(_tenant) || 'Sua Igreja');

// igreja real: zera o conteúdo de demonstração da Comunidade Vida, preservando
// os templates genéricos (jornada, níveis, medalhas, metas, devocional, versículo).
// Roda no boot, DEPOIS de a sessão confirmar quem é o membro.
function initMemberContext() {
  MEMBER_KEY = IS_DEMO_MEMBER ? 'demo' : ((CURRENT_MEMBER && CURRENT_MEMBER.key) || 'convidado');
  _mprefix = IS_DEMO_MEMBER ? '' : ('m_' + MEMBER_KEY + '_');
  ME = IS_DEMO_MEMBER
    ? { name: C.user.name, first: C.user.first, initials: C.initials }
    : { name: (CURRENT_MEMBER && CURRENT_MEMBER.name) || 'Membro',
        first: _firstOf((CURRENT_MEMBER && CURRENT_MEMBER.name) || 'Membro'),
        initials: (CURRENT_MEMBER && CURRENT_MEMBER.initials) || 'M' };
  if (!IS_DEMO_MEMBER) {
    C.user = { name: ME.name, first: ME.first, church: CHURCH_NAME, initials: ME.initials };
    C.name = ME.name; C.first = ME.first; C.church = CHURCH_NAME; C.initials = ME.initials;
    C.scale = null;                          // sem escala até a igreja montar
    C.cell = null;                           // membro ainda sem célula
    C.leaderboard = { mensal: [], geral: [] };
    C.prayers = [];                          // lista pessoal de oração vazia
    C.journalSeed = '';
    C.courses = [];                          // nenhum curso matriculado
    C.xpLogSeed = [];
    C.catalog = []; C.library = []; C.seminars = [];
    C.quick = [
      { id: 'oracao', t: 'Central de Oração', s: 'Seus pedidos', icon: 'pray' },
      { id: 'trilha', t: 'Trilha', s: '', icon: 'sparkle' },
      { id: 'celula', t: 'Minha Célula', s: 'Comunhão', icon: 'users', soft: true },
      { id: 'curso', t: 'Meus Cursos', s: 'Aprenda', icon: 'book', soft: true },
    ];
  }
}
const XP0 = IS_DEMO_MEMBER ? C.level.xp : 0;
const STREAK0 = IS_DEMO_MEMBER ? 12 : 0;
// chaves compartilhadas pela igreja (escritas pelo admin / device); as demais
// são por-membro (namespace m_<membro>_), isolando os dados de cada acesso.
const SHARED_KEYS = new Set(['theme', 'site', 'catalog', 'library', 'events_pub', 'journey_def', 'devotionals_pub']);

// dia local em ISO — o devocional reinicia todo dia; a sequência quebra
// quando passa um dia inteiro sem concluir.
const dayISO = (d) => { const x = d || new Date(); return `${x.getFullYear()}-${String(x.getMonth() + 1).padStart(2, '0')}-${String(x.getDate()).padStart(2, '0')}`; };
const todayISO = () => dayISO();
const yesterISO = () => dayISO(new Date(Date.now() - 864e5));

// deriva o nível atual a partir do XP real (escada em C.levels) — corrige o nível "congelado"
function levelFor(xp) {
  const L = C.levels || [{ n: C.level.n, name: C.level.name, floor: C.level.floor }];
  let i = 0;
  for (let k = 0; k < L.length; k++) if (xp >= L[k].floor) i = k;
  const cur = L[i], nxt = L[i + 1];
  return {
    n: cur.n, name: cur.name, floor: cur.floor,
    ceil: nxt ? nxt.floor : cur.floor,
    next: nxt ? nxt.name : 'Nível máximo',
    max: !nxt,
  };
}

// semana corrente, para as metas semanais (zera a cada 7 dias)
const weekKey = () => Math.floor(Date.now() / 6048e5);
const freshWeek = (w) => (w && w.week === weekKey()) ? w : { week: weekKey(), devo: 0, chapters: 0, lessons: 0, claimed: [] };

const LS = {
  _k(k) { return 'devonew_' + (SHARED_KEYS.has(k) ? k : _mprefix + k); },
  get(k, d) { try { const v = localStorage.getItem(this._k(k)); return v == null ? d : JSON.parse(v); } catch { return d; } },
  set(k, v) {
    try { localStorage.setItem(this._k(k), JSON.stringify(v)); } catch {}
    if (!SHARED_KEYS.has(k)) queueState(k, v); // igreja real → espelha no servidor
  },
};

/* ---------- sincronização com o servidor (igrejas reais) ----------
   Escritas por-membro entram numa fila e seguem em lote para o /api/state
   (debounce). O localStorage continua valendo como cache: o app abre offline
   com o último estado e empurra as mudanças quando a rede voltar. */
let _syncOn = false;            // ligado pelo boot quando há sessão real
const _pend = {};
let _flushT = null;
let _retryMs = 5000;
function queueState(key, value) {
  if (!_syncOn) return;
  _pend[key] = value;
  clearTimeout(_flushT);
  _flushT = setTimeout(flushState, 700);
}
async function flushState() {
  const keys = Object.keys(_pend);
  if (!keys.length) return;
  const set = {};
  keys.forEach((k) => { set[k] = _pend[k]; delete _pend[k]; });
  try {
    const r = await fetch('/api/state', {
      method: 'PUT', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ set }), keepalive: true,
    });
    if (!r.ok) throw new Error('HTTP ' + r.status);
    _retryMs = 5000;
  } catch (e) {
    // sem rede / servidor indisponível → devolve pra fila e tenta de novo
    keys.forEach((k) => { if (!(k in _pend)) _pend[k] = set[k]; });
    clearTimeout(_flushT);
    _flushT = setTimeout(flushState, _retryMs);
    _retryMs = Math.min(_retryMs * 2, 60000);
  }
}
window.DEVO_SYNC = queueState;  // bible.jsx usa p/ sincronizar a leitura bíblica
document.addEventListener('visibilitychange', () => { if (document.hidden) flushState(); });
window.addEventListener('pagehide', () => flushState());

const ACCENTS = {
  oliveira: { h: 145, c: 0.055, label: 'Oliveira' },
  pomba:    { h: 245, c: 0.070, label: 'Pomba' },
  alvorada: { h: 70,  c: 0.105, label: 'Alvorada' },
  argila:   { h: 33,  c: 0.080, label: 'Argila' },
};

/* ============================================================
   Intro animation
   ============================================================ */
function Intro({ onDone }) {
  const frozen = new URLSearchParams(location.search).get('freeze'); // 'phrase' | 'word'
  const [phase, setPhase] = useState(frozen === 'word' ? 'word' : 'phrase');
  const [out, setOut] = useState(false);
  const timers = useRef([]);

  useEffect(() => {
    if (frozen) return;
    const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
    const seq = reduce
      ? [[0, () => setPhase('phrase')], [1400, () => setPhase('word')], [3000, () => setOut(true)], [3700, onDone]]
      : [[4200, () => setPhase('word')], [6500, () => setOut(true)], [7350, onDone]];
    timers.current = seq.map(([t, fn]) => setTimeout(fn, t));
    return () => timers.current.forEach(clearTimeout);
  }, []);

  const skip = () => { timers.current.forEach(clearTimeout); setOut(true); setTimeout(onDone, 650); };

  return (
    <div className={`intro ${out ? 'fade-out' : ''}`} onClick={skip}>
      {phase === 'phrase' ? (
        <div className={`intro-line ${frozen ? '' : 'intro-phrase'}`}>Breathing new life into your daily devotion</div>
      ) : (
        <div className="intro-word" style={frozen ? { animation: 'none' } : null}>
          <span className="mark"/>
          <span><b>devo</b><span>new</span></span>
          <span className="tld">.com</span>
        </div>
      )}
      {!frozen && <button className="intro-skip" onClick={skip}>Toque para pular</button>}
    </div>
  );
}

/* ============================================================
   Nav config
   ============================================================ */
const NAV = [
  { id: 'hoje', label: 'Hoje', icon: 'home' },
  { id: 'devocional', label: 'Devocional', icon: 'book' },
  { id: 'biblia', label: 'Bíblia', icon: 'bible' },
  { id: 'oracao', label: 'Oração', icon: 'pray' },
  { id: 'celula', label: 'Célula', icon: 'users' },
  { id: 'trilha', label: 'Trilha', icon: 'sparkle' },
  { id: 'cursos', label: 'Cursos', icon: 'grad' },
  { id: 'mural', label: 'Testemunhos', short: 'Mural', icon: 'hands' },
];
// item dinâmico: só entra na navegação quando há campanha publicada e ativa
const CORRENTE_NAV = { id: 'corrente', label: 'Corrente de Oração', short: 'Corrente', icon: 'clock', isNew: true };
function buildNav(active) {
  if (!active) return NAV;
  const a = [...NAV];
  const i = a.findIndex((n) => n.id === 'oracao');
  a.splice(i + 1, 0, CORRENTE_NAV);
  return a;
}

function ThemeToggle({ theme, setTheme }) {
  return (
    <div className="theme-toggle" role="group" aria-label="tema">
      <button className={theme === 'light' ? 'on' : ''} onClick={() => setTheme('light')} aria-label="claro"><Icon name="sun" size={17}/></button>
      <button className={theme === 'dark' ? 'on' : ''} onClick={() => setTheme('dark')} aria-label="escuro"><Icon name="moon" size={16}/></button>
    </div>
  );
}

function Brand() {
  return (
    <div className="brand">
      <span className="brand-mark"/>
      <span className="brand-word"><b>devo</b><span>new</span></span>
    </div>
  );
}

// sigla a partir do nome da igreja (fallback quando o admin não definiu logo)
function siglaFrom(name) {
  return (name || '').split(/\s+/).filter(Boolean).map((w) => w[0]).slice(0, 2).join('').toUpperCase() || '✦';
}

// marca da igreja (lê identidade do Site Público; cai para o padrão do conteúdo)
function ChurchBrand({ name, logo, logoImg }) {
  return (
    <div className="church-brand">
      {logoImg
        ? <span className="cb-logo has-img"><img src={logoImg} alt={name}/></span>
        : <span className="cb-logo">{logo || siglaFrom(name)}</span>}
      <span className="cb-text">
        <span className="cb-name">{name}</span>
        <span className="cb-sub">Portal do Membro</span>
      </span>
    </div>
  );
}

// selo discreto "feito com devonew"
function PoweredBy() {
  return (
    <a className="powered" href={`//${window.DEVO_HOST || 'devonew.localhost:5050'}/`} title="Conheça o devonew">
      <span className="brand-mark sm"/>
      <span>feito com <b>devonew</b></span>
    </a>
  );
}

/* ============================================================
   App
   ============================================================ */
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "accent": "oliveira",
  "scriptureSerif": false
}/*EDITMODE-END*/;

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [intro, setIntro] = useState(() => {
    const q = new URLSearchParams(location.search);
    if (q.get('intro') === '0') return false;
    if (q.get('intro') === '1') return true;
    return !LS.get('intro_seen', false);
  });
  const [screen, setScreen] = useState(() => {
    const q = new URLSearchParams(location.search);
    if (q.get('intro') === '0') return q.get('screen') || 'hoje';
    return q.get('screen') || 'hoje';
  });
  const [theme, setThemeRaw] = useState(() => LS.get('theme', 'light'));
  const scrollRef = useRef(null);

  // gamified state
  // sequência: { n, date } — date é o dia da última conclusão. Formato antigo
  // (número puro, sem data) é aceito e migra na primeira conclusão.
  const [streakSt, setStreakSt] = useState(() => {
    const raw = LS.get('streak', STREAK0);
    return (raw && typeof raw === 'object') ? raw : { n: raw || 0, date: null };
  });
  const streak = (!streakSt.date || streakSt.date >= yesterISO()) ? (streakSt.n || 0) : 0;
  // devocional do dia: a conclusão vale só para HOJE (antes ficava "concluído"
  // para sempre e a sequência congelava no segundo dia)
  const [doneDate, setDoneDate] = useState(() => LS.get('done_date', null));
  const completedToday = doneDate === todayISO();
  const [xp, setXp] = useState(() => LS.get('xp', XP0));
  // trilha de crescimento: jornada de discipulado, metas semanais e feed de conquistas
  const [journeyDone, setJourneyDone] = useState(() => LS.get('journey_done', []));
  const [weekly, setWeekly] = useState(() => freshWeek(LS.get('weekly', null)));
  const [xpLog, setXpLog] = useState(() => LS.get('xp_log', C.xpLogSeed || []));
  const [prayers, setPrayers] = useState(() => LS.get('prayers', C.prayers));
  const [journal, setJournalRaw] = useState(() => LS.get('journal', C.journalSeed));
  const [courses, setCourses] = useState(() => LS.get('courses', C.courses));
  // biblioteca + seminários (espelho do painel via bridge) + inscrições locais
  const [library, setLibrary] = useState(() => LS.get('library', C.library));
  const [seminars, setSeminars] = useState(() => LS.get('events_pub', C.seminars));
  const [eventRegs, setEventRegs] = useState(() => LS.get('event_regs', []));
  // testemunhos (shared store, written by the church panel too)
  const [testimonies, setTestimonies] = useState(() => window.TESTI.load());
  const [myReactions, setMyReactions] = useState(() => LS.get('testi_reacted', []));
  const [testiCredited, setTestiCredited] = useState(() => LS.get('testi_credited', []));
  // corrente de intercessão (shared store, written by the church panel too)
  const [campaign, setCampaign] = useState(() => window.INTER.load());
  const [sentinela, setSentinela] = useState(() => LS.get('inter_madrugada', []));
  // identidade da igreja (editada pelo admin em Site Público → mesma origem)
  const [site, setSite] = useState(() => LS.get('site', null));
  // trilha de discipulado publicada pelo admin (mesma origem) → cai para o padrão do conteúdo
  const [journeyDef, setJourneyDef] = useState(() => LS.get('journey_def', null));
  // devocionais publicados pelo admin (mesma origem) → o de hoje + histórico; cai no seed
  const [devotionalsPub, setDevotionalsPub] = useState(() => LS.get('devotionals_pub', []));
  // engajamento do membro no devocional (amém · salvar · reflexão pessoal)
  const [devoAmen, setDevoAmen] = useState(() => LS.get('devo_amen', []));
  const [devoSaved, setDevoSaved] = useState(() => LS.get('devo_saved', []));
  const [devoNotes, setDevoNotes] = useState(() => LS.get('devo_notes', {}));
  // catalog published by the church admin (Painel) → falls back to seed
  const catalog = (LS.get('catalog', C.catalog) || C.catalog).filter((x) => !courses.some((co) => co.id === x.id));
  // devocional de hoje = publicado mais recente com data ≤ hoje; histórico = os anteriores; sem nada → seed
  const _todayISO = (() => { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; })();
  const _pubDevos = (devotionalsPub || []).filter((x) => x && x.dateISO && x.dateISO <= _todayISO).sort((a, b) => b.dateISO.localeCompare(a.dateISO));
  const todayDevo = _pubDevos[0] || C.devotional;
  const devoArchive = _pubDevos.slice(1);
  const [toastMsg, setToastMsg] = useState(null);
  const [support, setSupport] = useState(false);
  const toastTimer = useRef(null);

  // apply theme
  useEffect(() => { document.documentElement.dataset.theme = theme; LS.set('theme', theme); }, [theme]);
  const setTheme = (v) => setThemeRaw(v);

  // apply accent: cor da igreja é a base; o tweak pessoal sobrescreve se mudado
  useEffect(() => {
    const churchAcc = site && site.accent && site.accent.h != null ? site.accent : null;
    const a = (t.accent === 'oliveira' && churchAcc) ? churchAcc : (ACCENTS[t.accent] || ACCENTS.oliveira);
    const r = document.documentElement.style;
    r.setProperty('--accent-h', a.h);
    r.setProperty('--accent-c', a.c);
  }, [t.accent, site]);

  // scripture serif tweak
  useEffect(() => {
    document.documentElement.dataset.scripture = t.scriptureSerif ? 'serif' : 'sans';
  }, [t.scriptureSerif]);

  // persist
  useEffect(() => LS.set('streak', streakSt), [streakSt]);
  useEffect(() => LS.set('done_date', doneDate), [doneDate]);
  useEffect(() => LS.set('xp', xp), [xp]);
  useEffect(() => LS.set('journey_done', journeyDone), [journeyDone]);
  useEffect(() => LS.set('weekly', weekly), [weekly]);
  useEffect(() => LS.set('xp_log', xpLog), [xpLog]);
  useEffect(() => LS.set('prayers', prayers), [prayers]);
  useEffect(() => LS.set('journal', journal), [journal]);
  useEffect(() => LS.set('courses', courses), [courses]);
  useEffect(() => LS.set('event_regs', eventRegs), [eventRegs]);
  useEffect(() => LS.set('testi_reacted', myReactions), [myReactions]);
  useEffect(() => LS.set('inter_madrugada', sentinela), [sentinela]);
  useEffect(() => LS.set('devo_amen', devoAmen), [devoAmen]);
  useEffect(() => LS.set('devo_saved', devoSaved), [devoSaved]);
  useEffect(() => LS.set('devo_notes', devoNotes), [devoNotes]);
  const setJournal = (v) => setJournalRaw(v);

  // re-read shared stores when returning to the tab (picks up panel changes live)
  useEffect(() => {
    const reload = () => { setTestimonies(window.TESTI.load()); setCampaign(window.INTER.load()); setSite(LS.get('site', null)); setLibrary(LS.get('library', C.library)); setSeminars(LS.get('events_pub', C.seminars)); setJourneyDef(LS.get('journey_def', null)); setDevotionalsPub(LS.get('devotionals_pub', [])); };
    const onVis = () => { if (!document.hidden) reload(); };
    window.addEventListener('focus', reload);
    document.addEventListener('visibilitychange', onVis);
    return () => { window.removeEventListener('focus', reload); document.removeEventListener('visibilitychange', onVis); };
  }, []);

  // credit +20 XP when one of MY testimonies gets approved (máx. 1 por semana)
  useEffect(() => {
    const fresh = testimonies.filter((t) => t.mine && t.status === 'aprovado' && !testiCredited.includes(t.id));
    if (!fresh.length) return;
    const last = LS.get('testi_lastxp', 0);
    const canXp = Date.now() - last >= 7 * 86400000;
    const credited = [...testiCredited, ...fresh.map((t) => t.id)];
    setTestiCredited(credited); LS.set('testi_credited', credited);
    if (canXp) {
      LS.set('testi_lastxp', Date.now());
      awardXp(20, 'Testemunho aprovado', 'hands');
      toast('Seu testemunho foi aprovado · +20 XP 🙌');
    } else {
      toast('Seu testemunho foi aprovado e já abençoa a igreja 🙌');
    }
  }, [testimonies]);

  const toast = (m) => {
    setToastMsg(m);
    clearTimeout(toastTimer.current);
    toastTimer.current = setTimeout(() => setToastMsg(null), 2400);
  };

  const go = (s) => { setScreen(s); if (scrollRef.current) scrollRef.current.scrollTop = 0; };

  // trilha de discipulado ativa: a publicada pelo admin (se houver) ou a padrão do conteúdo
  const journey = (journeyDef && journeyDef.length) ? journeyDef : C.journey;

  // concede XP e registra no feed de "Conquistas recentes" da Trilha
  const awardXp = (n, label, icon) => {
    setXp((v) => v + n);
    setXpLog((l) => [{ ts: Date.now(), label, icon: icon || 'sparkle', xp: n }, ...l].slice(0, 12));
  };

  // comemora a subida de nível (o nível deriva do XP real)
  const lvlRef = useRef(levelFor(LS.get('xp', XP0)).n);
  useEffect(() => {
    const n = levelFor(xp).n;
    if (n > lvlRef.current) toast(`🎉 Novo nível ${n} · ${levelFor(xp).name}!`);
    lvlRef.current = n;
  }, [xp]);

  // metas semanais: soma o contador e premia ao bater a meta (uma vez por semana)
  const bumpWeekly = (key) => {
    const cur = freshWeek(weekly);
    const val = (cur[key] || 0) + 1;
    const next = { ...cur, [key]: val };
    const goal = (C.weeklyGoals || []).find((g) => g.id === key);
    if (goal && val >= goal.target && !cur.claimed.includes(key)) {
      next.claimed = [...cur.claimed, key];
      setTimeout(() => { awardXp(goal.xp, 'Meta da semana: ' + goal.label, goal.icon); toast(`🎯 Meta batida · +${goal.xp} XP`); }, 700);
    }
    setWeekly(next);
  };

  // conclui uma etapa manual da jornada de discipulado (persiste e concede XP)
  const completeJourneyStep = (id) => {
    const step = (journey || []).find((s) => s.id === id);
    if (!step || journeyDone.includes(id)) return;
    setJourneyDone((s) => [...s, id]);
    if (step.xp) awardXp(step.xp, 'Etapa: ' + step.name, step.icon);
    toast(`✓ ${step.name}${step.xp ? ` · +${step.xp} XP` : ''} 🙌`);
  };

  // strip entrance-animation classes once settled (keeps re-renders/captures stable)
  useEffect(() => {
    const id = setTimeout(() => {
      scrollRef.current?.querySelectorAll('.stagger, .fade-up')
        .forEach((el) => el.classList.remove('stagger', 'fade-up'));
    }, 850);
    return () => clearTimeout(id);
  }, [screen]);

  const finishIntro = () => { setIntro(false); LS.set('intro_seen', true); };
  const replayIntro = () => { setIntro(true); };

  // sair: encerra a sessão no servidor (igreja real) e volta pro acesso DA igreja
  const doLogout = () => {
    const fin = () => { location.href = '/acesso'; };
    if (IS_DEMO_MEMBER) { fin(); return; }
    flushState();
    fetch('/api/auth/logout', { method: 'POST' }).catch(() => {}).finally(fin);
  };

  const completeDevotional = () => {
    if (completedToday) return;
    const gain = (todayDevo && todayDevo.xp != null) ? todayDevo.xp : C.devotional.xp;
    setDoneDate(todayISO());
    awardXp(gain, 'Devocional concluído', 'book');
    bumpWeekly('devo');
    // continua a sequência se a última conclusão foi ontem (ou legado sem data); senão recomeça em 1
    setStreakSt((s) => ({
      n: (s.date === yesterISO() || (s.date == null && s.n)) ? (s.n || 0) + 1 : 1,
      date: todayISO(),
    }));
    toast(`Devocional concluído · +${gain} XP 🔥`);
    setTimeout(() => go('hoje'), 900);
  };

  const toggleDevoAmen = (id) => setDevoAmen((a) => a.includes(id) ? a.filter((x) => x !== id) : [...a, id]);
  const toggleDevoSave = (id) => setDevoSaved((a) => a.includes(id) ? a.filter((x) => x !== id) : [...a, id]);
  const setDevoNote = (id, text) => setDevoNotes((n) => { const m = { ...n }; if (text && text.trim()) m[id] = text; else delete m[id]; return m; });

  const completeLesson = (courseId, lessonId) => {
    const co = courses.find((c) => c.id === courseId);
    const lesson = co && co.modules.flatMap((m) => m.lessons).find((l) => l.id === lessonId);
    if (!lesson || lesson.done) return;
    setCourses((cs) => cs.map((c) => c.id !== courseId ? c : ({
      ...c,
      modules: c.modules.map((m) => ({ ...m, lessons: m.lessons.map((l) => l.id === lessonId ? { ...l, done: true } : l) })),
    })));
    awardXp(C.lessonXp, 'Lição de curso', 'grad');
    bumpWeekly('lessons');
    toast(`Lição concluída · +${C.lessonXp} XP ✓`);
  };

  // matrícula real: copia o curso do catálogo para "Meus cursos" (lições zeradas) e persiste
  const enrollCourse = (cat) => {
    if (courses.some((c) => c.id === cat.id)) { toast('Você já está matriculado'); go('cursos'); return; }
    const fresh = { ...cat, modules: (cat.modules || []).map((m) => ({ ...m, lessons: m.lessons.map((l) => ({ ...l, done: false })) })) };
    setCourses((cs) => [fresh, ...cs]);
    toast('Matrícula confirmada 🎓');
  };
  const toggleEventReg = (id) => setEventRegs((rs) => rs.includes(id) ? rs.filter((x) => x !== id) : [...rs, id]);

  // leitura bíblica → Trilha: persiste, credita XP e celebra marcos (livro, NT, Bíblia toda)
  const markChapterRead = (b, c) => {
    const before = window.BIBLE.progress();
    if (!window.BIBLE.markRead(b, c)) return;   // já lido → não pontua de novo
    const after = window.BIBLE.progress();
    awardXp(C.bibleXp, 'Capítulo lido', 'bible');
    bumpWeekly('chapters');
    if (after.read >= after.total && before.read < before.total) toast(`🎉 Você concluiu toda a Bíblia! +${C.bibleXp} XP`);
    else if (after.nt.read >= after.nt.total && before.nt.read < before.nt.total) toast(`✨ Novo Testamento concluído! +${C.bibleXp} XP`);
    else if (after.gospelsDone > before.gospelsDone) toast(`📖 Evangelho concluído! +${C.bibleXp} XP`);
    else if (after.booksDone > before.booksDone) toast(`📘 Livro concluído · +${C.bibleXp} XP`);
    else toast(`Capítulo lido · +${C.bibleXp} XP 📖`);
  };

  const submitTestimony = ({ title, cat, body, anon }) => {
    const item = {
      id: 't' + Date.now().toString(36), title, cat, body, anon,
      author: C.user.name, initials: C.initials, status: 'pendente',
      ts: Date.now(), reactions: 0, mine: true,
    };
    setTestimonies(() => { const next = [item, ...window.TESTI.load()]; window.TESTI.save(next); return next; });
    toast('Testemunho enviado para moderação 🙏');
  };
  const reactTestimony = (id) => {
    const has = myReactions.includes(id);
    setTestimonies(() => {
      const next = window.TESTI.load().map((t) => t.id === id
        ? { ...t, reactions: Math.max(0, (t.reactions || 0) + (has ? -1 : 1)) } : t);
      window.TESTI.save(next); return next;
    });
    setMyReactions((s) => has ? s.filter((x) => x !== id) : [...s, id]);
  };

  const togglePrayer = (id) => setPrayers((ps) => ps.map((p) =>
    p.id === id ? { ...p, answered: !p.answered, sub: !p.answered ? 'Respondida hoje' : 'Reaberta hoje' } : p));
  const addPrayer = (title) => setPrayers((ps) => [{ id: Date.now(), title, sub: 'Iniciado hoje', answered: false }, ...ps]);

  /* ---- corrente de intercessão ---- */
  const corrente = window.INTER.activeForMember(campaign) ? campaign : null;
  const persistCampaign = (next) => { window.INTER.save(next); setCampaign(next); };

  const assumeShift = (day, idx, anon) => {
    const c = window.INTER.load();
    const mineNear = (c.reservations || []).some((r) => r.mine && r.day === day && Math.abs(r.idx - idx) <= 1);
    if (mineNear) { toast('Você já tem um turno nesse horário ou muito próximo'); return; }
    if (window.INTER.slotFull(c, day, idx)) { toast('Esse turno já está completo'); return; }
    const res = { day, idx, name: C.user.name, initials: C.initials, anon: !!anon, mine: true, doneAt: null };
    persistCampaign({ ...c, reservations: [...(c.reservations || []), res] });
    toast('Turno assumido — avisaremos 15 min antes 🙏');
  };
  const releaseShift = (day, idx) => {
    const c = window.INTER.load();
    persistCampaign({ ...c, reservations: (c.reservations || []).filter((r) => !(r.mine && r.day === day && r.idx === idx)) });
    toast('Turno liberado para outro intercessor');
  };
  const finishShift = (day, idx) => {
    const c = window.INTER.load();
    const slot = window.INTER.slotsForDay(c)[idx];
    const live = window.INTER.slotState(c, day, slot) === 'agora';
    persistCampaign({ ...c, reservations: (c.reservations || []).map((r) => (r.mine && r.day === day && r.idx === idx) ? { ...r, doneAt: Date.now() } : r) });
    if (!live) { toast('Você só pontua se finalizar dentro do horário do turno'); return; } // trava de pontuação justa
    const gain = window.INTER.xpForHour(slot.hour);
    awardXp(gain, window.INTER.isMadrugada(slot.hour) ? 'Vigília cumprida' : 'Turno cumprido', 'moon');
    if (window.INTER.isMadrugada(slot.hour)) {
      const key = c.id + '|' + day + '|' + idx;
      if (!sentinela.includes(key)) {
        const ns = [...sentinela, key];
        setSentinela(ns);
        const n = ns.filter((k) => k.startsWith(c.id + '|')).length;
        if (n === 3) toast('🌙 Medalha desbloqueada: Sentinela da Promessa!');
        else toast(`Vigília cumprida · +${gain} XP 🌙 (${n}/3 p/ medalha)`);
        return;
      }
    }
    toast(`Turno cumprido · +${gain} XP 🙌`);
  };

  // medalhas dinâmicas: Sentinela (3 vigílias de madrugada) + leitura bíblica (Trilha)
  const madrugadaDone = campaign ? sentinela.filter((k) => k.startsWith(campaign.id + '|')).length : 0;
  const bibProg = window.BIBLE.progress();
  const badges = C.badges.map((b) => {
    switch (b.name) {
      case 'Firme na Rocha': return { ...b, unlocked: streak >= 7 };
      case 'Coração de Servo': return { ...b, unlocked: journeyDone.includes('servir') };
      case 'Guerreiro de Oração': return { ...b, unlocked: prayers.some((p) => p.answered) };
      case 'Sentinela da Promessa': return { ...b, unlocked: madrugadaDone >= 3 };
      case 'Estudioso da Palavra': return { ...b, unlocked: bibProg.read >= 25 };
      case 'Evangelho no Coração': return { ...b, unlocked: bibProg.gospelsDone >= 1 };
      case 'Novo Testamento Vivo': return { ...b, unlocked: bibProg.nt.read >= bibProg.nt.total };
      case 'Toda a Escritura': return { ...b, unlocked: bibProg.read >= bibProg.total };
      default: return b;
    }
  });

  // sinais da jornada de discipulado (auto-conclui etapas a partir do estado real)
  const journeySignals = {
    member: true,
    devo: streak >= 1 || completedToday,
    bible1: bibProg.read >= 1,
    cell: true,
    course: courses.some((co) => co.modules.every((m) => m.lessons.every((l) => l.done))),
  };
  // etapa vinculada a um curso específico: concluída quando aquele curso está 100%
  const courseDoneById = (id) => {
    const co = courses.find((c) => c.id === id);
    return !!co && co.modules.every((m) => m.lessons.every((l) => l.done));
  };
  const level = levelFor(xp);

  const nav = buildNav(!!corrente);
  const cur = nav.find((n) => n.id === screen) || nav[0];
  const churchName = (site && site.name) || C.church;
  const churchLogo = (site && site.logo) || siglaFrom(churchName);

  // se o módulo sumir (campanha despublicada) enquanto está aberto, volta para Hoje
  useEffect(() => { if (!nav.some((n) => n.id === screen)) go('hoje'); }, [!!corrente]);

  return (
    <>
      {intro && <Intro onDone={finishIntro} />}

      <div className="app">
        {/* sidebar (desktop) */}
        <aside className="sidebar">
          <ChurchBrand name={churchName} logo={churchLogo} logoImg={site && site.logoImg} />
          <nav className="nav">
            {nav.map((n) => (
              <button key={n.id} className={`nav-item ${screen === n.id ? 'active' : ''}`} onClick={() => go(n.id)}>
                <Icon name={n.icon} size={21}/>{n.label}
                {n.isNew && <span className="nav-new-dot"/>}
              </button>
            ))}
          </nav>
          <div className="nav-spacer"/>
          <div className="side-foot">
            <button className="support-trigger" onClick={() => setSupport(true)}>
              <Icon name="heart" size={17}/>Apoie o criador
            </button>
            <ThemeToggle theme={theme} setTheme={setTheme}/>
            <div className="row between" style={{ padding: '4px 6px' }}>
              <div className="row gap-10">
                <div className="avatar sm">{C.initials}</div>
                <div>
                  <div style={{ fontSize: 14, fontWeight: 600 }}>{C.user.name}</div>
                  <div style={{ fontSize: 12, color: 'var(--muted)' }}>{C.church}</div>
                </div>
              </div>
              <button className="iconbtn" onClick={doLogout} title="Sair" style={{ width: 34, height: 34, color: 'var(--muted)' }}>
                <Icon name="logout" size={17}/>
              </button>
            </div>
            <PoweredBy />
          </div>
        </aside>

        {/* main */}
        <main className="main">
          <div className="topbar">
            <ChurchBrand name={churchName} logo={churchLogo} logoImg={site && site.logoImg} />
            <div className="row gap-10">
              <button className="iconbtn" onClick={() => setSupport(true)} aria-label="apoie o criador"><Icon name="heart" size={18}/></button>
              <ThemeToggle theme={theme} setTheme={setTheme}/>
              <button className="iconbtn" onClick={() => toast('Sem novos avisos')} aria-label="avisos"><Icon name="bell" size={19}/></button>
            </div>
          </div>

          <div className="scroll" ref={scrollRef}>
            {screen === 'hoje' && <HojeScreen c={C} devo={todayDevo} streak={streak} completedToday={completedToday} xp={xp} level={level} go={go} toast={toast}/>}
            {screen === 'devocional' && <DevocionalScreen c={C} devo={todayDevo} archive={devoArchive} completedToday={completedToday} onComplete={completeDevotional} go={go} amenSet={devoAmen} savedSet={devoSaved} notes={devoNotes} onAmen={toggleDevoAmen} onSave={toggleDevoSave} onNote={setDevoNote}/>}
            {screen === 'biblia' && <BibliaScreen go={go} toast={toast} onMarkRead={markChapterRead} readXp={C.bibleXp}/>}
            {screen === 'oracao' && <OracaoScreen c={C} prayers={prayers} togglePrayer={togglePrayer} addPrayer={addPrayer} journal={journal} setJournal={setJournal} toast={toast}/>}
            {screen === 'celula' && <CelulaScreen c={C} toast={toast}/>}
            {screen === 'trilha' && <TrilhaScreen c={C} xp={xp} level={level} badges={badges} streak={streak} journey={journey} signals={journeySignals} journeyDone={journeyDone} courseDone={courseDoneById} onCompleteStep={completeJourneyStep} weekly={freshWeek(weekly)} xpLog={xpLog} go={go} bible={bibProg}/>}
            {screen === 'cursos' && <CursosScreen c={C} courses={courses} catalog={catalog} library={library} seminars={seminars} eventRegs={eventRegs} completeLesson={completeLesson} enrollCourse={enrollCourse} toggleEventReg={toggleEventReg} go={go} toast={toast}/>}
            {screen === 'mural' && <TestemunhosScreen c={C} testimonies={testimonies} myReactions={myReactions} onReact={reactTestimony} onSubmit={submitTestimony} go={go} toast={toast}/>}
            {screen === 'corrente' && corrente && <CorrenteScreen c={C} campaign={corrente} onAssume={assumeShift} onRelease={releaseShift} onFinish={finishShift} go={go} toast={toast}/>}
          </div>

          {/* bottom tabs (mobile) */}
          <nav className="tabbar">
            {nav.map((n) => (
              <button key={n.id} className={`tab ${screen === n.id ? 'active' : ''}`} onClick={() => go(n.id)}>
                <Icon name={n.icon} size={23}/>{n.short || n.label}
              </button>
            ))}
          </nav>
        </main>
      </div>

      {/* support modal */}
      {support && <SupportModal onClose={() => setSupport(false)} toast={toast}
        sender={{ name: C.user.name, role: 'Membro', church: C.church, source: 'app' }}/>}

      {/* toast */}
      <div className={`toast ${toastMsg ? 'show' : ''}`}>{toastMsg && <><Icon name="check" size={17}/>{toastMsg}</>}</div>

      {/* tweaks */}
      <TweaksPanel title="Tweaks">
        <TweakSection label="Aparência" />
        <TweakSelect label="Cor de destaque" value={t.accent}
          options={Object.entries(ACCENTS).map(([k, v]) => ({ value: k, label: v.label }))}
          onChange={(v) => setTweak('accent', v)} />
        <TweakToggle label="Escritura em serifa" value={t.scriptureSerif}
          onChange={(v) => setTweak('scriptureSerif', v)} />
        <TweakSection label="Animação" />
        <TweakButton label="Rever abertura" onClick={replayIntro} />
      </TweaksPanel>
    </>
  );
}

/* ---------- boot ----------
   Demo: renderiza direto (vitrine, tudo local). Igreja real: confirma a
   sessão (/api/me) — sem sessão volta para /acesso; com sessão, baixa o
   estado do membro (/api/state) para o cache local e liga a sincronização.
   Se a rede falhar (PWA offline), abre com o cache do último login. */
const _root = ReactDOM.createRoot(document.getElementById('root'));

async function bootApp() {
  if (!IS_DEMO_MEMBER) {
    let meResp = null;
    try { const r = await fetch('/api/me'); if (r.ok) meResp = await r.json(); } catch (e) {}
    if (meResp && !meResp.authenticated) { location.replace('/acesso'); return; }
    if (meResp && meResp.member) {
      const m = meResp.member;
      CURRENT_MEMBER = { key: m.email, name: m.name, initials: m.initials };
      try { localStorage.setItem('devonew_current_member', JSON.stringify(CURRENT_MEMBER)); } catch (e) {}
      if (m.church && m.church.name && !(_siteSaved && _siteSaved.name)) CHURCH_NAME = m.church.name;
      // conteúdo publicado pela igreja (site/cursos/eventos/trilha/devocionais)
      // → cache local; em aparelho novo o membro vê o que o admin publicou.
      try {
        const rc = await fetch('/api/church-state');
        if (rc.ok) {
          const cst = (await rc.json()).state || {};
          if (Object.keys(cst).length) {
            for (const k of ['site', 'catalog', 'library', 'events_pub', 'journey_def', 'devotionals_pub']) {
              if (k in cst) { try { localStorage.setItem('devonew_' + k, JSON.stringify(cst[k])); } catch (e) {} }
              else { try { localStorage.removeItem('devonew_' + k); } catch (e) {} } // despublicado
            }
            if (cst.site && cst.site.name) CHURCH_NAME = cst.site.name;
          }
        }
      } catch (e) {} // offline → cache local
      initMemberContext();
      try {
        const r = await fetch('/api/state');
        if (r.ok) {
          const st = (await r.json()).state || {};
          const pref = 'devonew_' + _mprefix;
          // servidor é a fonte da verdade → atualiza o cache local
          for (const k of Object.keys(st)) {
            try { localStorage.setItem(pref + k, JSON.stringify(st[k])); } catch (e) {}
          }
          // primeira sincronização deste aparelho: sobe o que só existe aqui
          const up = {};
          for (let i = 0; i < localStorage.length; i++) {
            const lk = localStorage.key(i);
            if (!lk || !lk.startsWith(pref)) continue;
            const k = lk.slice(pref.length);
            if (k in st) continue;
            try { const v = localStorage.getItem(lk); if (v != null) up[k] = JSON.parse(v); } catch (e) {}
          }
          _syncOn = true;
          if (Object.keys(up).length) { Object.assign(_pend, up); flushState(); }
        } else { _syncOn = true; }
      } catch (e) { _syncOn = true; } // offline → usa o cache; sobe quando a rede voltar
    } else {
      // API não respondeu (offline / dev local sem backend) → último login salvo
      initMemberContext();
      _syncOn = true;
    }
  } else {
    initMemberContext();
  }
  _root.render(<App />);
}
bootApp();
