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

const LS_KEY = "scolist-proto-v1";

// O progresso do wizard (data/current) era guardado num único slot global
// de localStorage, sem relação nenhuma com qual conta estava logada. A
// maior parte do wizard nunca teve persistência própria no backend — só
// livro pinado (pinBookToGrade/unpinBookFromGrade) e marca da escola
// (updateSchoolBranding) de fato salvam no Scolist. Tudo mais (segmento
// renomeado, temas, calibragem, didáticos, papelaria, "outros materiais",
// bimestres) só existia nesse slot único — e handleLogout apagava ele
// inteiro. Bug reportado pela Joana em vídeo no WhatsApp, 27/08/2026:
// renomeou um segmento, deu logout/login, voltou pro nome antigo.
//
// Fix: escopar o slot por conta (uid do Firebase Auth) — dar logout só
// limpa o estado em memória (pra tela de login aparecer limpa), não apaga
// o slot salvo; a mesma conta logando de novo recupera o progresso dela.
// O slot genérico antigo continua sendo escrito também, só por segurança
// (não é lido de volta depois que a conta resolve — ver loadScopedProgress).
//
// Escopado também por escola (account.school.id), não só por uid — com o
// seletor de escola no login (conta que atende mais de uma escola), o
// progresso de cada escola precisa ficar independente; senão trocar de
// escola pelo menu lateral mostrava a mesma lista pra qualquer uma delas
// (pedido explícito, 31/08/2026). "_none" é o fallback pra conta sem
// escola vinculada ainda.
function lsKeyFor(account) {
  const uid = account && account.user && account.user.id;
  if (!uid) return null;
  const schoolId = (account && account.school && account.school.id) || "_none";
  return `${LS_KEY}:${uid}:${schoolId}`;
}

function loadScopedProgress(account) {
  const scopedKey = lsKeyFor(account);
  if (!scopedKey) return null;
  try {
    return JSON.parse(localStorage.getItem(scopedKey) || "null");
  } catch (e) {
    return null;
  }
}

// window.CATALOG (catalog.js) é um array global mutável, nunca persistido
// — Step9_Recommend só empurra pra ele os livros da série que está sendo
// vista no momento (mais resultado de busca). Step11_Confirm e
// Step8_Pricing resolvem TODOS os livros pinados (de todas as séries)
// contra esse mesmo array via CATALOG.find(id) — então, depois de um
// refresh, qualquer série que a pessoa não tenha revisitado em
// Recomendações nesta sessão fica sem detalhe pra resolver, e o
// CATALOG.find(...).filter(Boolean) descarta o livro da lista mesmo com
// o id certinho em pinned[série]. Bug reportado pelo Alex, 27/08/2026:
// "não tá persistindo a mesma lista do passo 7 [Recomendações], no passo
// 13 [Confirmação final]".
//
// Fix: espelha as entradas "runtime" do CATALOG (id começando com "rt-"
// — o catálogo estático de catalog.js não precisa, já vem no arquivo)
// no mesmo slot de progresso por conta, e restaura no load.
function snapshotRuntimeCatalog() {
  if (!Array.isArray(window.CATALOG)) return [];
  return window.CATALOG.filter((b) => b && typeof b.id === "string" && b.id.startsWith("rt-"));
}

function restoreRuntimeCatalog(snapshot) {
  if (!Array.isArray(snapshot) || !Array.isArray(window.CATALOG)) return;
  snapshot.forEach((book) => {
    if (!book || !book.id) return;
    const existing = window.CATALOG.find((b) => b.id === book.id);
    if (existing) Object.assign(existing, book);
    else window.CATALOG.push(book);
  });
}

// Firestore tem limite de 1 MiB por documento. data.schoolLogoUrl pode ser
// um data:-URL de até ~2MB (ver uploadSchoolLogo em api.js) — sozinho já
// estouraria esse limite. A logo já persiste separadamente (school.logo,
// via updateSchoolBranding) e é lida de volta com prioridade correta em
// pages-familia.jsx/steps-final.jsx (account.school.logoUrl) — não precisa
// duplicar no payload sincronizado com o backend.
function stripLargeFieldsForSync(data) {
  if (!data) return data;
  const { schoolLogoUrl, schoolLogoName, ...rest } = data;
  return rest;
}

function buildWizardSyncPayload({ current, data, catalog, updatedAt }) {
  return { current, data: stripLargeFieldsForSync(data), catalog, updatedAt };
}

