import {
  ConflictException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { LoggingService } from 'src/database/logging/logging.service';
import { TeamRolesEnum } from 'src/team-roles/team-roles.enum';
import { Team } from 'src/teams/entities/team.entity';
import { TeamAccessService } from 'src/teams/team-access.service';
import { DataSource, EntityManager, Repository } from 'typeorm';
import { CreatePenaltyDTO } from './dto/create-penalty.dto';
import { PenaltyResponseDTO } from './dto/penalty-response.dto';
import { UpdatePenaltyDTO } from './dto/update-penalty.dto';
import { PenaltyEntity } from './entities/penalty.entity';

@Injectable()
export class PenaltyService {
  constructor(
    @InjectRepository(PenaltyEntity)
    private readonly repository: Repository<PenaltyEntity>,
    @InjectRepository(Team)
    private readonly teamRepository: Repository<Team>,
    private readonly dataSource: DataSource,
    private readonly access: TeamAccessService,
    private readonly logger: LoggingService,
  ) {}

  async getUserPenalties(userId: number): Promise<PenaltyResponseDTO[]> {
    const penalties = await this.repository.find({
      where: {
        team: { players: { user: { id: userId }, active: true } },
      },
      order: { description: 'ASC' },
    });
    return penalties.map((penalty) => this.toResponse(penalty));
  }

  async getTeamPenalties(
    userId: number,
    teamId: number,
  ): Promise<PenaltyResponseDTO[]> {
    const team = await this.teamRepository.findOne({ where: { id: teamId } });
    if (!team) throw new NotFoundException('Team nicht gefunden.');
    await this.access.assertMember(userId, teamId);
    const penalties = await this.repository.find({
      where: { team: { id: teamId } },
      order: { description: 'ASC' },
    });
    return penalties.map((penalty) => this.toResponse(penalty));
  }

  createPenalty(
    dto: CreatePenaltyDTO,
    userId: number,
  ): Promise<PenaltyResponseDTO> {
    return this.dataSource.transaction(async (manager) => {
      const team = await this.lockTeam(manager, dto.teamId);
      await this.access.assertAtLeast(
        userId,
        team.id,
        'penalty_manage_min_role',
        TeamRolesEnum.captain,
        manager,
      );
      const repository = manager.getRepository(PenaltyEntity);
      const description = dto.description.trim();
      await this.assertUniqueDescription(repository, team.id, description);
      const saved = await repository.save(
        repository.create({ team, description, amount: dto.amount }),
      );
      await this.logger.info(
        {
          event: 'penalty_catalog_create',
          details: `teamId=${team.id} penaltyId=${saved.id} action=create`,
          userId,
        },
        manager,
      );
      return this.toResponse(saved);
    });
  }

  async updatePenalty(
    penaltyId: number,
    dto: UpdatePenaltyDTO,
    userId: number,
  ): Promise<PenaltyResponseDTO> {
    const owner = await this.findOwner(penaltyId);
    return this.dataSource.transaction(async (manager) => {
      const team = await this.lockTeam(manager, owner.team.id);
      await this.access.assertAtLeast(
        userId,
        team.id,
        'penalty_manage_min_role',
        TeamRolesEnum.captain,
        manager,
      );
      const repository = manager.getRepository(PenaltyEntity);
      const penalty = await this.findTransactionalPenalty(
        repository,
        penaltyId,
        team.id,
      );
      const description = dto.description.trim();
      await this.assertUniqueDescription(
        repository,
        team.id,
        description,
        penaltyId,
      );
      penalty.description = description;
      penalty.amount = dto.amount;
      const saved = await repository.save(penalty);
      await this.logger.info(
        {
          event: 'penalty_catalog_update',
          details: `teamId=${team.id} penaltyId=${penaltyId} action=update`,
          userId,
        },
        manager,
      );
      return this.toResponse(saved);
    });
  }

  async deletePenalty(penaltyId: number, userId: number): Promise<void> {
    const owner = await this.findOwner(penaltyId);
    await this.dataSource.transaction(async (manager) => {
      const team = await this.lockTeam(manager, owner.team.id);
      await this.access.assertAtLeast(
        userId,
        team.id,
        'penalty_manage_min_role',
        TeamRolesEnum.captain,
        manager,
      );
      const repository = manager.getRepository(PenaltyEntity);
      const penalty = await this.findTransactionalPenalty(
        repository,
        penaltyId,
        team.id,
      );
      await repository.remove(penalty);
      await this.logger.info(
        {
          event: 'penalty_catalog_delete',
          details: `teamId=${team.id} penaltyId=${penaltyId} action=delete`,
          userId,
        },
        manager,
      );
    });
  }

  private async findOwner(penaltyId: number): Promise<PenaltyEntity> {
    const penalty = await this.repository.findOne({
      where: { id: penaltyId },
      relations: ['team'],
    });
    if (!penalty?.team) throw new NotFoundException('Strafe nicht gefunden.');
    return penalty;
  }

  private async lockTeam(
    manager: EntityManager,
    teamId: number,
  ): Promise<Team> {
    const team = await manager
      .getRepository(Team)
      .createQueryBuilder('team')
      .where('team.id = :teamId', { teamId })
      .setLock('pessimistic_write')
      .getOne();
    if (!team) throw new NotFoundException('Team nicht gefunden.');
    return team;
  }

  private async findTransactionalPenalty(
    repository: Repository<PenaltyEntity>,
    penaltyId: number,
    teamId: number,
  ): Promise<PenaltyEntity> {
    const penalty = await repository.findOne({
      where: { id: penaltyId, team: { id: teamId } },
      relations: ['team'],
    });
    if (!penalty) throw new NotFoundException('Strafe nicht gefunden.');
    return penalty;
  }

  private async assertUniqueDescription(
    repository: Repository<PenaltyEntity>,
    teamId: number,
    description: string,
    penaltyId?: number,
  ): Promise<void> {
    const query = repository
      .createQueryBuilder('penalty')
      .where('penalty.teamId = :teamId', { teamId })
      .andWhere('LOWER(TRIM(penalty.description)) = :description', {
        description: description.toLocaleLowerCase('de'),
      });
    if (penaltyId !== undefined) {
      query.andWhere('penalty.id != :penaltyId', { penaltyId });
    }
    if ((await query.getCount()) > 0) {
      throw new ConflictException(
        'Ein Eintrag mit dieser Beschreibung existiert bereits.',
      );
    }
  }

  private toResponse(penalty: PenaltyEntity): PenaltyResponseDTO {
    return {
      id: penalty.id,
      description: penalty.description,
      amount: Number(penalty.amount),
      createdAt: penalty.createdAt,
    };
  }
}
