import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { DataSource, EntityManager, Repository } from 'typeorm';
import { LoggingService } from '../database/logging/logging.service';
import { Player } from '../players/entities/player.entity';
import { TeamRole } from '../team-roles/entities/team-roles.entity';
import { TeamRolesEnum } from '../team-roles/team-roles.enum';
import { Transaction } from '../transactions/entitites/transaction.entity';
import { TransactionType } from '../transactions/entitites/transaction-type.entity';
import { TransactionTypeEnum } from '../transactions/transaction-type.enum';
import { TeamAccessService } from './team-access.service';

export const DEACTIVATION_ADJUSTMENT_NOTE_PREFIX =
  'Ausgleichsbuchung (Deaktivierung)';

@Injectable()
export class TeamMembersService {
  constructor(
    private readonly dataSource: DataSource,
    private readonly logger: LoggingService,
    private readonly access: TeamAccessService,
  ) {}

  async setActive(
    actorUserId: number,
    teamId: number,
    playerId: number,
    active: boolean,
  ): Promise<Player> {
    await this.access.assertAtLeast(
      actorUserId,
      teamId,
      'member_manage_min_role',
      TeamRolesEnum.captain,
    );

    return this.dataSource.transaction(async (manager) => {
      const activeTreasurers = await this.lockActiveTreasurers(manager, teamId);
      const playerRepository = manager.getRepository(Player);
      const player = await this.findLockedPlayer(playerRepository, playerId, teamId);

      if (player.active === active) return player;

      const isDeactivation = player.active && !active;
      if (
        isDeactivation &&
        player.teamRole?.id === TeamRolesEnum.treasurer &&
        activeTreasurers.length <= 1
      ) {
        throw new ConflictException(
          'Mindestens ein aktiver Kassenwart muss im Team verbleiben.',
        );
      }

      player.active = active;

      if (isDeactivation) {
        await this.zeroBalance(manager, player);
      } else {
        await this.recomputeBalance(manager, player);
      }

      await playerRepository.save(player);
      await this.log(
        manager,
        'player_active_update',
        actorUserId,
        `teamId=${teamId} playerId=${playerId} active=${active}`,
      );
      return player;
    });
  }

  async setTeamRole(
    actorUserId: number,
    teamId: number,
    playerId: number,
    teamRoleId: TeamRolesEnum,
  ): Promise<Player> {
    await this.access.assertAtLeast(
      actorUserId,
      teamId,
      'member_manage_min_role',
      TeamRolesEnum.captain,
    );

    return this.dataSource.transaction(async (manager) => {
      const activeTreasurers = await this.lockActiveTreasurers(manager, teamId);
      const playerRepository = manager.getRepository(Player);
      const player = await this.findLockedPlayer(playerRepository, playerId, teamId);

      if (player.teamRole?.id === teamRoleId) return player;

      const isDemotionFromTreasurer =
        player.active &&
        player.teamRole?.id === TeamRolesEnum.treasurer &&
        teamRoleId !== TeamRolesEnum.treasurer;
      if (isDemotionFromTreasurer && activeTreasurers.length <= 1) {
        throw new ConflictException(
          'Mindestens ein aktiver Kassenwart muss im Team verbleiben.',
        );
      }

      player.teamRole = { id: teamRoleId } as TeamRole;
      await playerRepository.save(player);
      await this.log(
        manager,
        'player_team_role_update',
        actorUserId,
        `teamId=${teamId} playerId=${playerId} teamRoleId=${teamRoleId}`,
      );
      return player;
    });
  }

  // insert() statt save(): umgeht bewusst @BeforeInsert setBalance() auf Transaction,
  // das intern this.player.save() (ActiveRecord/globale DataSource, NICHT `manager`)
  // aufruft -- würde zusammen mit dem pessimistic_write-Lock oben zur Selbstblockade führen.
  // Saldo wird stattdessen explizit über den transaktionalen `manager` gesetzt.
  private async zeroBalance(manager: EntityManager, player: Player): Promise<void> {
    const currentBalance = Number(player.balance);
    if (currentBalance === 0) return;
    await manager.getRepository(Transaction).insert({
      amount: -currentBalance,
      date: new Date().toISOString(),
      note: `${DEACTIVATION_ADJUSTMENT_NOTE_PREFIX} #${player.id}`,
      player: { id: player.id } as Player,
      type: { id: TransactionTypeEnum.credit } as TransactionType,
    });
    player.balance = 0;
  }

  private async recomputeBalance(manager: EntityManager, player: Player): Promise<void> {
    const transactions = await manager.getRepository(Transaction).find({
      where: { player: { id: player.id } },
      relations: ['type'],
    });
    player.balance = transactions
      .filter((t) => !t.note?.startsWith(DEACTIVATION_ADJUSTMENT_NOTE_PREFIX))
      .reduce((acc, t) => {
        let amount = Number(t.amount);
        if (t.type && t.type.id > 10 && amount > 0) amount = -amount;
        return acc + amount;
      }, 0);
  }

  private lockActiveTreasurers(
    manager: EntityManager,
    teamId: number,
  ): Promise<Player[]> {
    return manager
      .getRepository(Player)
      .createQueryBuilder('player')
      .innerJoin('player.team', 'team')
      .innerJoinAndSelect('player.teamRole', 'teamRole')
      .where('team.id = :teamId', { teamId })
      .andWhere('teamRole.id = :treasurerRole', {
        treasurerRole: TeamRolesEnum.treasurer,
      })
      .andWhere('player.active = :active', { active: true })
      .setLock('pessimistic_write')
      .orderBy('player.id', 'ASC')
      .getMany();
  }

  private async findLockedPlayer(
    repository: Repository<Player>,
    playerId: number,
    teamId: number,
  ): Promise<Player> {
    const player = await repository
      .createQueryBuilder('lockedPlayer')
      .leftJoinAndSelect('lockedPlayer.teamRole', 'teamRole')
      .leftJoinAndSelect('lockedPlayer.team', 'team')
      .where('lockedPlayer.id = :playerId', { playerId })
      .setLock('pessimistic_write')
      .getOne();
    if (!player || player.team?.id !== teamId) {
      throw new NotFoundException('Spieler nicht gefunden.');
    }
    return player;
  }

  private log(
    manager: EntityManager,
    event: Parameters<LoggingService['info']>[0]['event'],
    userId: number,
    details: string,
  ) {
    return this.logger.info({ event, userId, details }, manager);
  }
}
