import {
  BadRequestException,
  Inject,
  Injectable,
  Logger,
  ServiceUnavailableException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import {
  FONTE_DADOS,
  FonteIndisponivelError,
} from '../fonte-dados/fonte-dados.port';
// `import type`: interfaces em construtor decorado não podem ser emitidas como
// valor pelo emitDecoratorMetadata.
import type {
  FonteDeDados,
  JogoExterno,
} from '../fonte-dados/fonte-dados.port';
import { CriarPoolDto, PoolResponse } from './dto/criar-pool.dto';

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

  constructor(
    private readonly prisma: PrismaService,
    @Inject(FONTE_DADOS) private readonly fonte: FonteDeDados,
  ) {}

  async criar(dto: CriarPoolDto, criadorId: string): Promise<PoolResponse> {
    if (dto.matchdayTo < dto.matchdayFrom) {
      throw new BadRequestException(
        'a rodada final não pode ser antes da inicial',
      );
    }

    let jogos: JogoExterno[];
    try {
      jogos = await this.fonte.listarJogos(
        dto.competitionCode,
        dto.matchdayFrom,
        dto.matchdayTo,
      );
    } catch (error) {
      // BE-5: sem jogos não se cria bolão pela metade. Bolão sem jogo é o pior
      // estado possível — o admin acha que criou e não tem o que palpitar.
      if (error instanceof FonteIndisponivelError) {
        this.log.error(`criação abortada: ${error.message}`);
        throw new ServiceUnavailableException(
          'não foi possível carregar os jogos do campeonato agora. Tente de novo em alguns minutos',
        );
      }
      throw error;
    }

    if (jogos.length === 0) {
      throw new BadRequestException(
        'não há jogos nessas rodadas para este campeonato',
      );
    }

    // Quantos jogos ainda dão para palpitar. Bolão inteiro em rodadas já
    // começadas é criado com sucesso e é **inútil**: ninguém palpita, e o
    // convite é recusado porque a entrada fecha no primeiro jogo. Sem este
    // número o admin só descobre isso pelo grupo reclamando (CC-1, critério
    // "rodadas já encerradas").
    const agora = new Date();
    const comPrazoAberto = jogos.filter((j) => j.inicioEm > agora).length;

    // Transação: bolão, participação do criador e vínculo dos jogos entram
    // juntos ou não entram. É o que garante a atomicidade de BE-3.
    return this.prisma.$transaction(async (tx) => {
      await tx.match.createMany({
        data: jogos.map((j) => ({
          externalId: j.externalId,
          competitionCode: j.competitionCode,
          matchday: j.rodada,
          homeTeam: j.timeCasa,
          awayTeam: j.timeFora,
          kickoffAt: j.inicioEm,
          status: j.status,
          homeScore: j.golsCasa,
          awayScore: j.golsFora,
          sourceUpdatedAt: j.atualizadoEm,
        })),
        // Jogo pode já existir de outro bolão no mesmo campeonato.
        skipDuplicates: true,
      });

      const persistidos = await tx.match.findMany({
        where: { externalId: { in: jogos.map((j) => j.externalId) } },
        select: { id: true },
      });

      const pool = await tx.pool.create({
        data: {
          name: dto.name.trim(),
          competitionCode: dto.competitionCode,
          matchdayFrom: dto.matchdayFrom,
          matchdayTo: dto.matchdayTo,
          // Regras da criação (CC-2). Omitir qualquer campo cai no default do
          // schema, que é o padrão decidido no PRD: 10 / 5 / 3.
          pointsExact: dto.rules?.pointsExact,
          pointsWinnerDiff: dto.rules?.pointsWinnerDiff,
          pointsWinner: dto.rules?.pointsWinner,
          tiebreakOrder: dto.rules?.tiebreakOrder,
          createdById: criadorId,
          // BE-4: quem cria é admin **e** participante. Admin permanente:
          // a decisão de 03/08 impede transferir ou perder a administração.
          members: { create: { userId: criadorId, role: 'ADMIN' } },
          matches: {
            create: persistidos.map((m) => ({ matchId: m.id })),
          },
        },
        select: {
          id: true,
          name: true,
          competitionCode: true,
          matchdayFrom: true,
          matchdayTo: true,
          createdAt: true,
          _count: { select: { matches: true } },
        },
      });

      return {
        id: pool.id,
        name: pool.name,
        competitionCode: pool.competitionCode,
        matchdayFrom: pool.matchdayFrom,
        matchdayTo: pool.matchdayTo,
        totalMatches: pool._count.matches,
        openMatches: comPrazoAberto,
        createdAt: pool.createdAt,
      };
    });
  }

  /**
   * Bolões ativos do participante.
   *
   * Lacuna encontrada ao construir o app (06/08): nenhuma story cobria listar
   * os próprios bolões — só o histórico de encerrados (CV-5). Sem isto o app
   * não tem como navegar para um bolão existente.
   */
  async meusBoloes(userId: string) {
    const membros = await this.prisma.poolMember.findMany({
      // Arquivados **entram** na resposta, marcados: a lista precisa separá-los
      // (CV-6 FE-1), e omiti-los fazia o bolão desaparecer do app sem que nada
      // dissesse que ele foi arquivado.
      where: { userId, leftAt: null },
      orderBy: { pool: { createdAt: 'desc' } },
      select: {
        role: true,
        pool: {
          select: {
            id: true,
            name: true,
            competitionCode: true,
            matchdayFrom: true,
            matchdayTo: true,
            closedAt: true,
            archivedAt: true,
            _count: {
              select: {
                matches: true,
                // Só quem continua no bolão. Contar quem saiu fazia a home
                // dizer "3 participantes" enquanto a lista mostrava um deles
                // como "saiu do bolão".
                members: { where: { leftAt: null } },
              },
            },
          },
        },
      },
    });

    return membros.map((m) => ({
      id: m.pool.id,
      name: m.pool.name,
      competitionCode: m.pool.competitionCode,
      matchdayFrom: m.pool.matchdayFrom,
      matchdayTo: m.pool.matchdayTo,
      totalMatches: m.pool._count.matches,
      totalMembers: m.pool._count.members,
      isAdmin: m.role === 'ADMIN',
      closed: m.pool.closedAt !== null,
      archived: m.pool.archivedAt !== null,
    }));
  }

  /** Campeonatos disponíveis para criar bolão. */
  async competicoes() {
    try {
      return await this.fonte.listarCompeticoes();
    } catch (error) {
      if (error instanceof FonteIndisponivelError) {
        throw new ServiceUnavailableException(
          'não foi possível listar os campeonatos agora',
        );
      }
      throw error;
    }
  }
}
