/* ============================================================
   EDUCARE — EstadoNuvem: o que era localStorage agora é do servidor

   O QUE MORA AQUI: rascunho de avaliação em andamento, normas e
   configuração dos instrumentos, perfil profissional e recebimentos
   avulsos. Tudo isso vivia no localStorage do computador da
   profissional — não entrava em backup, não ia junto para outra
   máquina, sumia com uma limpeza do navegador, e dividia um teto de
   ~5MB que o áudio costumava estourar ("memória cheia").

   POR QUE A API É SÍNCRONA: os instrumentos leem e gravam o rascunho a
   cada tecla (loadAvals/saveAvals). Trocar isso por await espalharia
   async por centenas de pontos em cinco arquivos grandes, e um await no
   meio do preenchimento da grade é atraso que a profissional sente com
   a criança na frente. Então o servidor é carregado UMA vez na abertura
   (hidratar), fica espelhado em memória, e a escrita volta na hora — o
   envio acontece depois, agrupado.

   O PREÇO DISSO é que existe uma janela em que a alteração está só na
   memória. Ela é curta (ATRASO_MS) e coberta por duas defesas: um
   espelho no localStorage, que sobrevive a fechar o navegador, e o
   aviso do beforeunload enquanto houver coisa por enviar.

   O localStorage não morreu: virou CACHE e rede de segurança offline,
   como o IndexedDB virou cache do áudio. Quem manda é o servidor.
   ============================================================ */
