import { Injectable } from '@nestjs/common';
import { DataSource, EntityManager, IsNull, In } from 'typeorm';
import { ProgramAccessRepository } from './program-access.repository';
import { AddProgramAccessUsersDto } from './dto/add-program-access-users.dto';
import { UpdateProgramAccessUserDto } from './dto/update-program-access-user.dto';
import { QueryProgramAccessUsersDto } from './dto/query-program-access-users.dto';
import { BulkUpdateProgramAccessDto } from './dto/bulk-update-program-access.dto';
import { BulkRemoveProgramAccessDto } from './dto/bulk-remove-program-access.dto';
import {
  ProgramAccessUserResponseDto,
  ProgramAccessUsersListResponseDto,
} from './dto/program-access-user-response.dto';
import { AppLoggerService } from 'src/common/services/logger.service';
import { ProgramAccessUserMap } from 'src/common/entities/program-access-user-map.entity';
import { ProgramRegistration } from 'src/common/entities';
import { programAccessServiceMessages, ROLE_KEYS } from 'src/common/constants/strings-constants';

@Injectable()
export class ProgramAccessService {
  constructor(
    private readonly programAccessRepository: ProgramAccessRepository,
    private readonly dataSource: DataSource,
    private readonly logger: AppLoggerService,
  ) {}

  /**
   * Add users to program access map
   */
  async addProgramAccessUsers(
    programId: number,
    dto: AddProgramAccessUsersDto,
    createdBy: number,
  ): Promise<ProgramAccessUserResponseDto[]> {
    try {
      this.logger.log(programAccessServiceMessages.ADDING_USERS(dto.userIds.length, programId));

      const mappings = await this.programAccessRepository.addProgramAccessUsers(
        programId,
        dto,
        createdBy,
      );

      return mappings.map((mapping) => this.mapToResponseDto(mapping));
    } catch (error) {
      this.logger.error(`Error in addProgramAccessUsers: ${error.message}`, error.stack);
      throw error;
    }
  }

  /**
   * Update program access for a specific user
   */
  async updateProgramAccessUser(
    programId: number,
    userId: number,
    dto: UpdateProgramAccessUserDto,
    updatedBy: number,
  ): Promise<ProgramAccessUserResponseDto> {
    try {
      this.logger.log(programAccessServiceMessages.UPDATING_USER(userId, programId));

      const mapping = await this.programAccessRepository.updateProgramAccessUser(
        programId,
        userId,
        dto,
        updatedBy,
      );

      return this.mapToResponseDto(mapping);
    } catch (error) {
      this.logger.error(`Error in updateProgramAccessUser: ${error.message}`, error.stack);
      throw error;
    }
  }

  /**
   * Remove program access for a specific user (soft delete)
   */
  async removeProgramAccessUser(
    programId: number,
    userId: number,
    deletedBy: number,
  ): Promise<{ success: boolean; message: string }> {
    try {
      this.logger.log(programAccessServiceMessages.REMOVING_USER(userId, programId));

      await this.programAccessRepository.removeProgramAccessUser(programId, userId, deletedBy);

      return {
        success: true,
        message: programAccessServiceMessages.ACCESS_REMOVED(userId, programId),
      };
    } catch (error) {
      this.logger.error(`Error in removeProgramAccessUser: ${error.message}`, error.stack);
      throw error;
    }
  }

  /**
   * Get all users with access to a program
   */
  async getProgramAccessUsers(
    programId: number,
    query: QueryProgramAccessUsersDto,
  ): Promise<ProgramAccessUsersListResponseDto> {
    try {
      this.logger.log(programAccessServiceMessages.FETCHING_USERS(programId));

      const { data, total } = await this.programAccessRepository.getProgramAccessUsers(
        programId,
        query,
      );

      const responseDtos = data.map((mapping) => this.mapToResponseDto(mapping, true));
      
      const limit = query.limit || 20;
      const offset = query.offset || 0;

      return {
        data: responseDtos,
        pagination: {
          totalPages: Math.ceil(total / limit),
          pageNumber: Math.floor(offset / limit) + 1,
          pageSize: limit,
          totalRecords: total,
          numberOfRecords: responseDtos.length,
        },
      };
    } catch (error) {
      this.logger.error(`Error in getProgramAccessUsers: ${error.message}`, error.stack);
      throw error;
    }
  }

  /**
   * Check if a user has access to view a program
   */
  async hasUserAccessToProgram(programId: number, userId: number): Promise<boolean> {
    try {
      return await this.programAccessRepository.hasUserAccessToProgram(programId, userId);
    } catch (error) {
      this.logger.error(`Error in hasUserAccessToProgram: ${error.message}`, error.stack);
      return false;
    }
  }

