import { getFirestore, type Firestore } from 'firebase-admin/firestore';
import { onCall } from 'firebase-functions/v2/https';
import { bolaoOuErro, exigirAdmin, exigirMembroAtivo, uidDe } from './acesso';
import { classificar, type Agregado, type CriterioDesempate } from './regra/classificacao';

/**
 * Extrato da rodada e ranking da rodada — **portados** de `extrato.service.ts` e
 * `ranking.service.ts`.
 *
 * São as duas leituras que o `PRD` avisou que doeriam: **sem `join`**, cada uma
 * compõe em memória o que o Postgres montava numa consulta. A composição fica no
 * servidor, e não no app, por dois motivos — o app pagaria as leituras em rede a
 * cada abertura de tela, e o ranking da rodada precisa dos palpites de **todo
 * mundo**, que o cliente não pode ler enquanto o jogo não termina (`PP-7`).
 */

/** Limite do operador `in` do Firestore. */
const LOTE_IN = 30;

function emLotes<T>(itens: T[]): T[][] {
  const lotes: T[][] = [];
  for (let i = 0; i < itens.length; i += LOTE_IN) {
    lotes.push(itens.slice(i, i + LOTE_IN));
  }
  return lotes;
}

async function jogosDaRodada(
  db: Firestore,
  competitionCode: string,
  matchday: number,
) {
  const docs = await db
    .collection('jogos')
    .where('competitionCode', '==', competitionCode)
    .where('matchday', '==', matchday)
    .orderBy('kickoffAt')
    .get();
  return docs.docs;
}

export type EstadoJogo =
  | 'AGUARDANDO_RESULTADO'
  | 'APURADO'
  | 'PRAZO_ABERTO'
  | 'SEM_PALPITE';

export const extratoDaRodada = onCall(async (request) =>
  extrato(
    uidDe(request),
    (request.data?.poolId ?? '') as string,
    request.data?.matchday as number,
  ),
);

export async function extrato(
  uid: string,
  poolId: string,
  matchday: number,
  db: Firestore = getFirestore(),
) {
  const bolao = await bolaoOuErro(db, poolId);
  await exigirMembroAtivo(db, poolId, uid);

  const jogos = await jogosDaRodada(db, bolao.competitionCode, matchday);
  const ids = jogos.map((j) => j.id);
  if (ids.length === 0) return { rows: [], total: 0 };

  const meus = new Map<string, FirebaseFirestore.DocumentData>();
  for (const lote of emLotes(ids)) {
    const docs = await db
      .collection(`boloes/${poolId}/palpites`)
      .where('userId', '==', uid)
      .where('matchId', 'in', lote)
      .get();
    docs.docs.forEach((d) => meus.set(d.data().matchId as string, d.data()));
  }

  // Última correção de cada jogo. Consulta de grupo com o `matchId` no próprio
  // documento: sem esse campo seria uma consulta por jogo da rodada.
  const ultimaCorrecao = new Map<string, FirebaseFirestore.DocumentData>();
  for (const lote of emLotes(ids)) {
    const docs = await db
      .collectionGroup('correcoes')
      .where('matchId', 'in', lote)
      .orderBy('createdAt', 'desc')
      .get();
    for (const d of docs.docs) {
      const mid = d.data().matchId as string;
      if (!ultimaCorrecao.has(mid)) ultimaCorrecao.set(mid, d.data());
    }
  }

  const agora = Date.now();
  const rows = jogos.map((doc) => {
    const j = doc.data();
    const meu = meus.get(doc.id);
    const apurado = meu?.points != null;
    const terminou = j.status === 'FINISHED' && j.homeScore != null;

    // Jogo que passou do horário sem ponto apurado fica "aguardando resultado":
    // a fonte atrasa o placar, e isso não pode parecer zero nem tela travada.
    let estado: EstadoJogo;
    if (apurado) estado = 'APURADO';
    // Jogo terminado sem palpite **não** está aguardando resultado: o resultado
    // chegou, o palpite é que faltou. Dizer o contrário esconde da pessoa que
    // ela esqueceu de palpitar, que é o que ela precisa saber para não repetir.
    else if (terminou && !meu) estado = 'SEM_PALPITE';
    else if (j.kickoffAt.toMillis() > agora) estado = 'PRAZO_ABERTO';
    else estado = 'AGUARDANDO_RESULTADO';

    const correcao = ultimaCorrecao.get(doc.id);
    return {
      matchId: doc.id,
      homeTeam: j.homeTeam,
      awayTeam: j.awayTeam,
      kickoffAt: j.kickoffAt.toDate().toISOString(),
      estado,
      myPrediction: meu
        ? { homeScore: meu.homeScore, awayScore: meu.awayScore }
        : null,
      officialScore:
        j.homeScore != null && j.awayScore != null
          ? { homeScore: j.homeScore, awayScore: j.awayScore }
          : null,
      // Sem palpite em jogo apurado vale 0, não "sem informação" — senão a soma
      // do extrato não bate com o ranking, que é a desconfiança que `PR-2`
      // existe para eliminar.
      points: apurado ? (meu?.points ?? 0) : terminou && !meu ? 0 : null,
      band: meu?.band ?? null,
      correction: correcao
        ? {
            previousHome: correcao.previousHome ?? null,
            previousAway: correcao.previousAway ?? null,
            manual: correcao.source === 'MANUAL',
            at: correcao.createdAt.toDate().toISOString(),
          }
        : null,
    };
  });

  return {
    rows,
    total: rows.reduce((soma, r) => soma + (r.points ?? 0), 0),
  };
}

