import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { randomBytes } from 'crypto';
import { PenaltyEntity } from '../penalty/entities/penalty.entity';
import { Player } from '../players/entities/player.entity';
import { TeamRolesEnum } from '../team-roles/team-roles.enum';
import { Transaction } from '../transactions/entitites/transaction.entity';
import { Repository } from 'typeorm';
import {
  PublicAccessStatusDto,
  PublicPlayerDto,
  PublicPlayerHistoryDto,
  PublicTeamDto,
  PublicTransactionDto,
} from './dto/public-access.dto';
import { Team } from './entities/team.entity';
import { TeamAccessService } from './team-access.service';

@Injectable()
export class PublicTeamAccessService {
  constructor(
    @InjectRepository(Team)
    private readonly teamRepository: Repository<Team>,
    @InjectRepository(Player)
    private readonly playerRepository: Repository<Player>,
    @InjectRepository(Transaction)
    private readonly transactionRepository: Repository<Transaction>,
    @InjectRepository(PenaltyEntity)
    private readonly penaltyRepository: Repository<PenaltyEntity>,
    private readonly access: TeamAccessService,
  ) {}

  async getStatus(
    userId: number,
    teamId: number,
  ): Promise<PublicAccessStatusDto> {
    await this.access.assertMember(userId, teamId);
    return this.toStatus(await this.loadManagedTeam(teamId));
  }

  async setEnabled(
    userId: number,
    teamId: number,
    enabled: boolean,
  ): Promise<PublicAccessStatusDto> {
    await this.access.assertAtLeast(
      userId,
      teamId,
      'public_access_manage_min_role',
      TeamRolesEnum.captain,
    );
    const team = await this.loadManagedTeam(teamId);
    if (enabled && !team.publicAccessToken) {
      team.publicAccessToken = this.createToken();
    }
    team.publicAccessEnabled = enabled;
    await this.teamRepository.save(team);
    return this.toStatus(team);
  }

  async rotate(userId: number, teamId: number): Promise<PublicAccessStatusDto> {
    await this.access.assertAtLeast(
      userId,
      teamId,
      'public_access_manage_min_role',
      TeamRolesEnum.captain,
    );
    const team = await this.loadManagedTeam(teamId);
    team.publicAccessToken = this.createToken();
    await this.teamRepository.save(team);
    return this.toStatus(team);
  }

  async getPublicTeam(token: string): Promise<PublicTeamDto> {
    const team = await this.loadPublicTeam(token);
    const [players, penalties] = await Promise.all([
      this.playerRepository.find({
        where: { team: { id: team.id }, active: true },
        order: { lastName: 'ASC', firstName: 'ASC' },
      }),
      this.penaltyRepository.find({
        where: { team: { id: team.id } },
        order: { description: 'ASC' },
      }),
    ]);
    const publicPlayers = players.map((player) => this.toPublicPlayer(player));

    return {
      name: team.name,
      balance: Number(team.balance),
      outstanding: publicPlayers.reduce(
        (total, player) => total - Number(player.balance),
        0,
      ),
      players: publicPlayers,
      penalties: penalties.map((penalty) => ({
        id: penalty.id,
        description: penalty.description,
        amount: Number(penalty.amount),
      })),
    };
  }

  async getPublicPlayerHistory(
    token: string,
    playerId: number,
  ): Promise<PublicPlayerHistoryDto> {
    const team = await this.loadPublicTeam(token);
    const player = await this.playerRepository.findOne({
      where: { id: playerId, team: { id: team.id }, active: true },
    });
    if (!player) throw new NotFoundException('Freigabe nicht gefunden.');

    return {
      player: this.toPublicPlayer(player),
      transactions: await this.loadTransactions(player.id),
    };
  }

  async getMemberPlayerTransactions(
    userId: number,
    teamId: number,
    playerId: number,
  ): Promise<PublicTransactionDto[]> {
    await this.access.assertMember(userId, teamId);
    const player = await this.playerRepository.findOne({
      where: { id: playerId, team: { id: teamId } },
    });
    if (!player) throw new NotFoundException('Spieler nicht gefunden.');
    return this.loadTransactions(player.id);
  }

  private async loadManagedTeam(teamId: number): Promise<Team> {
    const team = await this.teamRepository
      .createQueryBuilder('team')
      .addSelect('team.publicAccessToken')
      .where('team.id = :teamId', { teamId })
      .getOne();
    if (!team) throw new NotFoundException('Team nicht gefunden.');
    return team;
  }

  private async loadPublicTeam(token: string): Promise<Team> {
    if (!/^[a-f0-9]{64}$/.test(token)) {
      throw new NotFoundException('Freigabe nicht gefunden.');
    }
    const team = await this.teamRepository.findOne({
      where: { publicAccessToken: token, publicAccessEnabled: true },
      select: {
        id: true,
        name: true,
        balance: true,
        publicAccessEnabled: true,
        publicAccessToken: true,
      },
    });
    if (!team) throw new NotFoundException('Freigabe nicht gefunden.');
    return team;
  }

  private async loadTransactions(
    playerId: number,
  ): Promise<PublicTransactionDto[]> {
    const transactions = await this.transactionRepository.find({
      where: { player: { id: playerId } },
      order: { date: 'DESC' },
      take: 20,
    });
    return transactions.map((transaction) => ({
      id: transaction.id,
      note: transaction.note,
      date: transaction.date,
      amount: Number(transaction.amount),
      type: transaction.type
        ? { id: transaction.type.id, name: transaction.type.name }
        : null,
    }));
  }

  private toPublicPlayer(player: Player): PublicPlayerDto {
    return {
      id: player.id,
      firstName: player.firstName,
      lastName: player.lastName,
      balance: Number(player.balance),
      active: player.active,
    };
  }

  private toStatus(team: Team): PublicAccessStatusDto {
    return {
      enabled: team.publicAccessEnabled,
      token: team.publicAccessToken ?? null,
    };
  }

  private createToken(): string {
    return randomBytes(32).toString('hex');
  }
}
