import {
  ConflictException,
  ForbiddenException,
  GoneException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { createHash, randomBytes } from 'node:crypto';
import { PrismaService } from '../prisma/prisma.service';
import { ConviteResponse, EntrarResponse } from './dto/convite.dto';

@Injectable()
export class ConvitesService {
  /** Decidido em 03/08: o convite vale 1 dia. */
  private readonly validadeHoras = 24;

  constructor(private readonly prisma: PrismaService) {}

  /**
   * Gera o convite do bolão, **revogando o anterior**. É o que dá sentido a
   * "invalidar e gerar outro": o link antigo para de funcionar na hora.
   */
  async gerar(poolId: string, userId: string): Promise<ConviteResponse> {
    await this.exigirAdmin(poolId, userId);
    await this.exigirBolaoAberto(poolId);
    // Bolão que já começou não aceita mais ninguém. Gerar o convite aqui
    // entregava ao admin um código que **nunca** funcionaria: ele mandava no
    // grupo e o convidado é que descobria o erro. Mesma regra de `entrar`.
    await this.exigirEntradaAberta(poolId);

    const token = randomBytes(32).toString('base64url');
    const expiresAt = new Date(
      Date.now() + this.validadeHoras * 60 * 60 * 1000,
    );

    await this.prisma.$transaction([
      this.prisma.invite.updateMany({
        where: { poolId, revokedAt: null },
        data: { revokedAt: new Date() },
      }),
      this.prisma.invite.create({
        data: { poolId, tokenHash: this.hash(token), expiresAt },
      }),
    ]);

    return { token, expiresAt };
  }

  /** Invalida o convite atual sem gerar outro. */
  async invalidar(poolId: string, userId: string): Promise<void> {
    await this.exigirAdmin(poolId, userId);
    await this.prisma.invite.updateMany({
      where: { poolId, revokedAt: null },
      data: { revokedAt: new Date() },
    });
  }

  /**
   * Entrada pelo convite. Cada recusa tem motivo distinguível, porque o app
   * precisa dizer **por que** não deu — "erro" genérico faz o convidado
   * desistir e voltar ao WhatsApp.
   */
  async entrar(token: string, userId: string): Promise<EntrarResponse> {
    const convite = await this.prisma.invite.findUnique({
      where: { tokenHash: this.hash(token) },
      select: {
        poolId: true,
        expiresAt: true,
        revokedAt: true,
        pool: { select: { name: true, closedAt: true } },
      },
    });

    if (!convite || convite.revokedAt) {
      throw new NotFoundException('convite inválido ou já substituído');
    }
    if (convite.expiresAt < new Date()) {
      throw new GoneException(
        'este convite expirou. Peça um novo ao administrador',
      );
    }
    if (convite.pool.closedAt) {
      throw new GoneException('este bolão já foi encerrado');
    }

    // Não é possível entrar atrasado: a entrada fecha no início do 1º jogo.
    const primeiroJogo = await this.prisma.poolMatch.findFirst({
      where: { poolId: convite.poolId },
      orderBy: { match: { kickoffAt: 'asc' } },
      select: { match: { select: { kickoffAt: true } } },
    });
    if (primeiroJogo && primeiroJogo.match.kickoffAt <= new Date()) {
      throw new ConflictException(
        'este bolão já começou e não aceita mais participantes',
      );
    }

    const existente = await this.prisma.poolMember.findUnique({
      where: { poolId_userId: { poolId: convite.poolId, userId } },
      select: { leftAt: true },
    });

    if (existente?.leftAt) {
      // Sair é definitivo (CV-4). Reentrada não existe.
      throw new ConflictException(
        'você saiu deste bolão e a saída é definitiva — não é possível voltar',
      );
    }

    // Idempotente: abrir o link de novo não duplica participação nem erra.
    if (!existente) {
      await this.prisma.poolMember.create({
        data: { poolId: convite.poolId, userId, role: 'MEMBER' },
      });
    }

    return {
      poolId: convite.poolId,
      poolName: convite.pool.name,
      jaParticipava: !!existente,
    };
  }

  private hash(token: string): string {
    return createHash('sha256').update(token).digest('hex');
  }

  private async exigirBolaoAberto(poolId: string): Promise<void> {
    const pool = await this.prisma.pool.findUnique({
      where: { id: poolId },
      select: { closedAt: true },
    });
    if (!pool) throw new NotFoundException('bolão não encontrado');
    if (pool.closedAt) throw new GoneException('este bolão já foi encerrado');
  }

  /** A entrada fecha no início do primeiro jogo do bolão. */
  private async exigirEntradaAberta(poolId: string): Promise<void> {
    const primeiroJogo = await this.prisma.poolMatch.findFirst({
      where: { poolId },
      orderBy: { match: { kickoffAt: 'asc' } },
      select: { match: { select: { kickoffAt: true } } },
    });
    if (primeiroJogo && primeiroJogo.match.kickoffAt <= new Date()) {
      throw new ConflictException(
        'este bolão já começou e não aceita mais participantes',
      );
    }
  }

  private async exigirAdmin(poolId: string, userId: string): Promise<void> {
    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 fazer isso',
      );
    }
  }
}