export const rankingDaRodada = onCall(async (request) =>
  rodada(
    uidDe(request),
    (request.data?.poolId ?? '') as string,
    request.data?.matchday as number,
  ),
);

export async function rodada(
  uid: string,
  poolId: string,
  matchday: number,
  db: Firestore = getFirestore(),
) {
  const bolao = await bolaoOuErro(db, poolId);
  await exigirMembroAtivo(db, poolId, uid);

  const jogos = await jogosDaRodada(db, bolao.competitionCode, matchday);
  const ids = jogos.map((j) => j.id);
  const apurados = jogos.filter(
    (d) => d.data().status === 'FINISHED' && d.data().homeScore != null,
  ).length;

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

  const soma = new Map<string, Agregado>();
  for (const m of membros.docs) {
    soma.set(m.id, {
      userId: m.id,
      displayName: m.data().displayName as string,
      points: 0,
      exactScores: 0,
      winners: 0,
    });
  }

  for (const lote of emLotes(ids)) {
    if (lote.length === 0) continue;
    const palpites = await db
      .collection(`boloes/${poolId}/palpites`)
      .where('matchId', 'in', lote)
      .get();
    for (const p of palpites.docs) {
      const dados = p.data();
      const agregado = soma.get(dados.userId as string);
      // Quem saiu não entra no ranking ativo, mas o palpite dele permanece.
      if (!agregado || dados.points == null) continue;
      agregado.points += dados.points as number;
      if (dados.band === 'PLACAR_EXATO') agregado.exactScores++;
      if (dados.band && dados.band !== 'ERROU') agregado.winners++;
    }
  }

  const linhas = classificar(
    [...soma.values()],
    bolao.tiebreakOrder as CriterioDesempate[],
  );

  return {
    matchday,
    totalMatches: ids.length,
    scoredMatches: apurados,
    // Rodada parcialmente apurada precisa ficar dita na tela: ranking parcial
    // parecendo final gera comemoração e briga na sequência (`PR-4`).
    partial: apurados < ids.length,
    total: linhas.length,
    rows: linhas,
  };
}

/**
 * Quem ainda não palpitou na rodada (`PP-9`) — **sem os placares**.
 *
 * É a única visão em que o admin recebe dado sobre palpite alheio, e por isso ela
 * devolve apenas o **estado**. Placar aqui furaria o sigilo de `PP-7` por uma
 * porta lateral, e o admin joga no próprio bolão.
 *
 * Tem de ser callable pela mesma razão: o cliente não consegue (nem deve) ler os
 * palpites dos outros para contar.
 */
export const statusDaRodada = onCall(async (request) =>
  status(
    uidDe(request),
    (request.data?.poolId ?? '') as string,
    request.data?.matchday as number,
  ),
);

export async function status(
  uid: string,
  poolId: string,
  matchday: number,
  db: Firestore = getFirestore(),
) {
  const bolao = await bolaoOuErro(db, poolId);
  await exigirAdmin(db, poolId, uid);

  const jogos = await jogosDaRodada(db, bolao.competitionCode, matchday);
  const ids = jogos.map((j) => j.id);

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

  const feitos = new Map<string, number>();
  for (const lote of emLotes(ids)) {
    if (lote.length === 0) continue;
    const palpites = await db
      .collection(`boloes/${poolId}/palpites`)
      .where('matchId', 'in', lote)
      .select('userId')
      .get();
    for (const p of palpites.docs) {
      const dono = p.data().userId as string;
      feitos.set(dono, (feitos.get(dono) ?? 0) + 1);
    }
  }

  return {
    matchday,
    totalMatches: ids.length,
    rows: membros.docs.map((m) => {
      const quantos = feitos.get(m.id) ?? 0;
      return {
        userId: m.id,
        displayName: m.data().displayName,
        predictionsMade: quantos,
        estado:
          quantos === 0
            ? 'NENHUM'
            : quantos < ids.length
              ? 'PARCIAL'
              : 'COMPLETO',
      };
    }),
  };
}
