import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Player } from 'src/players/entities/player.entity';
import { EntityCondition } from 'src/utils/types/entity-condition.type';
import { IPaginationOptions } from 'src/utils/types/pagination-options';
import { Brackets, Repository, SelectQueryBuilder } from 'typeorm';
import { RoleEnum } from '../roles/roles.enum';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { UserDirectoryQueryDto } from './dto/user-directory-query.dto';
import {
  AdminUserDirectorySummaryDto,
  UserDirectoryAssignmentDto,
  UserDirectoryPageDto,
  UserDirectoryReferenceDto,
  UserDirectorySummaryDto,
} from './dto/user-directory-response.dto';
import { User } from './entities/user.entity';
import { UserTeamPlayerDto } from './dto/user-team-response.dto';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
    @InjectRepository(Player)
    private playersRepository: Repository<Player>,
  ) {}

  create(createProfileDto: CreateUserDto) {
    return this.usersRepository.save(
      this.usersRepository.create(createProfileDto),
    );
  }

  findManyWithPagination(paginationOptions: IPaginationOptions) {
    return this.usersRepository.find({
      skip: (paginationOptions.page - 1) * paginationOptions.limit,
      take: paginationOptions.limit,
    });
  }

  async findDirectory(
    requester: Pick<User, 'id' | 'role'>,
    query: UserDirectoryQueryDto,
  ): Promise<UserDirectoryPageDto> {
    const isAdmin = requester.role?.id === RoleEnum.admin;
    const page = query.page ?? 1;
    const limit = query.limit ?? 20;
    const sharedTeamsQuery = isAdmin
      ? undefined
      : this.createSharedTeamsQuery(requester.id);
    const directoryQuery = this.createDirectoryUsersQuery(
      isAdmin,
      sharedTeamsQuery,
      query.search,
    );
    const totalResult = await directoryQuery
      .clone()
      .select('COUNT(DISTINCT user.id)', 'total')
      .getRawOne<{ total: string }>();
    const userRows = await directoryQuery
      .select(this.directoryUserSelects(isAdmin))
      .distinct(true)
      .orderBy('user.id', 'ASC')
      .offset((page - 1) * limit)
      .limit(limit)
      .getRawMany<DirectoryUserRow>();
    const assignments = await this.findDirectoryAssignments(
      userRows.map((user) => Number(user.user_id)),
      isAdmin,
      sharedTeamsQuery,
    );
    const assignmentsByUserId = assignments.reduce((byUserId, assignment) => {
      const userAssignments = byUserId.get(Number(assignment.user_id)) ?? [];
      userAssignments.push(assignment);
      byUserId.set(Number(assignment.user_id), userAssignments);
      return byUserId;
    }, new Map<number, DirectoryAssignmentRow[]>());
    const total = Number(totalResult?.total ?? 0);
    const data = userRows.map((user) =>
      this.mapDirectoryUser(
        user,
        assignmentsByUserId.get(Number(user.user_id)) ?? [],
        isAdmin,
      ),
    );

    return {
      data,
      page,
      limit,
      total,
      hasNextPage: page * limit < total,
    };
  }

  findOne(fields: EntityCondition<User>) {
    return this.usersRepository.findOne({
      where: fields,
    });
  }

  update(id: number, updateProfileDto: UpdateUserDto) {
    return this.usersRepository.save(
      this.usersRepository.create({
        id,
        ...updateProfileDto,
      }),
    );
  }

  async softDelete(id: number): Promise<void> {
    await this.usersRepository.softDelete(id);
  }

  async findMyTeams(userId: number): Promise<UserTeamPlayerDto[]> {
    const rows = await this.playersRepository
      .createQueryBuilder('player')
      .innerJoin('player.team', 'team')
      .select([
        'player.id AS player_id',
        'player.firstName AS first_name',
        'player.lastName AS last_name',
        'team.id AS team_id',
        'team.name AS team_name',
      ])
      .where('player.userId = :userId', { userId })
      .orderBy('player.id', 'ASC')
      .getRawMany<{
        player_id: number | string;
        first_name: string;
        last_name: string;
        team_id: number | string;
        team_name: string;
      }>();
    return rows.map((row) => ({
      id: Number(row.player_id),
      firstName: row.first_name,
      lastName: row.last_name,
      team: { id: Number(row.team_id), name: row.team_name },
    }));
  }

  private createSharedTeamsQuery(requesterId: number) {
    return this.playersRepository
      .createQueryBuilder('requesterPlayer')
      .select('requesterPlayer.teamId')
      .where('requesterPlayer.userId = :requesterId', { requesterId });
  }

  private createDirectoryUsersQuery(
    isAdmin: boolean,
    sharedTeamsQuery: SelectQueryBuilder<Player> | undefined,
    search: string | undefined,
  ) {
    const query = this.usersRepository
      .createQueryBuilder('user')
      .leftJoin('user.status', 'status')
      .leftJoin('user.role', 'role');

    if (!isAdmin && sharedTeamsQuery) {
      query
        .innerJoin(
          'user.players',
          'visibleAssignment',
          `visibleAssignment.teamId IN (${sharedTeamsQuery.getQuery()})`,
        )
        .setParameters(sharedTeamsQuery.getParameters());
    }

    const term = search?.trim().toLocaleLowerCase();
    if (!term) {
      return query;
    }

    const assignmentAlias = isAdmin ? 'searchAssignment' : 'visibleAssignment';
    if (isAdmin) {
      query.leftJoin('user.players', assignmentAlias);
    }

    const searchColumns = [
      'user.firstName',
      'user.lastName',
      `${assignmentAlias}.firstName`,
      `${assignmentAlias}.lastName`,
      ...(isAdmin ? ['user.email'] : []),
    ];
    query.andWhere(
      new Brackets((where) => {
        searchColumns.forEach((column, index) => {
          const expression = `LOWER(${column}) LIKE :directorySearch`;
          if (index === 0) {
            where.where(expression);
          } else {
            where.orWhere(expression);
          }
        });
      }),
    );
    return query.setParameter('directorySearch', `%${term}%`);
  }

  private directoryUserSelects(isAdmin: boolean): string[] {
    return [
      'user.id AS user_id',
      'user.firstName AS first_name',
      'user.lastName AS last_name',
      'status.id AS status_id',
      'status.name AS status_name',
      ...(isAdmin
        ? [
            'user.email AS email',
            'role.id AS role_id',
            'role.name AS role_name',
          ]
        : []),
    ];
  }

  private async findDirectoryAssignments(
    userIds: number[],
    isAdmin: boolean,
    sharedTeamsQuery: SelectQueryBuilder<Player> | undefined,
  ): Promise<DirectoryAssignmentRow[]> {
    if (userIds.length === 0) {
      return [];
    }

    const query = this.playersRepository
      .createQueryBuilder('player')
      .innerJoin('player.team', 'team')
      .leftJoin('player.teamRole', 'teamRole')
      .select([
        'player.id AS player_id',
        'player.userId AS user_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',
        'teamRole.id AS team_role_id',
        'teamRole.name AS team_role_name',
      ])
      .where('player.userId IN (:...userIds)', { userIds })
      .orderBy('player.id', 'ASC');

    if (!isAdmin && sharedTeamsQuery) {
      query
        .andWhere(`player.teamId IN (${sharedTeamsQuery.getQuery()})`)
        .setParameters(sharedTeamsQuery.getParameters());
    }

    return query.getRawMany<DirectoryAssignmentRow>();
  }

  private mapDirectoryUser(
    user: DirectoryUserRow,
    assignments: DirectoryAssignmentRow[],
    includeAdminFields: boolean,
  ): UserDirectorySummaryDto | AdminUserDirectorySummaryDto {
    const summary: UserDirectorySummaryDto = {
      id: Number(user.user_id),
      firstName: user.first_name,
      lastName: user.last_name,
      status: this.mapDirectoryReference(user.status_id, user.status_name),
      assignments: assignments.map((assignment) =>
        this.mapDirectoryAssignment(assignment),
      ),
    };

    if (!includeAdminFields) {
      return summary;
    }

    return {
      ...summary,
      email: user.email,
      role: this.mapDirectoryReference(user.role_id, user.role_name),
    };
  }

  private mapDirectoryAssignment(
    player: DirectoryAssignmentRow,
  ): UserDirectoryAssignmentDto {
    return {
      id: Number(player.player_id),
      firstName: player.first_name,
      lastName: player.last_name,
      active: this.asBoolean(player.active),
      team: {
        id: Number(player.team_id),
        name: player.team_name,
        alias: player.team_alias,
      },
      teamRole: this.mapDirectoryReference(
        player.team_role_id,
        player.team_role_name,
      ),
    };
  }

  private asBoolean(value: boolean | number | string): boolean {
    return value === true || value === 1 || value === '1';
  }

  private mapDirectoryReference(
    id: number | string | null,
    name: string | null,
  ): UserDirectoryReferenceDto | null {
    if (id == null) {
      return null;
    }

    return {
      id: Number(id),
      name: name ?? undefined,
    };
  }
}

interface DirectoryUserRow {
  user_id: number | string;
  first_name: string | null;
  last_name: string | null;
  status_id: number | string | null;
  status_name: string | null;
  email?: string | null;
  role_id?: number | string | null;
  role_name?: string | null;
}

interface DirectoryAssignmentRow {
  player_id: number | string;
  user_id: number | string;
  first_name: string;
  last_name: string;
  active: boolean | number | string;
  team_id: number | string;
  team_name: string;
  team_alias: string;
  team_role_id: number | string | null;
  team_role_name: string | null;
}