// Resolve o progresso do wizard pra uma conta+escola recém-conhecida —
// decide entre o slot local escopado por (uid, schoolId) (loadScopedProgress,
// só localStorage) e o documento remoto (wizardProgress/{uid}_{schoolId}),
// e cobre a migração de quem só tinha progresso local (ex: bug reportado
// pela Joana, 29/08/2026: lista montada no Computador A não aparecia no
// Computador B).
//
// Escopado por (uid, schoolId) — antes era só uid, então trocar de escola
// mostrava o mesmo progresso pra qualquer uma (pedido explícito,
// 31/08/2026: "é importante que as listas entre as escolas sejam
// independentes"). O backend (WizardProgressService) cobre a migração de
// quem já tinha progresso salvo antes de existir separação por escola.
//
// Sem resolução de conflito sofisticada (last-write-wins, mesmo padrão do
// resto do backend):
// 1. Backend indisponível (erro de rede/servidor) -> usa o local como está.
// 2. Backend responde null (nunca sincronizou) + existe local -> MIGRAÇÃO:
//    local é a fonte de verdade, sobe pro backend (best-effort) e usa como
//    estado inicial.
// 3. Backend responde null + sem local -> conta/escola nova, estado vazio.
// 4. Backend responde com progresso + local sem updatedAt (formato salvo
//    antes deste fix) -> usa remoto.
// 5. Backend responde com progresso + local.updatedAt mais novo -> usa
//    local, reagenda push pro backend.
// 6. Caso contrário -> usa remoto.
async function resolveWizardProgress(account) {
  const uid = account && account.user && account.user.id;
  const schoolId = (account && account.school && account.school.id) || null;
  const local = loadScopedProgress(account);
  const empty = { current: null, data: null, catalog: null };

  if (!uid) return local || empty;

  let remote = null;
  try {
    remote = await window.ScolistAPI.loadWizardProgress(uid, schoolId);
  } catch (e) {
    console.warn("Falha ao buscar progresso do wizard no backend, usando cópia local (se houver):", e);
    return local || empty;
  }

  if (!remote) {
    if (local) {
      window.ScolistAPI.saveWizardProgress(uid, schoolId, buildWizardSyncPayload(local)).catch((e) => {
        console.warn("Falha ao migrar progresso local pro backend:", e);
      });
    }
    return local || empty;
  }

  if (!local || !local.updatedAt) return remote;

  const localTime = Date.parse(local.updatedAt) || 0;
  const remoteTime = Date.parse(remote.updatedAt) || 0;
  if (localTime > remoteTime) {
    window.ScolistAPI.saveWizardProgress(uid, schoolId, buildWizardSyncPayload(local)).catch((e) => {
      console.warn("Falha ao sincronizar progresso local mais recente pro backend:", e);
    });
    return local;
  }

  return remote;
}

// ============= PT | EN toggle (canto superior direito, fixo) =============
function LangToggle() {
  const [lang, setLangState] = useState(() => (window.I18N && window.I18N.getLang()) || "pt");
  useEffect(() => {
    const onChange = (e) => setLangState(e.detail.lang);
    window.addEventListener("langchange", onChange);
    return () => window.removeEventListener("langchange", onChange);
  }, []);
  const set = (l) => { if (window.I18N) window.I18N.setLang(l); };
  const pillBase = {
    padding: "5px 11px",
    fontSize: 12,
    fontWeight: 600,
    cursor: "pointer",
    border: "none",
    background: "transparent",
    color: "var(--text-mute)",
    letterSpacing: 0.4,
    transition: "color .15s, background .15s",
    fontFamily: "inherit",
  };
  const activePill = {
    ...pillBase,
    background: "#fff",
    color: "var(--teal-dark)",
    boxShadow: "0 1px 2px rgba(0,0,0,0.08)",
    borderRadius: 8,
  };
  return (
    <div data-no-i18n style={{
      position: "fixed",
      top: 14,
      right: 16,
      zIndex: 9999,
      display: "flex",
      alignItems: "center",
      background: "#eef4f5",
      border: "1px solid var(--border)",
      borderRadius: 10,
      padding: 3,
      boxShadow: "0 2px 8px rgba(0,0,0,0.05)",
    }}>
      <button onClick={() => set("pt")} style={lang === "pt" ? activePill : pillBase}>PT</button>
      <button onClick={() => set("en")} style={lang === "en" ? activePill : pillBase}>EN</button>
    </div>
  );
}

function LoadingScreen() {
  return (
    <div style={{
      minHeight: "100vh",
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
      background: "var(--bg-light)",
      color: "var(--text-mute)",
      fontSize: 15,
      fontWeight: 600,
    }}>
      Carregando contexto da plataforma...
    </div>
  );
}

function translateLoginError(error) {
  const code = (error && error.code) || "";
  if (code.includes("auth/invalid-credential")) return "Email ou senha inválidos.";
  if (code.includes("auth/wrong-password")) return "Email ou senha inválidos.";
  if (code.includes("auth/user-not-found")) return "Usuário não encontrado.";
  if (code.includes("auth/too-many-requests")) return "Muitas tentativas. Tente novamente em alguns minutos.";
  if (code.includes("auth/network-request-failed")) return "Falha de rede. Verifique sua conexão.";
  return "Não foi possível fazer login agora. Tente novamente.";
}

