import { getFirestore } from 'firebase-admin/firestore';
import { logger } from 'firebase-functions';
import { onDocumentWritten } from 'firebase-functions/v2/firestore';
import { recalcularClassificacao } from './apuracao';

/**
 * Mantém o índice `usuarios/{uid}/boloes/{poolId}` e a contagem de membros
 * ativos do bolão.
 *
 * **Por que gatilho e não escrita direta na callable:** as callables de entrada,
 * saída, papel e ciclo de vida são cinco portas diferentes para o mesmo estado.
 * Se cada uma tivesse de lembrar de atualizar o índice, a primeira que
 * esquecesse sumiria com o bolão da tela inicial de alguém — sem erro nenhum, e
 * só reclamando é que se descobre. Aqui é **uma** porta: mudou o vínculo,
 * reescreve o índice.
 *
 * O índice existe porque consulta de grupo em `membros` não é autorizável: o
 * `list` não vincula o `{uid}` do caminho e a rule não enxerga o `where`
 * (`MG-2-26`). Ele também é o que faz a tela inicial custar **uma** consulta em
 * vez de uma leitura por bolão.
 */
export const manterIndiceDoUsuario = onDocumentWritten(
  'boloes/{poolId}/membros/{uid}',
  async (evento) => {
    const { poolId, uid } = evento.params;
    const db = getFirestore();
    const depois = evento.data?.after.data();

    const indice = db.doc(`usuarios/${uid}/boloes/${poolId}`);
    const bolao = await db.doc(`boloes/${poolId}`).get();

    // Bolão apagado (só acontece em teste — bolão de verdade encerra ou
    // arquiva) leva o índice junto, senão a tela inicial aponta para o nada.
    if (!bolao.exists) {
      await indice.delete();
      return;
    }

    const b = bolao.data()!;
    const ativo = depois !== undefined && depois.leftAt == null;

    if (ativo) {
      await indice.set({
        poolId,
        name: b.name,
        matchdayFrom: b.matchdayFrom,
        matchdayTo: b.matchdayTo,
        activeMembers: b.activeMembers ?? 0,
        isAdmin: depois.role === 'ADMIN',
        closed: b.closedAt != null,
        archived: b.archivedAt != null,
        joinedAt: depois.joinedAt ?? new Date(),
      });
    } else {
      // Quem saiu perde o bolão da lista, mas **não** perde o histórico: o
      // vínculo continua em `membros` com `leftAt`, e os palpites continuam no
      // bolão (`CV-4`).
      await indice.delete();
    }

    await recontarMembros(poolId);

    // **A classificação também é do vínculo, não só do placar.**
    //
    // Divergência de migração encontrada em `CV-4-01` (08/08): no NestJS o
    // ranking era somado na leitura a partir de quem estava ativo, então sair do
    // bolão tirava a pessoa da lista na hora. Aqui ele é agregado materializado,
    // e só era reescrito na apuração — quem saía **continuava no ranking** até o
    // próximo jogo terminar, que pode ser semana que vem ou nunca.
    //
    // Recalcular na mudança de vínculo custa uma varredura do bolão em evento
    // raro (entrar, sair, ser removido). O erro que ela evita é o participante
    // ver no ranking alguém que saiu — e desconfiar do resto.
    const mudouOVinculo =
      (evento.data?.before.data()?.leftAt ?? null) !==
      (evento.data?.after.data()?.leftAt ?? null);
    if (mudouOVinculo || evento.data?.before.exists === false) {
      await recalcularClassificacao(poolId);
    }
  },
);

/**
 * Contagem de ativos no documento do bolão.
 *
 * Recontada, não incrementada: `FieldValue.increment` erra em silêncio quando o
 * gatilho roda duas vezes — e o Firestore garante *at-least-once*. O agregado
 * `count()` cobra por resultado, não por documento lido, então recontar custa
 * quase o mesmo e não acumula erro.
 */
async function recontarMembros(poolId: string): Promise<void> {
  const db = getFirestore();
  const ativos = await db
    .collection(`boloes/${poolId}/membros`)
    .where('leftAt', '==', null)
    .count()
    .get();
  const total = ativos.data().count;

  const bolao = db.doc(`boloes/${poolId}`);
  const atual = (await bolao.get()).data()?.activeMembers;
  // Só escreve se mudou: escrita igual acordaria o gatilho do bolão à toa, e
  // ele reescreve o índice de **todo** participante.
  if (atual !== total) await bolao.update({ activeMembers: total });
}

/**
 * Propaga para o índice o que muda no bolão: nome, encerramento, arquivamento e
 * número de participantes.
 *
 * Sem isto, o bolão renomeado ou encerrado continuaria aparecendo do jeito
 * antigo na tela inicial de todo mundo até a pessoa abrir o bolão — e
 * "encerrado" é justamente o que muda o que a tela oferece.
 */
export const propagarBolaoParaOIndice = onDocumentWritten(
  'boloes/{poolId}',
  async (evento) => {
    const poolId = evento.params.poolId;
    const antes = evento.data?.before.data();
    const depois = evento.data?.after.data();
    if (!depois) return;

    const mudou =
      antes === undefined ||
      antes.name !== depois.name ||
      antes.activeMembers !== depois.activeMembers ||
      (antes.closedAt == null) !== (depois.closedAt == null) ||
      (antes.archivedAt == null) !== (depois.archivedAt == null);
    if (!mudou) return;

    const db = getFirestore();
    const membros = await db
      .collection(`boloes/${poolId}/membros`)
      .where('leftAt', '==', null)
      .get();

    const lote = db.bulkWriter();
    for (const m of membros.docs) {
      lote.set(
        db.doc(`usuarios/${m.id}/boloes/${poolId}`),
        {
          name: depois.name,
          activeMembers: depois.activeMembers ?? membros.size,
          closed: depois.closedAt != null,
          archived: depois.archivedAt != null,
        },
        { merge: true },
      );
    }
    await lote.close();

    logger.info(`índice do bolão ${poolId} propagado para ${membros.size}`);
  },
);
