import {
  BadRequestException,
  ForbiddenException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { RankingService } from '../ranking/ranking.service';
import { ScoringService } from './scoring.service';

interface PosicaoAntes {
  userId: string;
  poolId: string;
  points: number;
  position: number;
}

/**
 * Aviso de correção **antes** de virar linha no banco.
 *
 * Existe para o chamador poder juntar vários numa mensagem só: a sincronização
 * corrige a rodada inteira de uma vez, e um aviso por jogo transforma o domingo
 * à noite em spam — que é exatamente o que faz o participante desligar as
 * notificações e parar de ver as que importam (critério de PR-7).
 */
export interface AvisoDeCorrecao {
  userId: string;
  poolId: string;
  kind: string;
  jogo: string;
  placarDe: string;
  placarPara: string;
  posicaoDe: number | null;
  posicaoPara: number | null;
}

@Injectable()
export class CorrecaoService {
  private readonly log = new Logger(CorrecaoService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly scoring: ScoringService,
    private readonly ranking: RankingService,
  ) {}

  /**
   * Aplica um placar novo a um jogo já apurado e recalcula tudo.
   *
   * Vale para os dois caminhos: correção da fonte oficial e correção manual do
   * admin. O que muda é a origem registrada na auditoria.
   */
  async aplicar(
    matchId: string,
    novoCasa: number,
    novoFora: number,
    origem: 'OFFICIAL' | 'MANUAL',
    correctedById?: string,
    /**
     * Quando presente, os avisos são **acumulados** em vez de gravados — quem
     * chamou grava depois, consolidados. É como a sincronização evita mandar um
     * aviso por jogo corrigido.
     */
    coletor?: AvisoDeCorrecao[],
  ): Promise<{ afetados: number; avisos: number }> {
    const jogo = await this.prisma.match.findUnique({
      where: { id: matchId },
      select: {
        homeScore: true,
        awayScore: true,
        kickoffAt: true,
        status: true,
      },
    });
    if (!jogo) throw new NotFoundException('jogo não encontrado');

    if (jogo.homeScore === novoCasa && jogo.awayScore === novoFora) {
      // Nada mudou: não recalcula e não avisa ninguém (critério de PR-6).
      return { afetados: 0, avisos: 0 };
    }

    // Snapshot de posições antes, para saber quem foi afetado de fato.
    const antes = await this.posicoesAtuais(matchId);

    await this.prisma.$transaction([
      this.prisma.scoreCorrection.create({
        data: {
          matchId,
          previousHome: jogo.homeScore,
          previousAway: jogo.awayScore,
          newHome: novoCasa,
          newAway: novoFora,
          source: origem,
          correctedById: origem === 'MANUAL' ? correctedById : null,
        },
      }),
      this.prisma.match.update({
        where: { id: matchId },
        data: {
          homeScore: novoCasa,
          awayScore: novoFora,
          status: 'FINISHED',
        },
      }),
      // Zera a apuração daquele jogo para forçar recálculo.
      this.prisma.prediction.updateMany({
        where: { matchId },
        data: { points: null, band: null, scoredAt: null },
      }),
    ]);

    const afetados = await this.scoring.apurarJogo(matchId);
    const depois = await this.posicoesAtuais(matchId);

    const avisos = await this.avisarAfetados(
      matchId,
      antes,
      depois,
      jogo.homeScore,
      jogo.awayScore,
      novoCasa,
      novoFora,
      origem,
      coletor,
    );

    this.log.log(
      `correção ${origem} no jogo ${matchId}: ${afetados} palpite(s) reapurado(s), ${avisos} aviso(s)`,
    );
    return { afetados, avisos };
  }

  /** Correção manual pelo admin (PR-10). */
  async corrigirManualmente(
    poolId: string,
    matchId: string,
    userId: string,
    novoCasa: number,
    novoFora: number,
  ) {
    const membro = await this.prisma.poolMember.findUnique({
      where: { poolId_userId: { poolId, userId } },
      select: { role: true, leftAt: true },
    });
    if (!membro || membro.leftAt || membro.role !== 'ADMIN') {
      throw new ForbiddenException(
        'apenas o administrador do bolão pode corrigir placar',
      );
    }

    const vinculo = await this.prisma.poolMatch.findUnique({
      where: { poolId_matchId: { poolId, matchId } },
      select: { match: { select: { kickoffAt: true, status: true } } },
    });
    if (!vinculo)
      throw new NotFoundException('este jogo não pertence ao bolão');

    // Permitido depois do prazo mesmo sem `FINISHED`: é assim que um W.O. — que
    // nunca é reagendado — deixa de manter o bolão pendente.
    const prazoEncerrado = vinculo.match.kickoffAt <= new Date();
    if (!prazoEncerrado && vinculo.match.status !== 'FINISHED') {
      throw new BadRequestException(
        'não é possível informar placar de jogo que ainda não começou',
      );
    }

    return this.aplicar(matchId, novoCasa, novoFora, 'MANUAL', userId);
  }

  /** Posição e pontos de cada participante, nos bolões que contêm o jogo. */
  private async posicoesAtuais(matchId: string): Promise<PosicaoAntes[]> {
    const pools = await this.prisma.poolMatch.findMany({
      where: { matchId },
      select: { poolId: true },
    });

    const saida: PosicaoAntes[] = [];
    for (const { poolId } of pools) {
      // Usa o primeiro membro só para satisfazer a checagem de participação;
      // a leitura em si é interna.
      const membro = await this.prisma.poolMember.findFirst({
        where: { poolId, leftAt: null },
        select: { userId: true },
      });
      if (!membro) continue;

      const { rows } = await this.ranking.geral(poolId, membro.userId, 1000, 0);
      for (const r of rows) {
        saida.push({
          userId: r.userId,
          poolId,
          points: r.points,
          position: r.position,
        });
      }
    }
    return saida;
  }

  /**
   * Avisa **só** quem teve ponto ou posição alterada, com um aviso consolidado
   * por participante e por bolão.
   */
  /**
   * Grava os avisos acumulados, **um por participante e bolão**.
   *
   * Vários jogos corrigidos na mesma sincronização viram uma mensagem só, que
   * lista os jogos e diz a mudança líquida de posição. O critério de PR-7 é
   * explícito: aviso consolidado, não um por jogo.
   */
  async gravarConsolidado(avisos: AvisoDeCorrecao[]): Promise<number> {
    if (avisos.length === 0) return 0;

    const porPessoa = new Map<string, AvisoDeCorrecao[]>();
    for (const a of avisos) {
      const k = `${a.poolId}:${a.userId}`;
      porPessoa.set(k, [...(porPessoa.get(k) ?? []), a]);
    }

    const linhas = [...porPessoa.values()].map((lista) =>
      CorrecaoService.montarNotificacao(lista),
    );
    await this.prisma.notification.createMany({ data: linhas });
    return linhas.length;
  }

  /** Texto do aviso, para um jogo ou para vários. */
  private static montarNotificacao(lista: AvisoDeCorrecao[]) {
    const primeiro = lista[0];
    const manual = lista.some((a) => a.kind === 'PLACAR_CORRIGIDO_MANUAL');

    // Posição líquida: de onde a pessoa estava antes da primeira correção para
    // onde ficou depois da última. Somar as mudanças intermediárias contaria
    // degraus que ninguém viu.
    const comPosicao = lista.filter((a) => a.posicaoDe !== null);
    const de = comPosicao.length > 0 ? comPosicao[0].posicaoDe : null;
    const para =
      comPosicao.length > 0
        ? comPosicao[comPosicao.length - 1].posicaoPara
        : null;
    const detalhe =
      de !== null && para !== null && de !== para
        ? ` Sua posição no ranking passou de ${de}º para ${para}º.`
        : '';

    if (lista.length === 1) {
      return {
        userId: primeiro.userId,
        poolId: primeiro.poolId,
        kind: primeiro.kind,
        title: 'Sua pontuação mudou',
        body:
          `${primeiro.jogo}: o placar mudou de ${primeiro.placarDe} ` +
          `para ${primeiro.placarPara}` +
          (manual ? ' (corrigido pelo administrador)' : '') +
          `.${detalhe}`,
      };
    }

    const jogos = lista
      .map((a) => `${a.jogo} (${a.placarDe} → ${a.placarPara})`)
      .join('; ');
    return {
      userId: primeiro.userId,
      poolId: primeiro.poolId,
      kind: primeiro.kind,
      title: `Sua pontuação mudou em ${lista.length} jogos`,
      body:
        `${lista.length} jogos tiveram o placar corrigido: ${jogos}` +
        (manual ? ' (corrigido pelo administrador)' : '') +
        `.${detalhe}`,
    };
  }

  private async avisarAfetados(
    matchId: string,
    antes: PosicaoAntes[],
    depois: PosicaoAntes[],
    anteriorCasa: number | null,
    anteriorFora: number | null,
    novoCasa: number,
    novoFora: number,
    origem: 'OFFICIAL' | 'MANUAL',
    coletor?: AvisoDeCorrecao[],
  ): Promise<number> {
    const jogo = await this.prisma.match.findUnique({
      where: { id: matchId },
      select: { homeTeam: true, awayTeam: true },
    });

    const chave = (p: PosicaoAntes) => `${p.poolId}:${p.userId}`;
    const mapaAntes = new Map(antes.map((p) => [chave(p), p]));

    const placarAnterior =
      anteriorCasa !== null && anteriorFora !== null
        ? `${anteriorCasa}x${anteriorFora}`
        : 'sem placar';

    const avisos: AvisoDeCorrecao[] = depois
      .filter((d) => {
        const a = mapaAntes.get(chave(d));
        // Ponto igual e posição igual = nada a avisar.
        return !a || a.points !== d.points || a.position !== d.position;
      })
      .map((d) => {
        const a = mapaAntes.get(chave(d));
        return {
          userId: d.userId,
          poolId: d.poolId,
          kind:
            origem === 'MANUAL'
              ? 'PLACAR_CORRIGIDO_MANUAL'
              : 'PLACAR_CORRIGIDO',
          jogo: `${jogo?.homeTeam} x ${jogo?.awayTeam}`,
          placarDe: placarAnterior,
          placarPara: `${novoCasa}x${novoFora}`,
          posicaoDe: a && a.position !== d.position ? a.position : null,
          posicaoPara: a && a.position !== d.position ? d.position : null,
        };
      });

    if (avisos.length === 0) return 0;

    if (coletor) {
      coletor.push(...avisos);
      return avisos.length;
    }

    await this.prisma.notification.createMany({
      data: avisos.map((a) => CorrecaoService.montarNotificacao([a])),
    });
    return avisos.length;
  }
}