  /**
   * Check if a user can register for a program
   */
  async canUserRegisterForProgram(programId: number, userId: number): Promise<boolean> {
    try {
      return await this.programAccessRepository.canUserRegisterForProgram(programId, userId);
    } catch (error) {
      this.logger.error(`Error in canUserRegisterForProgram: ${error.message}`, error.stack);
      return false;
    }
  }

  /**
   * Get user's access details for a program
   */
  async getUserAccessMapping(
    programId: number,
    userId: number,
  ): Promise<ProgramAccessUserResponseDto | null> {
    try {
      const mapping = await this.programAccessRepository.getUserAccessMapping(programId, userId);

      if (!mapping) {
        return null;
      }

      return this.mapToResponseDto(mapping, true);
    } catch (error) {
      this.logger.error(`Error in getUserAccessMapping: ${error.message}`, error.stack);
      return null;
    }
  }

  /**
   * Bulk check access for multiple program-user pairs
   */
  async bulkCheckAccess(
    checks: Array<{ programId: number; userId: number }>,
  ): Promise<Map<string, boolean>> {
    try {
      return await this.programAccessRepository.bulkCheckAccess(checks);
    } catch (error) {
      this.logger.error(`Error in bulkCheckAccess: ${error.message}`, error.stack);
      return new Map();
    }
  }

  /**
   * Bulk update program access for multiple users
   */
  async bulkUpdateProgramAccessUsers(
    programId: number,
    dto: BulkUpdateProgramAccessDto,
    updatedBy: number,
  ): Promise<{ updated: number; failed: number; errors: string[] }> {
    try {
      this.logger.log(programAccessServiceMessages.BULK_UPDATING(dto.userIds.length, programId));

      const updateData = {
        accessScope: dto.accessScope,
        state: dto.state,
        effectiveFrom: dto.effectiveFrom ? new Date(dto.effectiveFrom) : undefined,
        effectiveTill: dto.effectiveTill ? new Date(dto.effectiveTill) : undefined,
        reason: dto.reason,
        meta: dto.meta,
      };

      const result = await this.programAccessRepository.bulkUpdateProgramAccessUsers(
        programId,
        dto.userIds,
        updateData,
        updatedBy,
      );

      this.logger.log(programAccessServiceMessages.BULK_UPDATE_COMPLETE(result.updated, result.failed));
      return result;
    } catch (error) {
      this.logger.error(`Error in bulkUpdateProgramAccessUsers: ${error.message}`, error.stack);
      throw error;
    }
  }

