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

export type EstadoPalpite = 'COMPLETO' | 'PARCIAL' | 'NENHUM';

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

  /** Participantes do bolão. Quem saiu aparece marcado, não desaparece. */
  async listar(poolId: string, userId: string, limit = 50, offset = 0) {
    await this.exigirParticipante(poolId, userId);
    ({ limit, offset } = paginacao(limit, offset));

    const [total, rows] = await Promise.all([
      this.prisma.poolMember.count({ where: { poolId } }),
      this.prisma.poolMember.findMany({
        where: { poolId },
        orderBy: [{ role: 'asc' }, { joinedAt: 'asc' }],
        take: limit,
        skip: offset,
        select: {
          userId: true,
          role: true,
          joinedAt: true,
          leftAt: true,
          user: { select: { displayName: true } },
        },
      }),
    ]);

    const criador = await this.criadorDo(poolId);

    return {
      total,
      rows: rows.map((m) => ({
        userId: m.userId,
        displayName: m.user.displayName,
        role: m.role,
        isCreator: m.userId === criador,
        joinedAt: m.joinedAt,
        leftAt: m.leftAt,
        active: m.leftAt === null,
      })),
    };
  }

  /**
   * Quem ainda não palpitou na rodada — **sem os placares**.
   *
   * É a única visão em que o admin recebe dado agregado sobre palpite alheio, e
   * por isso a resposta carrega apenas o estado (PP-9). Placar aqui furaria o
   * sigilo de PP-7.
   */
  async statusDaRodada(poolId: string, matchday: number, userId: string) {
    await this.exigirAdmin(poolId, userId);

    const totalJogos = await this.prisma.poolMatch.count({
      where: { poolId, match: { matchday } },
    });

    const membros = await this.prisma.poolMember.findMany({
      where: { poolId, leftAt: null },
      select: { userId: true, user: { select: { displayName: true } } },
    });

    const contagens = await this.prisma.prediction.groupBy({
      by: ['userId'],
      where: { poolId, match: { matchday } },
      _count: { _all: true },
    });
    const porUsuario = new Map(contagens.map((c) => [c.userId, c._count._all]));

    return {
      matchday,
      totalMatches: totalJogos,
      rows: membros.map((m) => {
        const feitos = porUsuario.get(m.userId) ?? 0;
        let estado: EstadoPalpite;
        if (feitos === 0) estado = 'NENHUM';
        else if (feitos < totalJogos) estado = 'PARCIAL';
        else estado = 'COMPLETO';
        return {
          userId: m.userId,
          displayName: m.user.displayName,
          predictionsMade: feitos,
          estado,
        };
      }),
    };
  }

  /** Promove participante a admin. Mais de um admin é permitido e desejado. */
  async promover(poolId: string, alvoId: string, userId: string) {
    await this.exigirAdmin(poolId, userId);
    const alvo = await this.membroAtivo(poolId, alvoId);
    if (alvo.role === 'ADMIN') return { changed: false };

    await this.prisma.poolMember.update({
      where: { poolId_userId: { poolId, userId: alvoId } },
      data: { role: 'ADMIN' },
    });
    return { changed: true };
  }

  /** Rebaixa admin a participante comum. */
  async rebaixar(poolId: string, alvoId: string, userId: string) {
    await this.exigirAdmin(poolId, userId);
    await this.membroAtivo(poolId, alvoId);

    // Decidido em 03/08: quem cria o bolão é admin permanente e não pode ser
    // rebaixado — nem por outro admin, nem por si mesmo.
    if (alvoId === (await this.criadorDo(poolId))) {
      throw new ConflictException(
        'quem criou o bolão é administrador permanente e não pode ser rebaixado',
      );
    }

    const admins = await this.prisma.poolMember.count({
      where: { poolId, role: 'ADMIN', leftAt: null },
    });
    if (admins <= 1) {
      // Bolão sem admin é estado irrecuperável pelo app.
      throw new ConflictException('o bolão não pode ficar sem administrador');
    }

    await this.prisma.poolMember.update({
      where: { poolId_userId: { poolId, userId: alvoId } },
      data: { role: 'MEMBER' },
    });
    return { changed: true };
  }

  /**
   * Saída voluntária (CV-4). Definitiva: o vínculo é encerrado, não apagado —
   * os jogos já apurados permanecem no histórico e o ranking de quem ficou
   * **não** é recalculado.
   */
  async sair(poolId: string, userId: string) {
    const membro = await this.membroAtivo(poolId, userId);

    if (userId === (await this.criadorDo(poolId))) {
      // Consequência de "criador é admin permanente": ele nunca consegue sair.
      throw new ConflictException(
        'quem criou o bolão é administrador permanente e não pode sair dele',
      );
    }

    if (membro.role === 'ADMIN') {
      const admins = await this.prisma.poolMember.count({
        where: { poolId, role: 'ADMIN', leftAt: null },
      });
      if (admins <= 1) {
        throw new ConflictException(
          'promova outro administrador antes de sair — o bolão não pode ficar sem admin',
        );
      }
    }

    await this.prisma.poolMember.update({
      where: { poolId_userId: { poolId, userId } },
      data: { leftAt: new Date() },
    });
    return { left: true };
  }

  /**
   * Remoção pelo admin (CC-9). Usa a **mesma** regra da saída voluntária:
   * decidido em 03/08 que os palpites do removido ficam no histórico, para o
   * admin não poder apagar histórico alheio.
   */
  async remover(poolId: string, alvoId: string, userId: string) {
    await this.exigirAdmin(poolId, userId);
    await this.membroAtivo(poolId, alvoId);

    if (alvoId === (await this.criadorDo(poolId))) {
      throw new ConflictException('quem criou o bolão não pode ser removido');
    }
    if (alvoId === userId) {
      throw new BadRequestException('para sair do bolão use a ação de sair');
    }

    await this.prisma.poolMember.update({
      where: { poolId_userId: { poolId, userId: alvoId } },
      data: { leftAt: new Date() },
    });
    return { removed: true };
  }

  private async criadorDo(poolId: string): Promise<string> {
    const pool = await this.prisma.pool.findUnique({
      where: { id: poolId },
      select: { createdById: true },
    });
    if (!pool) throw new NotFoundException('bolão não encontrado');
    return pool.createdById;
  }

  private async membroAtivo(poolId: string, alvoId: string) {
    const membro = await this.prisma.poolMember.findUnique({
      where: { poolId_userId: { poolId, userId: alvoId } },
      select: { role: true, leftAt: true },
    });
    if (!membro)
      throw new NotFoundException('participante não encontrado neste bolão');
    if (membro.leftAt)
      throw new ConflictException('este participante já saiu do bolão');
    return membro;
  }

  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');
    }
  }

  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',
      );
    }
  }
}