function AccessGate({ onLoggedIn, onBack }) {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [rememberMe, setRememberMe] = useState(true);
  const [showPassword, setShowPassword] = useState(false);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");
  const C = window.HOME_COLORS;

  const handleLogin = async (event) => {
    event.preventDefault();
    if (loading) return;

    setLoading(true);
    setError("");

    try {
      await window.ScolistAPI.login(email.trim(), password, rememberMe);
      const context = await window.ScolistAPI.getSetupContext();
      onLoggedIn(context);
      setPassword("");
    } catch (e) {
      console.error(e);
      setError(translateLoginError(e));
    } finally {
      setLoading(false);
    }
  };

  return (
    <div style={{
      minHeight: "100vh",
      display: "flex",
      flexDirection: "column",
      background: "#fff",
      fontFamily: C.font,
    }}>
      <div style={{ flex: 1, display: "flex", flexWrap: "wrap", alignItems: "center", justifyContent: "center", gap: 40, padding: "28px 40px 16px" }}>

        <div style={{ flex: "0 1 420px", display: "flex", flexDirection: "column", justifyContent: "center" }}>
          <div style={{ width: "100%", maxWidth: 420 }}>
            {onBack ? (
              <button
                type="button"
                onClick={onBack}
                style={{ background: "none", border: "none", cursor: "pointer", padding: 0, marginBottom: 18, font: `600 14px/1 ${C.font}`, color: C.slate600, display: "inline-flex", alignItems: "center", gap: 6 }}
              >
                ← Voltar
              </button>
            ) : null}
            <img src="assets/scolist-wordmark-teal.png" alt="Scolist" style={{ height: 42, width: "auto", display: "block" }} />

            <div style={{ font: `600 13px/1.2 ${C.font}`, letterSpacing: "0.18em", textTransform: "uppercase", color: C.teal300, marginTop: 26 }}>Entrar</div>
            <h1 style={{ font: `600 32px/1.16 ${C.font}`, color: C.textStrong, margin: "12px 0 0" }}>Bom te ver de volta</h1>
            <p style={{ font: `400 17px/1.55 ${C.font}`, color: C.textBody, margin: "10px 0 0" }}>
              Escolas montam a lista do próximo ano com o Lex, série por série.
            </p>

            <form onSubmit={handleLogin} style={{ display: "grid", gap: 14, marginTop: 26 }}>
              <label style={{ display: "grid", gap: 8 }}>
                <span style={{ font: `600 14px/1.2 ${C.font}`, color: C.textStrong }}>E-mail institucional</span>
                <input
                  type="email"
                  value={email}
                  onChange={(event) => setEmail(event.target.value)}
                  required
                  placeholder="coordenacao@suaescola.com.br"
                  style={{
                    width: "100%", boxSizing: "border-box", font: `400 16px/1.2 ${C.font}`, color: C.textStrong,
                    padding: "14px 18px", borderRadius: 999, background: "#fff", border: `1.5px solid ${C.borderHairline}`, outline: "none",
                  }}
                />
              </label>

              <div style={{ display: "grid", gap: 8 }}>
                <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 12 }}>
                  <span style={{ font: `600 14px/1.2 ${C.font}`, color: C.textStrong }}>Senha</span>
                  <a href="#" onClick={(e) => e.preventDefault()} style={{ font: `500 13px/1.2 ${C.font}`, color: C.teal300 }}>Esqueci minha senha</a>
                </div>
                <div style={{ position: "relative", display: "flex", alignItems: "center" }}>
                  <input
                    type={showPassword ? "text" : "password"}
                    value={password}
                    onChange={(event) => setPassword(event.target.value)}
                    required
                    placeholder="••••••••"
                    style={{
                      width: "100%", boxSizing: "border-box", font: `400 16px/1.2 ${C.font}`, color: C.textStrong,
                      padding: "14px 92px 14px 18px", borderRadius: 999, background: "#fff", border: `1.5px solid ${C.borderHairline}`, outline: "none",
                    }}
                  />
                  <button
                    type="button"
                    onClick={() => setShowPassword((v) => !v)}
                    style={{ position: "absolute", right: 8, border: "none", background: "transparent", cursor: "pointer", font: `600 13px/1 ${C.font}`, color: C.teal300, padding: "10px 12px", borderRadius: 999 }}
                  >
                    {showPassword ? "ocultar" : "mostrar"}
                  </button>
                </div>
              </div>

              <label style={{ display: "flex", alignItems: "center", gap: 8, font: `400 14px/1.2 ${C.font}`, color: C.slate600 }}>
                <input
                  type="checkbox"
                  checked={rememberMe}
                  onChange={(event) => setRememberMe(event.target.checked)}
                />
                Manter sessão ativa neste navegador
              </label>

              {error && (
                <div style={{
                  background: "#fff1f1", border: "1px solid #f3b2b2", borderRadius: 10, padding: "10px 12px",
                  color: "#8a2525", font: `600 13px/1.4 ${C.font}`,
                }}>
                  {error}
                </div>
              )}

              <button
                type="submit"
                disabled={loading}
                style={{
                  width: "100%", border: "none", cursor: loading ? "default" : "pointer", borderRadius: 999,
                  padding: "15px 26px", font: `600 16px/1 ${C.font}`,
                  background: loading ? C.teal100 : C.sun400, color: C.teal700,
                }}
              >
                {loading ? "Entrando..." : "Entrar"}
              </button>

              <p style={{ font: `400 14px/1.4 ${C.font}`, color: C.textBody, margin: 0, textAlign: "center" }}>
                <a href="mailto:contato@scolist.com.br" style={{ color: C.teal300 }}>Fale com a equipe</a>
              </p>
            </form>
          </div>
        </div>

        <div style={{ flex: "0 1 360px", display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center", textAlign: "center" }}>
          <img src="assets/lex1.png" alt="Lex" style={{ width: "100%", maxWidth: 230, height: "auto", display: "block" }} />
          <p style={{ font: `600 24px/1.3 ${C.font}`, color: C.textStrong, margin: "16px 0 0", maxWidth: "20ch" }}>
            A lista de volta às aulas, montada nível por nível.
          </p>
          <p style={{ font: `400 15px/1.5 ${C.font}`, color: C.textBody, margin: "8px 0 0", maxWidth: "34ch" }}>
            Literatura, didáticos e papelaria em um link só, com a escala Scolex® por trás de cada indicação.
          </p>
        </div>

      </div>

      <div style={{ padding: "0 0 18px" }}>
        <div style={{ font: `600 13px/1.2 ${C.font}`, letterSpacing: "0.18em", textTransform: "uppercase", color: C.slate600, textAlign: "center" }}>
          Editoras parceiras
        </div>
        <img
          src="assets/editoras-strip-crop.png"
          alt="Companhia das Letras, Penguin, Scholastic, Pallas, Bloomsbury, Pallas Mini, Global, Record, Moderna, HarperCollins, Piraporiando, Savvas, Pearson, Girassol, Editora Globo e FTD"
          style={{ width: "100%", height: "auto", display: "block", marginTop: 10, maxWidth: 900, marginLeft: "auto", marginRight: "auto" }}
        />
      </div>
    </div>
  );
}