  /**
   * Bulk remove program access for multiple users
   * Optionally cascade delete registrations
   */
  async bulkRemoveProgramAccessUsers(
    programId: number,
    dto: BulkRemoveProgramAccessDto,
    deletedBy: number,
  ): Promise<{ removed: number; failed: number; registrationsCascaded: number; errors: string[] }> {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      this.logger.log(programAccessServiceMessages.BULK_REMOVING(dto.userIds.length, programId));

      // Remove access mappings
      const result = await this.programAccessRepository.bulkRemoveProgramAccessUsers(
        programId,
        dto.userIds,
        dto.reason,
        deletedBy,
        queryRunner.manager,
      );

      let registrationsCascaded = 0;

      // Cascade delete registrations if requested
      if (dto.cascadeDeleteRegistrations) {
        this.logger.log(programAccessServiceMessages.CASCADING_DELETION(dto.userIds.length));
        registrationsCascaded = await this.cascadeDeleteRegistrations(
          programId,
          dto.userIds,
          deletedBy,
          queryRunner.manager,
        );
      }

      await queryRunner.commitTransaction();

      this.logger.log(
        programAccessServiceMessages.BULK_REMOVE_COMPLETE(result.removed, result.failed, registrationsCascaded),
      );

      return {
        removed: result.removed,
        failed: result.failed,
        registrationsCascaded,
        errors: result.errors,
      };
    } catch (error) {
      await queryRunner.rollbackTransaction();
      this.logger.error(`Error in bulkRemoveProgramAccessUsers: ${error.message}`, error.stack);
      throw error;
    } finally {
      await queryRunner.release();
    }
  }

  /**
   * Cascade delete registrations for users and update program seat counts
   * @private
   */
  private async cascadeDeleteRegistrations(
    programId: number,
    userIds: number[],
    deletedBy: number,
    manager: EntityManager,
  ): Promise<number> {
    try {
      const registrationRepo = manager.getRepository(ProgramRegistration);
      const programRepo = manager.getRepository('Program');

      // Find active registrations for these users
      const registrations = await registrationRepo.find({
        where: {
          programId,
          userId: In(userIds),
          deletedAt: IsNull(),
        },
      });

      if (registrations.length === 0) {
        return 0;
      }

      // Soft delete registrations
      const now = new Date();
      for (const registration of registrations) {
        registration.deletedAt = now;
        // You might want to update status as well
        // registration.status = RegistrationStatusEnum.CANCELLED;
      }

      await registrationRepo.save(registrations);

      // Update program seat counts
      // Decrease filled_seats by the number of deleted registrations
      await programRepo.decrement(
        { id: programId },
        'filledSeats',
        registrations.length,
      );

      // Increase available_seats by the same amount
      await programRepo.increment(
        { id: programId },
        'availableSeats',
        registrations.length,
      );

      this.logger.log(
        programAccessServiceMessages.CASCADED_REGISTRATIONS(registrations.length, programId),
      );

      return registrations.length;
    } catch (error) {
      this.logger.error(`Error cascading registration deletion: ${error.message}`, error.stack);
      throw error;
    }
  }

  /**
   * Map entity to response DTO
   */
  /**
   * Check if user has admin role (any role except ROLE_VIEWER)
   * Admin roles bypass program access checks
   */
  isAdminRole(userRoles: string[]): boolean {
    if (!userRoles || userRoles.length === 0) {
      return false;
    }
    
    // Admin roles are all roles except ROLE_VIEWER
    const adminRoles = [
      ROLE_KEYS.ADMIN,
      ROLE_KEYS.MAHATRIA,
      ROLE_KEYS.RM,
      ROLE_KEYS.FINANCE_MANAGER,
      ROLE_KEYS.RELATIONAL_MANAGER,
      ROLE_KEYS.SHOBA,
      ROLE_KEYS.OPERATIONAL_MANAGER,
      ROLE_KEYS.RM_SUPPORT,
    ];
    
    return userRoles.some(role => adminRoles.includes(role));
  }

  /**
   * Verify user access to view program (with admin bypass)
   * Admins bypass the access check, only ROLE_VIEWER needs explicit access
   */
  async verifyViewAccess(programId: number, userId: number, userRoles: string[]): Promise<boolean> {
    try {
      // Admin roles bypass the access check
      if (this.isAdminRole(userRoles)) {
        this.logger.log(programAccessServiceMessages.ADMIN_BYPASS_VIEW(userId, programId));
        return true;
      }

      // For ROLE_VIEWER, check program access
      const hasAccess = await this.hasUserAccessToProgram(programId, userId);
      
      if (!hasAccess) {
        this.logger.warn(programAccessServiceMessages.NO_VIEW_ACCESS(userId, programId));
      }
      
      return hasAccess;
    } catch (error) {
      this.logger.error(`Error in verifyViewAccess: ${error.message}`, error.stack);
      return false;
    }
  }

  /**
   * Verify user access to register for program (with admin bypass)
   * Admins bypass the access check, only ROLE_VIEWER needs explicit access
   */
  async verifyRegistrationAccess(programId: number, userId: number, userRoles: string[]): Promise<boolean> {
    try {
      // Admin roles bypass the access check
      if (this.isAdminRole(userRoles)) {
        this.logger.log(programAccessServiceMessages.ADMIN_BYPASS_REGISTER(userId, programId));
        return true;
      }

      // For ROLE_VIEWER, check program access
      const canRegister = await this.canUserRegisterForProgram(programId, userId);
      
      if (!canRegister) {
        this.logger.warn(programAccessServiceMessages.NO_REGISTER_ACCESS(userId, programId));
      }
      
      return canRegister;
    } catch (error) {
      this.logger.error(`Error in verifyRegistrationAccess: ${error.message}`, error.stack);
      return false;
    }
  }

  private mapToResponseDto(
    mapping: ProgramAccessUserMap,
    includeUser: boolean = false,
  ): ProgramAccessUserResponseDto {
    const dto: ProgramAccessUserResponseDto = {
      id: mapping.id,
      programId: mapping.programId,
      userId: mapping.userId,
      accessScope: mapping.accessScope,
      state: mapping.state,
      effectiveFrom: mapping.effectiveFrom,
      effectiveTill: mapping.effectiveTill,
      reason: mapping.reason,
      meta: mapping.meta,
    };

    if (includeUser && mapping.user) {
      dto.user = {
        id: mapping.user.id,
        fullName: mapping.user.fullName,
        email: mapping.user.email,
        phoneNumber: mapping.user.phoneNumber,
        gender: mapping.user.gender,
        role: mapping.user.role,
      };
    }

    return dto;
  }

  /**
   * Get only active user IDs with access to a program
   * @param programId - Program ID
   * @returns Array of user IDs with active access
   */
  async getProgramAccessUserIds(programId: number): Promise<number[]> {
    try {
      this.logger.log(`Fetching user IDs with access to program ${programId}`);
      return await this.programAccessRepository.getProgramAccessUserIds(programId);
    } catch (error) {
      this.logger.error(`Error in getProgramAccessUserIds: ${error.message}`, error.stack);
      return [];
    }
  }
}
