import { ForbiddenException, Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';

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

export interface LinhaExtrato {
  matchId: string;
  homeTeam: string;
  awayTeam: string;
  kickoffAt: Date;
  estado: EstadoJogo;
  myPrediction: { homeScore: number; awayScore: number } | null;
  officialScore: { homeScore: number; awayScore: number } | null;
  points: number | null;
  /** Motivo do ponto — é isto que evita "confia" como resposta. */
  band: string | null;
  /**
   * Última alteração do placar deste jogo, quando houve. Ponto que cai sem
   * explicação é indistinguível de bug — ou de fraude, quando é manual (PR-6).
   */
  correction: {
    previousHome: number | null;
    previousAway: number | null;
    manual: boolean;
    at: Date;
  } | null;
}

@Injectable()
export class ExtratoService {
  constructor(private readonly prisma: PrismaService) {}

  /**
   * Extrato do participante na rodada: o que palpitou, o placar oficial, quanto
   * valeu e **por quê**.
   *
   * Jogos sem palpite entram na lista com 0 e palpite nulo — omitir faria a
   * soma do extrato não bater com o ranking, que é exatamente a desconfiança
   * que esta story existe para eliminar.
   */
  async doParticipante(
    poolId: string,
    matchday: number,
    userId: string,
  ): Promise<{ rows: LinhaExtrato[]; total: number }> {
    await this.exigirParticipante(poolId, userId);

    const vinculos = await this.prisma.poolMatch.findMany({
      where: { poolId, match: { matchday } },
      orderBy: { match: { kickoffAt: 'asc' } },
      select: {
        match: {
          select: {
            id: true,
            homeTeam: true,
            awayTeam: true,
            kickoffAt: true,
            status: true,
            homeScore: true,
            awayScore: true,
          },
        },
      },
    });

    const palpites = await this.prisma.prediction.findMany({
      where: { poolId, userId, match: { matchday } },
      select: {
        matchId: true,
        homeScore: true,
        awayScore: true,
        points: true,
        band: true,
      },
    });
    const porJogo = new Map(palpites.map((p) => [p.matchId, p]));

    // Última correção por jogo: o extrato precisa dizer que o placar mudou e
    // qual era antes, senão ponto que cai vira acusação de fraude (PR-6).
    const correcoes = await this.prisma.scoreCorrection.findMany({
      where: { matchId: { in: vinculos.map((v) => v.match.id) } },
      orderBy: { createdAt: 'desc' },
      select: {
        matchId: true,
        previousHome: true,
        previousAway: true,
        source: true,
        createdAt: true,
      },
    });
    const ultimaCorrecao = new Map<string, (typeof correcoes)[number]>();
    for (const c of correcoes) {
      // `orderBy desc` + primeiro a entrar = a mais recente de cada jogo.
      if (!ultimaCorrecao.has(c.matchId)) ultimaCorrecao.set(c.matchId, c);
    }

    const agora = new Date();
    const rows: LinhaExtrato[] = vinculos.map(({ match }) => {
      const meu = porJogo.get(match.id);
      const apurado = meu?.points !== null && meu?.points !== undefined;
      const terminou = match.status === 'FINISHED' && match.homeScore !== null;

      // Jogo que passou do horário sem ponto apurado fica "aguardando
      // resultado": o tier gratuito atrasa o placar, e isso não pode parecer
      // zero nem tela travada (PR-1 FE-2).
      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 do
      // participante que ele simplesmente esqueceu de palpitar, que é
      // exatamente o que ele precisa saber para não repetir.
      else if (terminou && !meu) estado = 'SEM_PALPITE';
      else if (match.kickoffAt > agora) estado = 'PRAZO_ABERTO';
      else estado = 'AGUARDANDO_RESULTADO';

      return {
        matchId: match.id,
        homeTeam: match.homeTeam,
        awayTeam: match.awayTeam,
        kickoffAt: match.kickoffAt,
        estado,
        myPrediction: meu
          ? { homeScore: meu.homeScore, awayScore: meu.awayScore }
          : null,
        officialScore:
          match.homeScore !== null && match.awayScore !== null
            ? { homeScore: match.homeScore, awayScore: match.awayScore }
            : null,
        // Sem palpite em jogo apurado vale 0, não "sem informação".
        points: apurado ? (meu?.points ?? 0) : terminou && !meu ? 0 : null,
        band: meu?.band ?? null,
        correction: ultimaCorrecao.has(match.id)
          ? {
              previousHome: ultimaCorrecao.get(match.id)!.previousHome,
              previousAway: ultimaCorrecao.get(match.id)!.previousAway,
              manual: ultimaCorrecao.get(match.id)!.source === 'MANUAL',
              at: ultimaCorrecao.get(match.id)!.createdAt,
            }
          : null,
      };
    });

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

  private async exigirParticipante(
    poolId: string,
    userId: string,
  ): Promise<void> {
    const membro = await this.prisma.poolMember.findUnique({
      where: { poolId_userId: { poolId, userId } },
      select: { leftAt: true },
    });
    if (!membro || membro.leftAt) {
      throw new ForbiddenException('você não participa deste bolão');
    }
  }
}