// Tela de seleção de escola — aparece logo depois do login (ou ao restaurar
// sessão) quando a conta atende mais de uma escola. Antes disso, a conta
// caía silenciosamente na primeira escola em ordem alfabética
// (chooseSchool, api.js) — ninguém escolhia nada, então uma conta que
// atende duas escolas podia acabar editando/vendo a lista da escola errada
// sem perceber. A escolha feita aqui fica confirmada (escopada por uid, ver
// getConfirmedSchoolId/selectSchool em api.js) e não pergunta de novo nas
// próximas sessões — só reaparece via "trocar escola" (Shell, sidebar).
function SchoolPicker({ schools, currentSchoolId, onPick, onCancel, cancelLabel }) {
  const [picking, setPicking] = useState(null); // id da escola sendo confirmada
  const [error, setError] = useState("");

  const handlePick = async (schoolId) => {
    if (picking) return;
    setPicking(schoolId);
    setError("");
    try {
      await onPick(schoolId);
    } catch (e) {
      console.error(e);
      setError("Não foi possível confirmar essa escola agora. Tente de novo.");
      setPicking(null);
    }
  };

  return (
    <div style={{
      minHeight: "100vh",
      padding: "48px 20px 24px",
      background: "linear-gradient(180deg, #f0fbfb 0%, #e7f5f7 100%)",
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
    }}>
      <div style={{
        width: "100%",
        maxWidth: 640,
        background: "#fff",
        border: "1px solid var(--border)",
        borderRadius: 16,
        padding: 24,
        boxShadow: "var(--shadow-sm)",
      }}>
        <div style={{ fontSize: 12, fontWeight: 700, color: "var(--teal-dark)", textTransform: "uppercase", letterSpacing: 1 }}>
          Sua conta atende mais de uma escola
        </div>
        <h1 style={{ marginTop: 8, fontSize: 26, lineHeight: 1.15, letterSpacing: -0.5 }}>
          Qual escola você quer abrir?
        </h1>
        <p style={{ marginTop: 6, marginBottom: 18, color: "var(--text-mute)", fontSize: 14 }}>
          Você pode trocar de escola depois a qualquer momento pelo menu lateral.
        </p>

        <div style={{ display: "flex", flexDirection: "column", gap: 10, maxHeight: "50vh", overflowY: "auto", paddingRight: 4 }}>
          {schools.map((school) => {
            const isCurrent = school.id === currentSchoolId;
            const isPicking = picking === school.id;
            return (
              <button
                key={school.id}
                onClick={() => handlePick(school.id)}
                disabled={!!picking}
                style={{
                  display: "flex", alignItems: "center", gap: 14,
                  padding: "14px 16px", textAlign: "left",
                  border: `1.5px solid ${isCurrent ? "var(--teal)" : "var(--border)"}`,
                  background: isCurrent ? "var(--bg-light)" : "#fff",
                  borderRadius: 12,
                  cursor: picking ? "default" : "pointer",
                  opacity: picking && !isPicking ? 0.5 : 1,
                }}
              >
                <div style={{
                  width: 40, height: 40, borderRadius: school.logoUrl ? 8 : "50%",
                  background: school.logoUrl ? "#fff" : "var(--teal)",
                  color: "#fff", border: school.logoUrl ? "1px solid var(--border)" : "none",
                  display: "flex", alignItems: "center", justifyContent: "center",
                  overflow: "hidden", flexShrink: 0, fontSize: 14, fontWeight: 700,
                }}>
                  {school.logoUrl
                    ? <img src={school.logoUrl} alt={school.name} style={{ maxWidth: "82%", maxHeight: "82%", objectFit: "contain" }}/>
                    : (school.name || "?").split(/\s+/).map(w => w[0]).filter(Boolean).slice(0, 2).join("").toUpperCase()}
                </div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 15, fontWeight: 700, color: "var(--text)" }}>{school.name || "Escola sem nome"}</div>
                  {isCurrent && <div style={{ fontSize: 11.5, color: "var(--teal-dark)", fontWeight: 600, marginTop: 2 }}>Escola atual</div>}
                </div>
                <div style={{ fontSize: 13, fontWeight: 700, color: "var(--teal-dark)", flexShrink: 0 }}>
                  {isPicking ? "Abrindo…" : "Selecionar →"}
                </div>
              </button>
            );
          })}
        </div>

        {error && (
          <div style={{
            marginTop: 14,
            background: "#fff1f1", border: "1px solid #f3b2b2",
            borderRadius: 10, padding: "10px 12px",
            color: "#8a2525", fontSize: 13, fontWeight: 600,
          }}>
            {error}
          </div>
        )}

        {onCancel && (
          <div style={{ marginTop: 18, textAlign: "center" }}>
            <button onClick={onCancel} disabled={!!picking} style={{
              background: "transparent", border: "none",
              color: "var(--text-mute)", fontSize: 13, fontWeight: 600,
              cursor: picking ? "default" : "pointer",
            }}>
              {cancelLabel || "Cancelar"}
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

function App() {
  const [current, setCurrent] = useState(() => {
    try { return JSON.parse(localStorage.getItem(LS_KEY) || "{}").current || 0; } catch { return 0; }
  });
  const [data, setData] = useState(() => {
    try { return JSON.parse(localStorage.getItem(LS_KEY) || "{}").data || {}; } catch { return {}; }
  });

  const [account, setAccount] = useState(null);
  const [authResolved, setAuthResolved] = useState(false);
  // Visitante não-logado vê a Home institucional primeiro; "Entrar" (ou
  // qualquer CTA da Home) revela o AccessGate. Volta pra Home no "← Voltar"
  // do login ou depois de um logout.
  const [showLogin, setShowLogin] = useState(false);
  // { schools, selectedSchoolId } quando a conta atende mais de uma escola
  // e ainda precisa escolher (needsSchoolPick do backend) ou quando a
  // pessoa pediu pra trocar de escola pelo menu (ver switchSchool mais
  // abaixo) — null quando não há seleção pendente.
  const [pendingSchoolPick, setPendingSchoolPick] = useState(null);
  // true durante completeLogin (login, restauração de sessão, ou troca de
  // escola) — setAccount roda antes do await de resolveWizardProgress, o
  // que causa um render intermediário com a escola NOVA mas current/data
  // ainda da escola ANTIGA. Sem essa trava, o efeito de autosave (mais
  // abaixo) veria essa combinação inconsistente e poderia salvar o
  // progresso antigo sob a chave da escola nova. A guarda de debounce já
  // resolveria isso na prática (o próximo render, com os dados certos,
  // cancela esse timer errado antes dele disparar), mas essa trava fecha
  // a janela por completo em vez de depender só do timing.
  const isLoadingProgressRef = useRef(false);

  const persistRuntimeContext = (context) => {
    if (!context || !context.account) return;

    const schools = Array.isArray(context.schools) ? context.schools : [];
    const grades = Array.isArray(context.grades) ? context.grades : [];

    const schoolId = context.selectedSchoolId || (schools[0] && schools[0].id) || "";
    const school = schools.find((each) => each.id === schoolId) || schools[0] || null;

    const gradeId = context.selectedGradeId || (grades[0] && grades[0].id) || "";
    const grade = grades.find((each) => each.id === gradeId) || grades[0] || null;

    setData((prev) => ({
      ...prev,
      schoolName: (school && school.name) || prev.schoolName,
      runtimeSelection: {
        ...(prev.runtimeSelection || {}),
        availableSchools: schools,
        availableGrades: grades,
        schoolId,
        schoolName: (school && school.name) || "",
        period: context.period ? String(context.period) : (school && school.currentPeriod ? String(school.currentPeriod) : ""),
        gradeId,
        gradeName: (grade && (grade.name || grade.id)) || "",
        updatedAt: new Date().toISOString(),
      },
    }));
  };

  // Termina o login/restauração de sessão a partir de um contexto já
  // resolvido pra uma única escola (needsSchoolPick false — ou porque só
  // tem uma, ou porque a pessoa acabou de escolher no SchoolPicker).
  // Compartilhada entre o efeito de mount (restaura sessão) e o
  // onLoggedIn do AccessGate (login explícito) pra não duplicar essa
  // sequência em dois lugares.
  const completeLogin = async (context) => {
    isLoadingProgressRef.current = true;
    try {
      const nextAccount = (context && context.account) || null;
      setAccount(nextAccount);
      // Sobrepõe o que foi lido do slot genérico no primeiro render (pode
      // ser de outra conta/escola que usou esse navegador por último) pelo
      // progresso resolvido dessa conta+escola — local ou do backend, o
      // que estiver mais atualizado (ver resolveWizardProgress).
      const resolved = await resolveWizardProgress(nextAccount);
      // Sempre define current/data explicitamente (nunca deixa como
      // estava) — completeLogin também roda ao trocar de escola pelo
      // menu (switchSchool), e uma escola sem progresso salvo ainda
      // (resolved.current/data null) precisa começar vazia, não herdar o
      // que estava na tela da escola anterior.
      setCurrent(typeof resolved.current === "number" ? resolved.current : 0);
      setData(resolved.data || {});
      restoreRuntimeCatalog(resolved.catalog);
      if (context && context.account) persistRuntimeContext(context);
    } finally {
      isLoadingProgressRef.current = false;
    }
  };

  useEffect(() => {
    let cancel = false;
    (async () => {
      try {
        const context = await window.ScolistAPI.getSetupContext();
        if (cancel) return;
        if (context && context.needsSchoolPick) {
          setPendingSchoolPick({ schools: context.schools, selectedSchoolId: context.selectedSchoolId });
          return;
        }
        await completeLogin(context);
      } catch (e) {
        if (!cancel) {
          setAccount(null);
          console.warn("Falha ao carregar conta:", e);
        }
      } finally {
        if (!cancel) setAuthResolved(true);
      }
    })();
    return () => { cancel = true; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const [demoActive, setDemoActive] = useState(false);
  const demoTimerRef = useRef(null);

  // Hash-based route for family-facing pages (#familia, #familia/serie/3, etc.)
  const [hash, setHashState] = useState(() => (location.hash || "").replace(/^#\/?/, ""));
  useEffect(() => {
    const onHash = () => setHashState((location.hash || "").replace(/^#\/?/, ""));
    window.addEventListener("hashchange", onHash);
    return () => window.removeEventListener("hashchange", onHash);
  }, []);
  const setHash = (h) => { location.hash = "#" + h; window.scrollTo({ top: 0 }); };

  // Payload do autosave ainda não confirmado no backend — atualizado a cada
  // rodada do efeito abaixo, limpo quando o PUT (normal ou de emergência)
  // é enviado. Existe só pro flush de emergência (pagehide/visibilitychange
  // logo abaixo) saber o que reenviar se a aba fechar antes do debounce de
  // 350ms dar tempo de disparar sozinho. pendingSyncTimeoutRef guarda o
  // handle do setTimeout em si, só pra o flush poder cancelá-lo — sem
  // isso, depois de um flush de emergência bem sucedido o debounce
  // original ainda dispararia sozinho 300ms depois e reenviaria o mesmo
  // payload de novo (inofensivo, mas escrita/leitura à toa no Firestore).
  const pendingSyncRef = useRef(null);
  const pendingSyncTimeoutRef = useRef(null);

  useEffect(() => {
    // Durante completeLogin (login, restauração, ou troca de escola),
    // account já pode ter mudado pra escola nova mas current/data ainda
    // são da escola antiga (setAccount roda antes do await) — não grava
    // nada enquanto isso, senão salvaria o progresso errado sob a chave
    // da escola nova. Ver isLoadingProgressRef acima.
    if (isLoadingProgressRef.current) return;

    // catalog vai junto — snapshotRuntimeCatalog() lê window.CATALOG na
    // hora (não é React state, não dispara esse efeito sozinho, mas muda
    // toda vez que data.pinned muda também, então "de carona" nesse
    // efeito já cobre os momentos certos).
    const updatedAt = new Date().toISOString();
    const catalog = snapshotRuntimeCatalog();
    const snapshot = JSON.stringify({ current, data, catalog, updatedAt });
    localStorage.setItem(LS_KEY, snapshot);
    const scopedKey = lsKeyFor(account);
    if (scopedKey) localStorage.setItem(scopedKey, snapshot);

    const uid = account && account.user && account.user.id;
    if (!uid) return;
    const schoolId = (account && account.school && account.school.id) || null;

    // Autosave debounded pro backend (350ms — mesmo padrão de debounce já
    // usado no projeto pra busca ao digitar, ver steps-curation.jsx). O
    // localStorage acima já persistiu na hora; isso só empurra pro backend
    // pra sincronizar entre dispositivos (bug reportado pela Joana,
    // 29/08/2026: lista montada no Computador A não aparecia no B).
    const payload = buildWizardSyncPayload({ current, data, catalog, updatedAt });
    pendingSyncRef.current = { uid, schoolId, payload };
    const handle = setTimeout(() => {
      window.ScolistAPI.saveWizardProgress(uid, schoolId, payload).then(() => {
        if (pendingSyncRef.current && pendingSyncRef.current.payload === payload) pendingSyncRef.current = null;
      }).catch((error) => {
        // Falha silenciosa de propósito: o localStorage já persistiu acima
        // como rede de segurança local — só perde a sincronização entre
        // dispositivos até o próximo autosave bem sucedido.
        console.warn("Falha ao sincronizar progresso do wizard com o backend:", error);
      });
    }, 350);
    pendingSyncTimeoutRef.current = handle;
    return () => clearTimeout(handle);
  }, [current, data, account]);

  // Flush de emergência do autosave: o setTimeout de 350ms acima é
  // cancelado (clearTimeout) sempre que o efeito reroda ou o componente
  // desmonta — inclusive quando a aba simplesmente fecha, caso em que ele
  // nunca chega a disparar e a ÚLTIMA mudança (normalmente a mais
  // importante: "acabei de montar a lista, vou fechar e abrir no outro
  // computador") nunca sai do localStorage daquele navegador. Reproduz
  // exatamente o sintoma que a Joana reportou de novo depois do fix
  // original de sincronização (29/08 e 31/08/2026): lista feita no
  // Computador A não aparece no B.
  //
  // visibilitychange (aba escondida — trocou de app, minimizou, fechou)
  // cobre muito mais casos reais que beforeunload sozinho, que não
  // dispara de forma confiável em boa parte do mobile; pagehide cobre o
  // fechamento/navegação em si. Os dois chamam saveWizardProgressKeepalive
  // (api.js), que usa fetch keepalive:true — o único jeito de garantir que
  // o request saia antes da página sumir.
  useEffect(() => {
    const flush = () => {
      if (!pendingSyncRef.current) return;
      const { uid, schoolId, payload } = pendingSyncRef.current;
      clearTimeout(pendingSyncTimeoutRef.current);
      window.ScolistAPI.saveWizardProgressKeepalive(uid, schoolId, payload);
      pendingSyncRef.current = null;
    };
    const onVisibilityChange = () => {
      if (document.visibilityState === "hidden") flush();
    };
    document.addEventListener("visibilitychange", onVisibilityChange);
    window.addEventListener("pagehide", flush);
    return () => {
      document.removeEventListener("visibilitychange", onVisibilityChange);
      window.removeEventListener("pagehide", flush);
    };
  }, []);

  useEffect(() => {
    if (!demoActive) {
      if (demoTimerRef.current) clearTimeout(demoTimerRef.current);
      return;
    }
    const advance = () => {
      setCurrent(c => {
        const next = c + 1;
        if (next >= STEPS.length) { setDemoActive(false); return c; }
        if (next === 1) setData(d => ({ ...d, calendar: "bimestres" }));
        if (next === 2) setData(d => ({ ...d, pastListChoice: "skip" }));
        if (next === 3) setData(d => ({ ...d, themes: ["identidade", "diversidade", "emocoes", "aventura"] }));
        if (next === 4) setData(d => ({ ...d, scolexEN: "connected", scolexPT: "connected" }));
        if (next === 5) setData(d => ({ ...d, qtyPerYearPT: 3, qtyPerYearEN: 2, qtyPerYear: 5 }));
        return next;
      });
      demoTimerRef.current = setTimeout(advance, 3500);
    };
    demoTimerRef.current = setTimeout(advance, 3500);
    return () => { if (demoTimerRef.current) clearTimeout(demoTimerRef.current); };
  }, [demoActive]);

  const toggleDemo = () => {
    if (!demoActive) {
      setCurrent(0);
      setData(prev => ({
        ...prev,
        schoolName: resolveSchoolName(account),
      }));
    }
    setDemoActive(a => !a);
  };

  const canNext = true;

  const nextLabel = (() => {
    if (current === 0) return "Vamos nessa";
    if (current === STEPS.length - 2) return "Gerar link final";
    if (current === STEPS.length - 1) return "Finalizar";
    return "Continuar";
  })();

  const onNext = () => {
    if (current === STEPS.length - 1) {
      setCurrent(0);
      setData(prev => ({
        schoolName: resolveSchoolName(account),
        runtimeSelection: prev.runtimeSelection || null,
      }));
      return;
    }
    setCurrent(c => Math.min(STEPS.length - 1, c + 1));
    window.scrollTo({ top: 0, behavior: "smooth" });
  };
  const onPrev = () => { setCurrent(c => Math.max(0, c - 1)); window.scrollTo({ top: 0, behavior: "smooth" }); };
  const onJump = i => { setCurrent(i); window.scrollTo({ top: 0, behavior: "smooth" }); };

  // Encerra a sessão (Firebase + Scolex) e zera o wizard local — evita que o
  // próximo login nesse navegador herde progresso de outra escola/usuário.
  const handleLogout = async () => {
    try {
      if (window.ScolistAPI && typeof window.ScolistAPI.logout === "function") {
        await window.ScolistAPI.logout();
      }
    } catch (e) {
      console.warn("Falha ao encerrar sessão:", e);
    }
    // Não apaga mais o progresso salvo (nem o slot genérico nem o
    // escopado por conta) — só limpa o estado em memória, pra tela de
    // login aparecer limpa. Se a mesma conta logar de novo,
    // loadScopedProgress recupera tudo pelo slot escopado por uid (ver
    // comentário em lsKeyFor). Antes o localStorage.removeItem(LS_KEY)
    // destruía tudo que não tinha persistência própria no backend.
    setDemoActive(false);
    setCurrent(0);
    setData({});
    setAccount(null);
    setShowLogin(false);
  };

  // Reabre o seletor de escola a partir de dentro do app (menu lateral,
  // Shell) — usa a lista de escolas já carregada no login
  // (data.runtimeSelection.availableSchools), sem precisar ir no backend
  // de novo. Só existe quando a conta atende mais de uma escola.
  const switchSchool = () => {
    const schools = (data.runtimeSelection && data.runtimeSelection.availableSchools) || [];
    if (schools.length < 2) return;
    setPendingSchoolPick({ schools, selectedSchoolId: (data.runtimeSelection && data.runtimeSelection.schoolId) || null });
  };

  const handleSchoolPick = async (schoolId) => {
    const context = await window.ScolistAPI.selectSchool(schoolId);
    setPendingSchoolPick(null);
    await completeLogin(context);
  };

  const steps = [
    <Step0_Welcome/>, <Step1_Calendar/>, <Step1_PastList/>, <Step2_Themes/>,
    <Step3_Scolex/>, <Step5_Calibration/>,
    <Step7_Availability/>,
    <Step9_Recommend/>, <Step10_Detail/>,
    <StepDidaticosConfig/>, <StepDidaticosBuilder/>,
    <StepPapelariaIntro/>, <StepPapelariaBuilder/>,
    <Step11_Confirm/>, <Step8_Pricing/>, <Step13_Link/>,
  ];

  const schoolName = resolveSchoolName(account);
  const canSwitchSchool = ((data.runtimeSelection && data.runtimeSelection.availableSchools) || []).length > 1;
  const ctxValue = { data, setData, schoolName, account, setAccount, logout: handleLogout, switchSchool, canSwitchSchool };

  if (!authResolved) {
    return (
      <AppCtx.Provider value={ctxValue}>
        <LangToggle/>
        <LoadingScreen/>
      </AppCtx.Provider>
    );
  }

  // Family route takes over the whole viewport (outside the assistant shell)
  const familiaNode = window.renderFamiliaRoute ? window.renderFamiliaRoute(hash, data, setHash) : null;
  if (familiaNode) {
    return (
      <AppCtx.Provider value={ctxValue}>
        <LangToggle/>
        {familiaNode}
      </AppCtx.Provider>
    );
  }

  if (pendingSchoolPick) {
    return (
      <AppCtx.Provider value={ctxValue}>
        <LangToggle/>
        <SchoolPicker
          schools={pendingSchoolPick.schools}
          currentSchoolId={pendingSchoolPick.selectedSchoolId}
          onPick={handleSchoolPick}
          onCancel={account
            ? () => setPendingSchoolPick(null)
            : async () => { await window.ScolistAPI.logout(); setPendingSchoolPick(null); setAccount(null); }}
          cancelLabel={account ? "Cancelar" : "Sair e usar outra conta"}
        />
      </AppCtx.Provider>
    );
  }

  if (!account && !showLogin) {
    return (
      <AppCtx.Provider value={ctxValue}>
        <LangToggle/>
        <MarketingHome onEnter={() => setShowLogin(true)} />
      </AppCtx.Provider>
    );
  }

  if (!account) {
    return (
      <AppCtx.Provider value={ctxValue}>
        <LangToggle/>
        <AccessGate
          onBack={() => setShowLogin(false)}
          onLoggedIn={async (context) => {
            if (context && context.needsSchoolPick) {
              setPendingSchoolPick({ schools: context.schools, selectedSchoolId: context.selectedSchoolId });
              return;
            }
            // Mesma lógica do efeito de mount acima — recupera o progresso
            // dessa conta (local ou do backend, o que estiver mais
            // atualizado) se ela já tinha usado esse navegador antes, ou já
            // tinha sincronizado de outro dispositivo.
            await completeLogin(context);
            setCurrent(0);
          }}
        />
      </AppCtx.Provider>
    );
  }

  return (
    <AppCtx.Provider value={ctxValue}>
      <LangToggle/>
      <Shell
        current={current} onJump={onJump}
        onPrev={onPrev} onNext={onNext}
        canNext={canNext} nextLabel={nextLabel}
        onDemo={toggleDemo} demoActive={demoActive}>
        <div key={current} style={{ animation: "fadeIn 0.4s ease both" }}>
          {steps[current]}
        </div>
        <style>{`
          @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
          ::-webkit-scrollbar { width: 10px; height: 10px; }
          ::-webkit-scrollbar-thumb { background: #cfe5e7; border-radius: 5px; }
          ::-webkit-scrollbar-thumb:hover { background: var(--teal); }
          input[type="range"] { height: 4px; }
        `}</style>
      </Shell>
    </AppCtx.Provider>
  );
}

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