(function () {
  const TABELA = "user_state";
  const ATRASO_MS = 1200;      // agrupa rajadas de digitação numa gravação só
  /* O espelho local leva o id da conta na chave. Até 25/09/2026 era
     "educare-espelho-<chave>", sem dono: quando outra pessoa entrava no
     mesmo navegador, hidratar() lia o rascunho e o perfil da anterior e
     os ENVIAVA para a conta nova (auditoria, P0 — confirmado no servidor). */
  const PREFIXO_LOCAL = "educare-espelho:";
  const PREFIXO_SEM_DONO = "educare-espelho-";

  const sb = () => window.sb;

  let espelho = {};            // chave -> valor (objeto)
  let sujas = new Set();       // chaves alteradas e ainda não enviadas
  let timer = null;
  let hidratado = false;
  let dono = null;             // id da conta cujo espelho está em uso
  let base = {};               // chave -> último valor que o servidor confirmou

  /* Rascunhos são UM objeto por instrumento, { id → avaliação }, com as
     crianças todas dentro. Regravar o objeto inteiro fazia o computador
     que salvasse por último apagar o que o outro tinha criado (auditoria
     de 25/09/2026, P0). Para estas chaves o envio relê o servidor e
     junta por id, em três vias: o que mudou AQUI desde a base vale o
     daqui (inclusive apagar); o resto vale o do servidor. */
  const COLECOES_POR_ID = new Set([
    "educare-dnoi-avaliacoes", "educare-gan-avaliacoes",
    "educare-fluencia-avaliacoes", "educare-anamnese-infantil",
  ]);

  const copia = (v) => (v === undefined ? undefined : JSON.parse(JSON.stringify(v)));
  const igual = (a, b) => JSON.stringify(a) === JSON.stringify(b);

  function mesclarPorId(daBase, daqui, doServidor) {
    const b = daBase || {}, l = daqui || {}, s = doServidor || {};
    const junto = {};
    new Set([...Object.keys(b), ...Object.keys(l), ...Object.keys(s)]).forEach((id) => {
      const valor = igual(l[id], b[id]) ? s[id] : l[id];
      if (valor !== undefined) junto[id] = valor;
    });
    return junto;
  }

  /* ---------------- cache local (rede de segurança) ---------------- */

  const chaveLocal = (chave) => PREFIXO_LOCAL + dono + ":" + chave;

  function lerJson(nome) {
    try {
      const cru = localStorage.getItem(nome);
      return cru ? JSON.parse(cru) : null;
    } catch (e) { return null; }
  }

  function lerLocal(chave) {
    return dono ? lerJson(chaveLocal(chave)) : null;
  }

  /* Espelhar no navegador é o que segura o dado se a aba fechar antes do
     envio. Cota estourada aqui NÃO é erro: o servidor continua sendo o
     destino, e o espelho é dispensável. */
  function gravarLocal(chave, valor) {
    if (!dono) return;
    try { localStorage.setItem(chaveLocal(chave), JSON.stringify(valor)); }
    catch (e) { /* sem espelho; o envio ao servidor segue */ }
  }

  /* Dado local SEM dono: o espelho antigo e as chaves de antes da nuvem
     (que têm o mesmo nome da chave). Ele sobe uma única vez, para a
     primeira conta que abrir depois desta versão — no computador de uma
     profissional só, é dela — e é apagado em seguida, para que nenhuma
     outra conta o herde. */
  function tomarSemDono(chave, legado) {
    const valor = lerJson(PREFIXO_SEM_DONO + chave) || (legado[chave] ? legado[chave]() : null);
    try {
      localStorage.removeItem(PREFIXO_SEM_DONO + chave);
      localStorage.removeItem(chave);
    } catch (e) { /* armazenamento bloqueado: nada a apagar */ }
    return valor;
  }

  function apagarSemDonoRestante() {
    try {
      Object.keys(localStorage)
        .filter((k) => k.startsWith(PREFIXO_SEM_DONO))
        .forEach((k) => localStorage.removeItem(k));
    } catch (e) { /* armazenamento bloqueado */ }
  }

  /* Quais chaves desta conta ainda não chegaram ao servidor, gravado no
     navegador. Sem isso, fechar a aba (ou "Recarregar") antes do envio
     perdia a alteração: a próxima abertura dava razão ao servidor e
     ignorava o espelho mais novo (auditoria de 25/09/2026). */
  const chavePendentes = () => "educare-pendentes:" + dono;
  function lerPendentes() { return dono ? (lerJson(chavePendentes()) || []) : []; }
  function gravarPendentes() {
    if (!dono) return;
    try {
      if (sujas.size) localStorage.setItem(chavePendentes(), JSON.stringify([...sujas]));
      else localStorage.removeItem(chavePendentes());
    } catch (e) { /* sem registro de pendentes; o envio segue */ }
  }

  function apagarEspelhoDoDono() {
    if (!dono) return;
    try {
      Object.keys(localStorage)
        .filter((k) => k.startsWith(PREFIXO_LOCAL + dono + ":"))
        .forEach((k) => localStorage.removeItem(k));
      localStorage.removeItem(chavePendentes());
    } catch (e) { /* armazenamento bloqueado */ }
  }

  /* ---------------- servidor ---------------- */

  async function donoAtual() {
    if (!sb()) return null;
    const { data } = await sb().auth.getSession();
    return (data && data.session && data.session.user && data.session.user.id) || null;
  }

  async function enviar(chaves) {
    if (!chaves.length) return false;
    const atual = await donoAtual();
    if (!atual) throw new Error("sem sessão"); // volta para a fila
    /* Troca de conta com envio ainda agendado: o que está no espelho é
       da conta anterior e não pode ir para a atual. */
    if (atual !== dono) return false;

    const colecoes = chaves.filter((k) => COLECOES_POR_ID.has(k));
    if (colecoes.length) {
      const { data, error: erroLeitura } = await sb().from(TABELA).select("key, value").in("key", colecoes);
      if (erroLeitura) throw erroLeitura;
      const doServidor = {};
      (data || []).forEach((r) => { doServidor[r.key] = r.value; });
      colecoes.forEach((k) => {
        espelho[k] = mesclarPorId(base[k], espelho[k], doServidor[k]);
        gravarLocal(k, espelho[k]);
      });
    }

    const linhas = chaves.map((k) => ({ owner_id: dono, key: k, value: espelho[k] == null ? {} : espelho[k] }));
    const { error } = await sb().from(TABELA).upsert(linhas, { onConflict: "owner_id,key" });
    if (error) throw error;
    linhas.forEach((l) => { base[l.key] = copia(l.value); });
    return true;
  }

  async function descarregar() {
    // Quem chama direto (Configurações) já está enviando: o agendado sobraria.
    if (timer) { clearTimeout(timer); timer = null; }
    if (!sujas.size) return;
    const chaves = [...sujas];
    sujas = new Set();
    try {
      await enviar(chaves);
      gravarPendentes();
    } catch (e) {
      chaves.forEach((k) => sujas.add(k)); // devolve para a fila: tenta de novo no próximo ciclo
      gravarPendentes();
      console.warn("[EstadoNuvem] envio falhou, sera repetido:", e && e.message);
      window.dispatchEvent(new CustomEvent("educare-db-erro", {
        detail: "Suas alterações ainda não foram enviadas ao servidor. Verifique a conexão.",
      }));
    }
  }

  function agendar() {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => { timer = null; descarregar(); }, ATRASO_MS);
  }

  /* ---------------- API pública ---------------- */

  /* Carrega tudo de uma vez e resolve a convivência com o que já estava
     no computador. A regra: o SERVIDOR ganha quando tem a chave; quando
     não tem, o que estava local sobe. É isso que faz a primeira abertura
     depois desta mudança levar os rascunhos e as normas dela para a
     nuvem sem pedir nada — e é por isso que a leitura do localStorage
     antigo continua aqui, com o nome de chave de antes. */
  async function hidratar(legado) {
    espelho = {};
    hidratado = false;
    sujas = new Set();
    dono = null;
    if (!sb()) { hidratado = true; return; }
    dono = await donoAtual();

    const { data, error } = await sb().from(TABELA).select("key, value");
    if (error) throw new Error("Falha ao carregar suas configurações: " + error.message);
    base = {};
    (data || []).forEach((r) => { espelho[r.key] = r.value; base[r.key] = copia(r.value); });

    const subir = [];
    Object.keys(legado || {}).forEach((chave) => {
      const semDono = tomarSemDono(chave, legado);           // lido e apagado, sempre
      if (espelho[chave] !== undefined) return;            // servidor manda
      const antigo = lerLocal(chave) || semDono;           // espelho desta conta, senão o sem dono
      if (antigo == null) return;
      const vazio = typeof antigo === "object" && !Array.isArray(antigo) && !Object.keys(antigo).length;
      if (vazio) return;
      espelho[chave] = antigo;
      subir.push(chave);
    });

    apagarSemDonoRestante();

    /* O que ficou pendente na última abertura vale o daqui, e sobe de
       novo (nas coleções, com a junção por id contra o servidor). */
    lerPendentes().forEach((chave) => {
      const local = lerLocal(chave);
      if (local == null) return;
      espelho[chave] = local;
      if (!subir.includes(chave)) sujas.add(chave);
    });

    hidratado = true;
    if (sujas.size) agendar();
    if (subir.length) {
      subir.forEach((k) => gravarLocal(k, espelho[k]));
      try { await enviar(subir); } catch (e) { subir.forEach((k) => sujas.add(k)); agendar(); }
    }
  }

  // Síncrono de propósito (ver cabeçalho). Devolve o padrão até hidratar.
  function ler(chave, padrao) {
    if (espelho[chave] !== undefined) return espelho[chave];
    if (!hidratado) {
      const local = lerLocal(chave);
      if (local != null) return local;
    }
    return padrao === undefined ? null : padrao;
  }

  function gravar(chave, valor) {
    espelho[chave] = valor;
    gravarLocal(chave, valor);
    sujas.add(chave);
    gravarPendentes();
    agendar();
  }

  const pendente = () => sujas.size > 0 || !!timer;

  /* Sair da conta: envia o que falta e só então apaga o espelho desta
     conta do navegador (dado de saúde não fica em computador
     compartilhado). Se o envio falhar, o espelho fica — é a única cópia
     — e a função devolve false para quem chamou decidir. */
  async function sair() {
    if (timer) { clearTimeout(timer); timer = null; }
    await descarregar();
    if (sujas.size) return false;   // continua logada e com tudo em memória
    apagarEspelhoDoDono();
    esquecer();
    return true;
  }

  // Sessão acabou (logout em outra aba, token expirado): nada em memória.
  function esquecer() {
    if (timer) { clearTimeout(timer); timer = null; }
    espelho = {};
    base = {};
    sujas = new Set();
    hidratado = false;
    dono = null;
  }

  /* Fechar a aba com algo na fila perderia a alteração no servidor (o
     espelho local ainda a teria, e a próxima abertura a reenviaria —
     mas só naquele computador). Avisar é mais honesto do que confiar
     nisso silenciosamente. */
  window.addEventListener("beforeunload", (e) => {
    if (!pendente()) return;
    e.preventDefault();
    e.returnValue = "";
  });

  // Voltar a ter rede é o momento óbvio de tentar de novo.
  window.addEventListener("online", () => { if (sujas.size) agendar(); });

  window.EstadoNuvem = { hidratar, ler, gravar, pendente, descarregar, sair, esquecer };
})();
