import {
  ConflictException,
  ForbiddenException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { Brackets, DataSource, EntityManager, Repository } from 'typeorm';
import { LoggingService } from '../database/logging/logging.service';
import { Player } from '../players/entities/player.entity';
import { Role } from '../roles/entities/role.entity';
import { RoleEnum } from '../roles/roles.enum';
import { Status } from '../statuses/entities/status.entity';
import { StatusEnum } from '../statuses/statuses.enum';
import {
  AdminPlayerPageDto,
  AdminPlayerSummaryDto,
} from './dto/admin-player-response.dto';
import { AdminPlayerQueryDto, AdminUserProfileDto } from './dto/admin-user.dto';
import { AdminUserDirectorySummaryDto } from './dto/user-directory-response.dto';
import { User } from './entities/user.entity';

type PlayerSearchRow = {
  player_id: number | string;
  first_name: string;
  last_name: string;
  active: boolean;
  team_id: number | string;
  team_name: string;
  team_alias: string;
  user_id: number | string | null;
  user_first_name: string | null;
  user_last_name: string | null;
  status_id: number | string | null;
  status_name: string | null;
};

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

  async updateProfile(
    actorId: number,
    targetUserId: number,
    dto: AdminUserProfileDto,
  ): Promise<AdminUserDirectorySummaryDto> {
    const summary = await this.dataSource.transaction(async (manager) => {
      const repository = manager.getRepository(User);
      const target = await this.findLockedUser(repository, targetUserId);
      if (dto.firstName !== undefined) target.firstName = dto.firstName;
      if (dto.lastName !== undefined) target.lastName = dto.lastName;
      await repository.save(target);
      const summary = await this.findAdminSummary(manager, targetUserId);
      await this.log(
        manager,
        'admin_user_profile_update',
        actorId,
        `targetUserId=${targetUserId}`,
      );
      return summary;
    });
    return summary;
  }

  async updateRole(
    actorId: number,
    targetUserId: number,
    roleId: RoleEnum,
  ): Promise<AdminUserDirectorySummaryDto> {
    const summary = await this.dataSource.transaction(async (manager) => {
      const activeAdmins = await this.lockActiveAdmins(manager);
      const repository = manager.getRepository(User);
      const target = await this.findLockedUser(repository, targetUserId);
      const isDemotion =
        target.role?.id === RoleEnum.admin && roleId !== RoleEnum.admin;
      if (actorId === targetUserId && isDemotion) {
        throw new ForbiddenException('Administrators cannot demote themselves');
      }
      if (
        isDemotion &&
        target.status?.id === StatusEnum.active &&
        activeAdmins.length <= 1
      ) {
        throw new ConflictException('At least one active admin must remain');
      }
      target.role = { id: roleId } as Role;
      await repository.save(target);
      const summary = await this.findAdminSummary(manager, targetUserId);
      await this.log(
        manager,
        'admin_user_role_update',
        actorId,
        `targetUserId=${targetUserId} roleId=${roleId}`,
      );
      return summary;
    });
    return summary;
  }

  async updateStatus(
    actorId: number,
    targetUserId: number,
    statusId: StatusEnum,
  ): Promise<AdminUserDirectorySummaryDto> {
    const summary = await this.dataSource.transaction(async (manager) => {
      const activeAdmins = await this.lockActiveAdmins(manager);
      const repository = manager.getRepository(User);
      const target = await this.findLockedUser(repository, targetUserId);
      const isDeactivation =
        target.status?.id === StatusEnum.active &&
        statusId === StatusEnum.inactive;
      if (actorId === targetUserId && isDeactivation) {
        throw new ForbiddenException(
          'Administrators cannot deactivate themselves',
        );
      }
      if (
        isDeactivation &&
        target.role?.id === RoleEnum.admin &&
        activeAdmins.length <= 1
      ) {
        throw new ConflictException('At least one active admin must remain');
      }
      target.status = { id: statusId } as Status;
      if (statusId === StatusEnum.inactive) target.hash = null;
      await repository.save(target);
      const summary = await this.findAdminSummary(manager, targetUserId);
      await this.log(
        manager,
        'admin_user_status_update',
        actorId,
        `targetUserId=${targetUserId} statusId=${statusId}`,
      );
      return summary;
    });
    return summary;
  }

  async findPlayers(query: AdminPlayerQueryDto): Promise<AdminPlayerPageDto> {
    const page = query.page ?? 1;
    const limit = query.limit ?? 20;
    const repository = this.dataSource.manager.getRepository(Player);
    const builder = repository
      .createQueryBuilder('player')
      .innerJoin('player.team', 'team')
      .leftJoin('player.user', 'currentUser')
      .leftJoin('currentUser.status', 'currentStatus');
    const term = query.search?.trim().toLocaleLowerCase();
    if (term) {
      builder
        .andWhere(
          new Brackets((where) =>
            where
              .where('LOWER(player.firstName) LIKE :playerSearch')
              .orWhere('LOWER(player.lastName) LIKE :playerSearch')
              .orWhere('LOWER(team.name) LIKE :playerSearch'),
          ),
        )
        .setParameter('playerSearch', `%${term}%`);
    }
    if (query.teamId !== undefined) {
      builder.andWhere('player.teamId = :teamId', { teamId: query.teamId });
    }
    if (query.assignment === 'assigned') {
      builder.andWhere('player.userId IS NOT NULL');
    } else if (query.assignment === 'unassigned') {
      builder.andWhere('player.userId IS NULL');
    }
    const total = await builder.getCount();
    const rows = await builder
      .select([
        'player.id AS player_id',
        'player.firstName AS first_name',
        'player.lastName AS last_name',
        'player.active AS active',
        'team.id AS team_id',
        'team.name AS team_name',
        'team.alias AS team_alias',
        'currentUser.id AS user_id',
        'currentUser.firstName AS user_first_name',
        'currentUser.lastName AS user_last_name',
        'currentStatus.id AS status_id',
        'currentStatus.name AS status_name',
      ])
      .orderBy('player.id', 'ASC')
      .offset((page - 1) * limit)
      .limit(limit)
      .getRawMany<PlayerSearchRow>();
    return {
      data: rows.map((row) => this.mapPlayerSearchRow(row)),
      page,
      limit,
      total,
      hasNextPage: page * limit < total,
    };
  }

  async assignPlayer(
    actorId: number,
    targetUserId: number,
    playerId: number,
  ): Promise<AdminUserDirectorySummaryDto> {
    let previousUserId: number | null = null;
    const summary = await this.dataSource.transaction(async (manager) => {
      const userRepository = manager.getRepository(User);
      const playerRepository = manager.getRepository(Player);
      const target = await userRepository.findOne({
        where: { id: targetUserId },
      });
      if (!target) throw new NotFoundException('User not found');
      const lockedPlayer = await this.findLockedPlayer(
        playerRepository,
        playerId,
      );
      previousUserId = lockedPlayer.userId ?? null;
      lockedPlayer.user = target;
      await playerRepository.save(lockedPlayer);
      const summary = await this.findAdminSummary(manager, targetUserId);
      await this.log(
        manager,
        'admin_player_assign',
        actorId,
        `targetUserId=${targetUserId} playerId=${playerId} previousUserId=${
          previousUserId ?? 'none'
        }`,
      );
      return summary;
    });
    return summary;
  }

  async unlinkPlayer(
    actorId: number,
    targetUserId: number,
    playerId: number,
  ): Promise<AdminUserDirectorySummaryDto> {
    const summary = await this.dataSource.transaction(async (manager) => {
      const playerRepository = manager.getRepository(Player);
      const lockedPlayer = await this.findLockedPlayer(
        playerRepository,
        playerId,
      );
      if (lockedPlayer.userId !== targetUserId) {
        throw new ConflictException('Player is not assigned to this user');
      }
      lockedPlayer.user = null;
      await playerRepository.save(lockedPlayer);
      const summary = await this.findAdminSummary(manager, targetUserId);
      await this.log(
        manager,
        'admin_player_unlink',
        actorId,
        `targetUserId=${targetUserId} playerId=${playerId}`,
      );
      return summary;
    });
    return summary;
  }

  private lockActiveAdmins(manager: EntityManager): Promise<User[]> {
    return manager
      .getRepository(User)
      .createQueryBuilder('user')
      .innerJoinAndSelect('user.role', 'role')
      .innerJoinAndSelect('user.status', 'status')
      .where('role.id = :adminRole', { adminRole: RoleEnum.admin })
      .andWhere('status.id = :activeStatus', {
        activeStatus: StatusEnum.active,
      })
      .setLock('pessimistic_write')
      .orderBy('user.id', 'ASC')
      .getMany();
  }

  private async findLockedUser(
    repository: Repository<User>,
    userId: number,
  ): Promise<User> {
    const user = await repository
      .createQueryBuilder('lockedUser')
      .leftJoinAndSelect('lockedUser.role', 'role')
      .leftJoinAndSelect('lockedUser.status', 'status')
      .where('lockedUser.id = :userId', { userId })
      .setLock('pessimistic_write')
      .getOne();
    if (!user) throw new NotFoundException('User not found');
    return user;
  }

  private async findLockedPlayer(
    repository: Repository<Player>,
    playerId: number,
  ): Promise<Player> {
    const player = await repository
      .createQueryBuilder('lockedPlayer')
      .where('lockedPlayer.id = :playerId', { playerId })
      .setLock('pessimistic_write')
      .getOne();
    if (!player) throw new NotFoundException('Player not found');
    return player;
  }

  private async findAdminSummary(
    manager: EntityManager,
    userId: number,
  ): Promise<AdminUserDirectorySummaryDto> {
    const user = await manager.getRepository(User).findOne({
      where: { id: userId },
      relations: {
        role: true,
        status: true,
        players: { team: true, teamRole: true },
      },
    });
    if (!user) throw new NotFoundException('User not found');
    return {
      id: user.id,
      firstName: user.firstName,
      lastName: user.lastName,
      email: user.email,
      role: user.role ? { id: user.role.id, name: user.role.name } : null,
      status: user.status
        ? { id: user.status.id, name: user.status.name }
        : null,
      assignments: (user.players ?? []).map((player) => ({
        id: player.id,
        firstName: player.firstName,
        lastName: player.lastName,
        active: player.active,
        team: {
          id: player.team.id,
          name: player.team.name,
          alias: player.team.alias,
        },
        teamRole: player.teamRole
          ? { id: player.teamRole.id, name: player.teamRole.name }
          : null,
      })),
    };
  }

  private mapPlayerSearchRow(row: PlayerSearchRow): AdminPlayerSummaryDto {
    return {
      id: Number(row.player_id),
      firstName: row.first_name,
      lastName: row.last_name,
      active: Boolean(row.active),
      team: {
        id: Number(row.team_id),
        name: row.team_name,
        alias: row.team_alias,
      },
      currentUser:
        row.user_id === null
          ? null
          : {
              id: Number(row.user_id),
              firstName: row.user_first_name,
              lastName: row.user_last_name,
              status:
                row.status_id === null
                  ? null
                  : {
                      id: Number(row.status_id),
                      name: row.status_name ?? undefined,
                    },
            },
    };
  }

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