/* ============================================================
   EDUCARE — MediaStorageService (áudio das avaliações)

   PORQUÊ ESTE SERVIÇO EXISTE / REGRA DE OURO:
   Áudio NUNCA vai para localStorage. localStorage tem teto de ~5MB por
   origem e só aceita string — o áudio precisaria virar base64 (infla ~33%)
   e, ao estourar a quota no meio do setItem, DERRUBA os escores gravados
   junto. Por isso o áudio vive AQUI, em IndexedDB, como Blob NATIVO, numa
   base separada dos escores. Se for tentado a gravar áudio em localStorage
   de novo: NÃO. Foi exatamente o bug que este serviço corrige.

   ONDE O ÁUDIO MORA (mudou em 22/09/2026): a fonte de verdade é o bucket
   "avaliacao-audios" no Supabase, e o IndexedDB virou CACHE. Antes o áudio
   era tratado como material descartável, com prazo de 24h e só no aparelho
   — mas o reconto da criança é a evidência de onde saiu o escore, e há por
   que voltar nele meses depois. No aparelho, ele sumia com uma limpeza de
   navegador, não ia junto para outro computador e não entrava em backup.

   O TTL de 24h continua, valendo SÓ para a cópia local. Nada aqui apaga o
   áudio da nuvem por tempo: isso só acontece quando a profissional clica em
   apagar (deleteForItem / deleteByAssessment).

   Nenhuma tela fala com IndexedDB nem com o Storage direto — só por
   window.MediaStorageService.
   ============================================================ */
(function () {
  const DB_NAME = "educare-media";
  const DB_VER = 1;
  const STORE = "audio";
  const TTL_MS = 24 * 60 * 60 * 1000; // 24 horas

  /* ---------------- IndexedDB base ---------------- */
  let dbPromise = null;
  function openDB() {
    return new Promise((resolve, reject) => {
      if (!("indexedDB" in window)) { reject(new Error("IndexedDB indisponivel neste navegador.")); return; }
      const req = indexedDB.open(DB_NAME, DB_VER);
      req.onupgradeneeded = () => {
        const idb = req.result;
        if (!idb.objectStoreNames.contains(STORE)) {
          const os = idb.createObjectStore(STORE, { keyPath: "id" });
          os.createIndex("expiresAt", "expiresAt", { unique: false });
          os.createIndex("patientId", "patientId", { unique: false });
          os.createIndex("assessmentInstanceId", "assessmentInstanceId", { unique: false });
        }
      };
      req.onsuccess = () => resolve(req.result);
      req.onerror = () => reject(req.error || new Error("Falha ao abrir o banco de midia."));
    });
  }
  function db() { if (!dbPromise) dbPromise = openDB(); return dbPromise; }
  function store(mode) { return db().then((d) => d.transaction(STORE, mode).objectStore(STORE)); }
  const done = (r) => new Promise((res, rej) => { r.onsuccess = () => res(r.result); r.onerror = () => rej(r.error); });

  // id deterministico: a mesma (instancia + item) sempre aponta para o mesmo audio.
  function makeId(assessmentInstanceId, itemId) { return String(assessmentInstanceId) + "|" + String(itemId); }

  /* ---------------- nuvem (Supabase Storage) ----------------
     A NUVEM E O DONO DO AUDIO; o IndexedDB acima e cache.

     Era o contrario: o audio morava so no aparelho e vencia em 24h,
     tratado como material descartavel. So que o reconto da crianca e a
     evidencia de onde saiu o escore, e ha por que voltar nele meses
     depois — devolutiva, reavaliacao, laudo contestado. No aparelho, ele
     sumia quando o navegador limpava os dados, nao acompanhava a
     profissional para outro computador e nao entrava em backup nenhum.

     O cache continua existindo porque tocar o audio durante a aplicacao
     precisa ser instantaneo, e porque a queda de internet no meio de um
     atendimento nao pode travar o teste. O TTL agora vence apenas a
     copia local: a da nuvem fica. */
  const BUCKET = "avaliacao-audios";
  const sb = () => window.sb;

  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;
  }

  /* {dono}/{paciente}/{instancia}/{item}

     O primeiro nivel e o que a policy do bucket confere contra
     auth.uid(); mudar isso quebra o acesso.

     O paciente entra no caminho para que a regra de isolamento do
     getForItem valha TAMBEM quando o audio vem da nuvem. Sem ele, o
     caminho seria adivinhavel so com o avalId, e um patientId errado
     ainda acharia o arquivo — a checagem so pegaria o caso em que o
     audio ja estivesse em cache. Com o paciente no caminho, pedir pela
     crianca errada simplesmente nao encontra nada. */
  function caminhoPasta(dono, patientId, assessmentInstanceId) {
    return dono + "/" + String(patientId) + "/" + String(assessmentInstanceId);
  }
  function caminhoNuvem(dono, patientId, assessmentInstanceId, itemId) {
    return caminhoPasta(dono, patientId, assessmentInstanceId) + "/" + String(itemId);
  }

  async function enviarParaNuvem(rec) {
    const dono = await donoAtual();
    if (!dono) return false;
    const { error } = await sb().storage.from(BUCKET).upload(
      caminhoNuvem(dono, rec.patientId, rec.assessmentInstanceId, rec.itemId),
      rec.blob,
      { contentType: rec.mimeType, upsert: true } // regravar sobrescreve o mesmo caminho
    );
    if (error) throw error;
    return true;
  }

  async function baixarDaNuvem(patientId, assessmentInstanceId, itemId) {
    const dono = await donoAtual();
    if (!dono) return null;
    const { data, error } = await sb().storage.from(BUCKET)
      .download(caminhoNuvem(dono, patientId, assessmentInstanceId, itemId));
    if (error || !data) return null;
    return data; // Blob
  }

  async function apagarDaNuvem(patientId, assessmentInstanceId, itemId) {
    const dono = await donoAtual();
    if (!dono) return false;
    const { error } = await sb().storage.from(BUCKET)
      .remove([caminhoNuvem(dono, patientId, assessmentInstanceId, itemId)]);
    return !error;
  }

  // Quais itens desta avaliacao existem na nuvem (para o prontuario montar os players).
  async function listarNaNuvem(patientId, assessmentInstanceId) {
    const dono = await donoAtual();
    if (!dono || !patientId || !assessmentInstanceId) return [];
    const { data, error } = await sb().storage.from(BUCKET)
      .list(caminhoPasta(dono, patientId, assessmentInstanceId), { limit: 100 });
    if (error || !data) return [];
    return data.filter((o) => o && o.name).map((o) => o.name);
  }

  /* ---------------- API publica ---------------- */

  // save({ patientId, assessmentInstanceId, instrument, itemId, blob, mimeType }) -> id
  async function save(opts) {
    opts = opts || {};
    const patientId = opts.patientId, assessmentInstanceId = opts.assessmentInstanceId, itemId = opts.itemId, blob = opts.blob;
    if (!patientId || !assessmentInstanceId || !itemId) throw new Error("save exige patientId, assessmentInstanceId e itemId.");
    if (!(blob instanceof Blob)) throw new Error("save exige um Blob nativo (nunca base64).");
    const now = Date.now();
    const rec = {
      id: makeId(assessmentInstanceId, itemId),
      patientId: String(patientId),
      assessmentInstanceId: String(assessmentInstanceId),
      instrument: opts.instrument || "",
      itemId: String(itemId),
      blob: blob,
      mimeType: opts.mimeType || blob.type || "audio/webm",
      createdAt: now,
      expiresAt: now + TTL_MS,
    };
    /* Grava nos dois lugares e so desiste se OS DOIS falharem.
       Antes, um IndexedDB cheio ja era motivo para dizer "nao guardei" —
       hoje, com a nuvem sendo o dono, isso seria mentira: o audio esta
       salvo, e o que faltou foi so a copia rapida local. O inverso
       tambem vale: internet caida no meio do atendimento nao pode travar
       a aplicacao, entao a copia local sozinha ja conta como sucesso. */
    let local = false, nuvem = false, erroLocal = null, erroNuvem = null;

    try {
      const os = await store("readwrite");
      await done(os.put(rec));
      local = true;
    } catch (e) {
      erroLocal = e;
      console.warn("[MediaStore] cache local nao aceitou o audio:", (e && e.name) || "", e && e.message);
    }

    try {
      nuvem = await enviarParaNuvem(rec);
    } catch (e) {
      erroNuvem = e;
      console.warn("[MediaStore] envio para a nuvem falhou:", e && e.message);
    }

    if (!local && !nuvem) {
      const name = (erroLocal && erroLocal.name) || "";
      const err = new Error("audio-nao-armazenado");
      err.name = name === "QuotaExceededError" ? "QuotaExceededError" : "MediaStoreError";
      err.cause = erroNuvem || erroLocal;
      throw err;
    }
    // Quem chama pode avisar que o audio ainda nao subiu (so cache local).
    return { id: rec.id, local: local, nuvem: nuvem };
  }

  async function get(id) {
    if (!id) return null;
    const os = await store("readonly");
    const rec = await done(os.get(id));
    if (!rec) return null;
    if (rec.expiresAt <= Date.now()) { del(id).catch(() => {}); return null; }
    return rec;
  }

  /* Leitura enforcada: exige patientId E assessmentInstanceId (regra de isolamento).

     Cache primeiro, nuvem depois. O recuo para a nuvem e o que faz o
     audio abrir num computador onde ele nunca foi gravado, ou depois que
     a copia local venceu — sem ele, "salvo na nuvem" nao apareceria em
     lugar nenhum da tela. Ao baixar, repovoa o cache: reabrir a mesma
     ficha na sequencia nao paga a rede de novo. */
  async function getForItem(q) {
    q = q || {};
    if (!q.patientId || !q.assessmentInstanceId || !q.itemId) throw new Error("getForItem exige patientId, assessmentInstanceId e itemId.");

    const rec = await get(makeId(q.assessmentInstanceId, q.itemId));
    if (rec) {
      if (rec.patientId !== String(q.patientId) || rec.assessmentInstanceId !== String(q.assessmentInstanceId)) return null;
      return rec;
    }

    const blob = await baixarDaNuvem(q.patientId, q.assessmentInstanceId, q.itemId);
    if (!blob) return null;

    const now = Date.now();
    const baixado = {
      id: makeId(q.assessmentInstanceId, q.itemId),
      patientId: String(q.patientId),
      assessmentInstanceId: String(q.assessmentInstanceId),
      instrument: q.instrument || "",
      itemId: String(q.itemId),
      blob: blob,
      mimeType: blob.type || "audio/webm",
      createdAt: now,
      expiresAt: now + TTL_MS,
    };
    // Cache indisponivel (modo anonimo, cota) nao pode impedir a escuta.
    try { const os = await store("readwrite"); await done(os.put(baixado)); } catch (e) { /* segue com o blob em maos */ }
    return baixado;
  }

  // listByAssessment SEMPRE exige os dois filtros (patientId + assessmentInstanceId).
  async function listByAssessment(assessmentInstanceId, patientId) {
    if (!assessmentInstanceId || !patientId) throw new Error("listByAssessment exige assessmentInstanceId e patientId.");
    const os = await store("readonly");
    const recs = await done(os.index("assessmentInstanceId").getAll(String(assessmentInstanceId)));
    const now = Date.now();
    return (recs || []).filter((r) => r.patientId === String(patientId) && r.expiresAt > now);
  }

  async function del(id) { const os = await store("readwrite"); await done(os.delete(id)); }

  /* Apagar UMA gravação, a pedido da profissional. Exige os mesmos três
     campos do getForItem — e confere o dono antes de remover — para que
     um itemId repetido entre instâncias nunca apague o áudio da criança
     errada. Devolve true só quando havia algo e foi removido. */
  async function deleteForItem(q) {
    q = q || {};
    if (!q.patientId || !q.assessmentInstanceId || !q.itemId) throw new Error("deleteForItem exige patientId, assessmentInstanceId e itemId.");
    const rec = await getForItem(q);
    if (!rec) return false;
    /* Os DOIS lados. Apagar so o cache faria o audio reaparecer no
       proximo acesso, pelo recuo para a nuvem — o que, para quem clicou
       em "apagar", e o mesmo que nao ter apagado. A nuvem vem primeiro:
       se ela recusar, o cache continua e a funcao falha em vez de dizer
       que apagou. */
    /* Até 25/09/2026 o retorno era ignorado: com o Storage recusando, o
       botão dizia "Gravação apagada" e o áudio voltava no próximo acesso. */
    if (!(await apagarDaNuvem(q.patientId, q.assessmentInstanceId, q.itemId))) {
      throw new Error("Não foi possível apagar a gravação no servidor — tente novamente.");
    }
    await del(makeId(q.assessmentInstanceId, q.itemId));
    return true;
  }

  // Apaga a avaliacao inteira, nos dois lados — mesma razao do deleteForItem.
  async function deleteByAssessment(patientId, assessmentInstanceId) {
    if (!patientId || !assessmentInstanceId) return 0;
    /* A falha na nuvem era engolida: quem chamava achava que apagou e o
       audio da crianca ficava no Storage (auditoria de 25/09/2026). */
    const dono = await donoAtual();
    const itens = await listarNaNuvem(patientId, assessmentInstanceId);
    if (dono && itens.length) {
      const { error } = await sb().storage.from(BUCKET).remove(itens.map((n) => caminhoNuvem(dono, patientId, assessmentInstanceId, n)));
      if (error) throw new Error("Não foi possível apagar os áudios desta avaliação no servidor.");
    }
    const os = await store("readwrite");
    const keys = await done(os.index("assessmentInstanceId").getAllKeys(String(assessmentInstanceId)));
    let n = 0; for (const k of (keys || [])) { await done(os.delete(k)); n++; }
    return n;
  }

  // purgeExpired() -> quantidade removida
  /* Vence apenas o CACHE. A copia na nuvem nao tem prazo: o audio e
     parte do prontuario, e some so quando a profissional mandar apagar.
     Um item purgado aqui volta sozinho no proximo acesso, baixado da
     nuvem pelo getForItem. */
  async function purgeExpired() {
    try {
      const os = await store("readwrite");
      const range = IDBKeyRange.upperBound(Date.now());
      const keys = await done(os.index("expiresAt").getAllKeys(range));
      let n = 0; for (const k of (keys || [])) { await done(os.delete(k)); n++; }
      console.log("[MediaStore] purgeExpired: " + n + " copia(s) local(is) vencida(s) removida(s); a nuvem nao e afetada.");
      return n;
    } catch (e) {
      console.warn("[MediaStore] purgeExpired falhou:", e && e.message);
      return 0;
    }
  }

  /* "Apagar as avaliações de X" (DNOI, GAN, Fluência): antes apagava só os
     rascunhos, e o áudio da criança ficava no Storage para sempre. */
  async function apagarAudiosDe(patientId, assessmentIds) {
    for (const id of assessmentIds || []) await deleteByAssessment(patientId, id);
  }

  window.MediaStorageService = { save, get, getForItem, listByAssessment, listarNaNuvem, delete: del, deleteForItem, deleteByAssessment, apagarAudiosDe, purgeExpired, TTL_MS };

  /* ============================================================
     PARTE 2 — Purga do legado (execucao UNICA, por flag de versao).
     Remove APENAS o campo `audio` (base64) das instancias mistas em
     localStorage, PRESERVANDO escores/respostas. Se falhar no meio, a
     flag nao e gravada e roda de novo no proximo boot. Sem modal.
     ============================================================ */
  (function purgaLegado() {
    const FLAG = "educare.storageMigration.v1";
    try { if (localStorage.getItem(FLAG)) return; } catch (e) { return; }

    const bytesDe = (s) => (typeof s === "string" ? s.length : 0);
    let liberados = 0, chavesAjustadas = 0;

    const tiraAudioDeRecontos = (rec) => {
      let mudou = false;
      Object.keys(rec || {}).forEach((k) => {
        const r = rec[k];
        if (r && typeof r === "object" && r.audio) { liberados += bytesDe(r.audio); rec[k] = { ...r, audio: "" }; mudou = true; }
      });
      return mudou;
    };

    // Instancias mistas (audio + escores no MESMO objeto): tira so o audio.
    const alvos = {
      "educare-gan-avaliacoes": (all) => {
        let mudou = false;
        Object.keys(all).forEach((id) => { const a = all[id]; if (a && a.dados) ["a", "b"].forEach((k) => { if (a.dados[k] && a.dados[k].audio) { liberados += bytesDe(a.dados[k].audio); a.dados[k] = { ...a.dados[k], audio: "" }; mudou = true; } }); });
        return mudou;
      },
      "educare-fluencia-avaliacoes": (all) => {
        let mudou = false;
        Object.keys(all).forEach((id) => { const a = all[id]; if (a && a.dados) ["palavras", "texto"].forEach((k) => { if (a.dados[k] && a.dados[k].audio) { liberados += bytesDe(a.dados[k].audio); a.dados[k] = { ...a.dados[k], audio: "" }; mudou = true; } }); });
        return mudou;
      },
      "educare-dnoi-avaliacoes": (all) => {
        let mudou = false;
        Object.keys(all).forEach((id) => { const a = all[id]; if (a && a.recontos && tiraAudioDeRecontos(a.recontos)) mudou = true; });
        return mudou;
      },
    };

    try {
      Object.keys(alvos).forEach((key) => {
        const raw = localStorage.getItem(key); if (!raw) return;
        const all = JSON.parse(raw);
        if (alvos[key](all)) { localStorage.setItem(key, JSON.stringify(all)); chavesAjustadas++; }
      });

      // Rascunhos legados do DNOI por paciente (educare-dnoi-reconto-<pid>): tira so o audio.
      const keys = []; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k) keys.push(k); }
      keys.forEach((k) => {
        if (!/^educare-dnoi-reconto-/.test(k)) return;
        const raw = localStorage.getItem(k); if (!raw) return;
        try {
          const o = JSON.parse(raw);
          const alvo = (o && o.data) ? o.data : o;
          if (tiraAudioDeRecontos(alvo)) { localStorage.setItem(k, JSON.stringify(o)); chavesAjustadas++; }
        } catch (e) { /* ignora corrompido */ }
      });

      console.log("[Educare] Purga de audio legado: " + chavesAjustadas + " chave(s) ajustada(s), ~" + Math.round(liberados / 1024) + " KB (base64) liberados. Escores preservados.");
      localStorage.setItem(FLAG, String(Date.now()));
    } catch (e) {
      // Nao grava a flag: roda de novo no proximo boot.
      console.error("[Educare] Purga de audio legado FALHOU (flag nao gravada, tentara de novo):", e && e.message);
    }
  })();

  /* Purga de expirados: no boot + a cada 30 min enquanto o app esta aberto. */
  purgeExpired();
  setInterval(function () { purgeExpired(); }, 30 * 60 * 1000);
})();
