import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, EntityManager, IsNull, In } from 'typeorm';
import { ProgramAccessUserMap } from 'src/common/entities/program-access-user-map.entity';
import { Program, User } from 'src/common/entities';
import { AppLoggerService } from 'src/common/services/logger.service';
import { ERROR_CODES } from 'src/common/constants/error-string-constants';
import { InifniNotFoundException } from 'src/common/exceptions/infini-notfound-exception';
import InifniBadRequestException from 'src/common/exceptions/infini-badrequest-exception';
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 { AccessStateEnum } from 'src/common/enum/access-state.enum';
import { AccessScopeEnum } from 'src/common/enum/access-scope.enum';
import { ProgramAccessTypeEnum } from 'src/common/enum/program-access-type.enum';
import { programAccessRepositoryMessages } from 'src/common/constants/strings-constants';

@Injectable()
export class ProgramAccessRepository {
  constructor(
    @InjectRepository(ProgramAccessUserMap)
    private readonly accessMapRepo: Repository<ProgramAccessUserMap>,
    @InjectRepository(Program)
    private readonly programRepo: Repository<Program>,
    @InjectRepository(User)
    private readonly userRepo: Repository<User>,
    private readonly logger: AppLoggerService,
  ) {}

  /**
   * Add multiple users to program access map
   */
  async addProgramAccessUsers(
    programId: number,
    dto: AddProgramAccessUsersDto,
    createdBy: number,
    manager?: EntityManager,
  ): Promise<ProgramAccessUserMap[]> {
    try {
      const accessMapRepo = manager ? manager.getRepository(ProgramAccessUserMap) : this.accessMapRepo;
      const programRepo = manager ? manager.getRepository(Program) : this.programRepo;
      const userRepo = manager ? manager.getRepository(User) : this.userRepo;

      // Validate program exists
      const program = await programRepo.findOne({ where: { id: programId, deletedAt: IsNull() } });
      if (!program) {
        this.logger.error(programAccessRepositoryMessages.PROGRAM_NOT_FOUND_WITH_ID(programId));
        throw new InifniNotFoundException(ERROR_CODES.PROGRAM_NOTFOUND, null, null, programId.toString());
      }

      // Validate all users exist
      const users = await userRepo.find({ where: { id: In(dto.userIds) } });
      if (users.length !== dto.userIds.length) {
        const foundIds = users.map((u) => u.id);
        const missingIds = dto.userIds.filter((id) => !foundIds.includes(id));
        this.logger.error(programAccessRepositoryMessages.USER_NOT_FOUND_LIST(missingIds));
        throw new InifniNotFoundException(
          ERROR_CODES.USER_NOTFOUND,
          null,
          null,
          programAccessRepositoryMessages.USER_NOT_FOUND_LIST(missingIds),
        );
      }

      // Check for existing active mappings
      const existingMappings = await accessMapRepo.find({
        where: {
          programId,
          userId: In(dto.userIds),
          state: AccessStateEnum.ACTIVE,
          deletedAt: IsNull(),
        },
      });

      if (existingMappings.length > 0) {
        const existingUserIds = existingMappings.map((m) => m.userId);
        this.logger.warn(programAccessRepositoryMessages.ACTIVE_MAPPING_EXISTS(existingUserIds));
        throw new InifniBadRequestException(
          ERROR_CODES.PROGRAM_ACCESS_DUPLICATE,
          null,
          programAccessRepositoryMessages.ACTIVE_MAPPING_EXISTS(existingUserIds),
        );
      }

      // Create access mappings
      const mappings = dto.userIds.map((userId) => {
        const mapping = new ProgramAccessUserMap({});
        mapping.programId = programId;
        mapping.userId = userId;
        mapping.accessScope = dto.accessScope ?? AccessScopeEnum.VIEW_AND_REGISTER;
        mapping.effectiveFrom = dto.effectiveFrom ? new Date(dto.effectiveFrom) : undefined;
        mapping.effectiveTill = dto.effectiveTill ? new Date(dto.effectiveTill) : undefined;
        mapping.reason = dto.reason ?? undefined;
        mapping.meta = dto.meta ?? undefined;
        mapping.state = AccessStateEnum.ACTIVE;
        mapping.programStatus = program.status;
        mapping.createdBy = createdBy;
        return mapping;
      });

      const savedMappings = await accessMapRepo.save(mappings);
      this.logger.log(programAccessRepositoryMessages.USERS_ADDED_TO_PROGRAM(savedMappings.length, programId));

      return savedMappings;
    } catch (error) {
      this.logger.error(programAccessRepositoryMessages.ERROR_ADDING_USERS(error.message), error.stack);
      throw error;
    }
  }

  /**
   * Update program access for a specific user
   */
  async updateProgramAccessUser(
    programId: number,
    userId: number,
    dto: UpdateProgramAccessUserDto,
    updatedBy: number,
    manager?: EntityManager,
  ): Promise<ProgramAccessUserMap> {
    try {
      const accessMapRepo = manager ? manager.getRepository(ProgramAccessUserMap) : this.accessMapRepo;

      // Find existing active mapping 
      const mapping = await accessMapRepo.findOne({
        where: {
          programId,
          userId,
          deletedAt: IsNull(),
          state: AccessStateEnum.ACTIVE,
        },
      });

      if (!mapping) {
        this.logger.error(`Access mapping not found for program ${programId} and user ${userId}`);
        throw new InifniNotFoundException(
          ERROR_CODES.PROGRAM_ACCESS_NOTFOUND,
          null,
          null,
          `No access mapping found for program ${programId} and user ${userId}`,
        );
      }

      // Update fields
      if (dto.accessScope !== undefined) {
        mapping.accessScope = dto.accessScope;
      }
      if (dto.state !== undefined) {
        mapping.state = dto.state;
      }
      if (dto.effectiveFrom !== undefined) {
        mapping.effectiveFrom = dto.effectiveFrom ? new Date(dto.effectiveFrom) : undefined;
      }
      if (dto.effectiveTill !== undefined) {
        mapping.effectiveTill = dto.effectiveTill ? new Date(dto.effectiveTill) : undefined;
      }
      if (dto.reason !== undefined) {
        mapping.reason = dto.reason ?? undefined;
      }
      if (dto.meta !== undefined) {
        mapping.meta = dto.meta ?? undefined;
      }
      mapping.updatedBy = updatedBy;

      const updated = await accessMapRepo.save(mapping);
      this.logger.log(programAccessRepositoryMessages.MAPPING_UPDATED(userId, programId));

      return updated;
    } catch (error) {
      this.logger.error(programAccessRepositoryMessages.ERROR_UPDATING_USER(error.message), error.stack);
      throw error;
    }
  }

  /**
   * Soft delete program access for a specific user
   */
  async removeProgramAccessUser(
    programId: number,
    userId: number,
    deletedBy: number,
    manager?: EntityManager,
  ): Promise<void> {
    try {
      const accessMapRepo = manager ? manager.getRepository(ProgramAccessUserMap) : this.accessMapRepo;

      // Find existing mapping
      const mapping = await accessMapRepo.findOne({
        where: {
          programId,
          userId,
          deletedAt: IsNull(),
          state: AccessStateEnum.ACTIVE,
        },
      });

      if (!mapping) {
        this.logger.error(`Access mapping not found for program ${programId} and user ${userId}`);
        throw new InifniNotFoundException(
          ERROR_CODES.PROGRAM_ACCESS_NOTFOUND,
          null,
          null,
          `No access mapping found for program ${programId} and user ${userId}`,
        );
      }

      // Soft delete
      mapping.state = AccessStateEnum.REMOVED;
      mapping.updatedBy = deletedBy;
      mapping.deletedAt = new Date();

      await accessMapRepo.save(mapping);
      this.logger.log(programAccessRepositoryMessages.MAPPING_REMOVED(userId, programId));
    } catch (error) {
      this.logger.error(programAccessRepositoryMessages.ERROR_REMOVING_USER(error.message), error.stack);
      throw error;
    }
  }

  /**
   * Get all users with access to a program
   */
  async getProgramAccessUsers(
    programId: number,
    query: QueryProgramAccessUsersDto,
  ): Promise<{ data: ProgramAccessUserMap[]; total: number }> {
    try {
      const { accessScope, state, includeDeleted, limit, offset } = query;

      const queryBuilder = this.accessMapRepo
        .createQueryBuilder('access')
        .leftJoinAndSelect('access.user', 'user')
        .where('access.programId = :programId', { programId });

      if (!includeDeleted) {
        queryBuilder.andWhere('access.deletedAt IS NULL');
      }

      if (state) {
        queryBuilder.andWhere('access.state = :state', { state });
      }

      if (accessScope) {
        queryBuilder.andWhere('access.accessScope = :accessScope', { accessScope });
      }

      queryBuilder.orderBy('access.createdAt', 'DESC').skip(offset).take(limit);

      const [data, total] = await queryBuilder.getManyAndCount();

      return { data, total };
    } catch (error) {
      this.logger.error(programAccessRepositoryMessages.ERROR_FETCHING_USERS(error.message), error.stack);
      throw error;
    }
  }

  /**
   * Check if a user has access to view a program
   * For PUBLIC programs, always returns true
   * For INTERNAL/RESTRICTED programs, checks if user has valid active mapping
   */
  async hasUserAccessToProgram(
    programId: number,
    userId: number,
    manager?: EntityManager,
  ): Promise<boolean> {
    try {
      const programRepo = manager ? manager.getRepository(Program) : this.programRepo;
      const accessMapRepo = manager ? manager.getRepository(ProgramAccessUserMap) : this.accessMapRepo;

      // Get program to check access type
      const program = await programRepo.findOne({ where: { id: programId, deletedAt: IsNull() } });
      if (!program) {
        return false;
      }

      // PUBLIC programs: all users can view
      if (program.accessType === ProgramAccessTypeEnum.PUBLIC) {
        return true;
      }

      // INTERNAL/RESTRICTED programs: check mapping
      const now = new Date();
      const mapping = await accessMapRepo.findOne({
        where: {
          programId,
          userId,
          state: AccessStateEnum.ACTIVE,
          deletedAt: IsNull(),
        },
      });

      if (!mapping) {
        return false;
      }

      // Check effective window
      if (mapping.effectiveFrom && mapping.effectiveFrom > now) {
        return false;
      }

      if (mapping.effectiveTill && mapping.effectiveTill < now) {
        return false;
      }

      return true;
    } catch (error) {
      this.logger.error(`Error checking user access to program: ${error.message}`, error.stack);
      return false;
    }
  }

  /**
   * Check if user can register for a program
   * For PUBLIC programs with open registration, all users can register
   * INTERNAL/RESTRICTED: only mapped users with VIEW_AND_REGISTER
   */
  async canUserRegisterForProgram(
    programId: number,
    userId: number,
    manager?: EntityManager,
  ): Promise<boolean> {
    try {
      const programRepo = manager ? manager.getRepository(Program) : this.programRepo;
      const accessMapRepo = manager ? manager.getRepository(ProgramAccessUserMap) : this.accessMapRepo;

      // Get program to check access type
      const program = await programRepo.findOne({ where: { id: programId, deletedAt: IsNull() } });
      if (!program) {
        return false;
      }

      // PUBLIC programs with open registration: all users can register
      if (program.accessType === ProgramAccessTypeEnum.PUBLIC) {
        return true;
      }

      // For all other cases, check mapping
      const now = new Date();
      const mapping = await accessMapRepo.findOne({
        where: {
          programId,
          userId,
          state: AccessStateEnum.ACTIVE,
          deletedAt: IsNull(),
        },
      });

      if (!mapping) {
        return false;
      }

      // Check access scope
      if (mapping.accessScope !== AccessScopeEnum.VIEW_AND_REGISTER) {
        return false;
      }

      // Check effective window
      if (mapping.effectiveFrom && mapping.effectiveFrom > now) {
        return false;
      }

      if (mapping.effectiveTill && mapping.effectiveTill < now) {
        return false;
      }

      return true;
    } catch (error) {
      this.logger.error(`Error checking user registration access: ${error.message}`, error.stack);
      return false;
    }
  }

  /**
   * Get user's access mapping for a program
   */
  async getUserAccessMapping(
    programId: number,
    userId: number,
    manager?: EntityManager,
  ): Promise<ProgramAccessUserMap | null> {
    try {
      const accessMapRepo = manager ? manager.getRepository(ProgramAccessUserMap) : this.accessMapRepo;

      const mapping = await accessMapRepo.findOne({
        where: {
          programId,
          userId,
          state: AccessStateEnum.ACTIVE,
          deletedAt: IsNull(),
        },
        relations: ['user'],
      });

      return mapping;
    } catch (error) {
      this.logger.error(programAccessRepositoryMessages.ERROR_CHECKING_ACCESS(error.message), error.stack);
      return null;
    }
  }

  /**
   * Bulk check access for multiple program-user pairs
   */
  async bulkCheckAccess(
    checks: Array<{ programId: number; userId: number }>,
    manager?: EntityManager,
  ): Promise<Map<string, boolean>> {
    try {
      const accessMapRepo = manager ? manager.getRepository(ProgramAccessUserMap) : this.accessMapRepo;

      const results = new Map<string, boolean>();
      const now = new Date();

      // Build query for all checks
      const mappings = await accessMapRepo.find({
        where: checks.map((check) => ({
          programId: check.programId,
          userId: check.userId,
          state: AccessStateEnum.ACTIVE,
          deletedAt: IsNull(),
        })),
      });

      // Process each check
      for (const check of checks) {
        const key = `${check.programId}-${check.userId}`;
        const mapping = mappings.find(
          (m) => m.programId === check.programId && m.userId === check.userId,
        );

        if (!mapping) {
          results.set(key, false);
          continue;
        }

        // Check effective window
        const isEffective =
          (!mapping.effectiveFrom || mapping.effectiveFrom <= now) &&
          (!mapping.effectiveTill || mapping.effectiveTill >= now);

        results.set(key, isEffective);
      }

      return results;
    } catch (error) {
      this.logger.error(`Error in bulk access check: ${error.message}`, error.stack);
      return new Map();
    }
  }

  /**
   * Bulk update program access for multiple users
   */
  async bulkUpdateProgramAccessUsers(
    programId: number,
    userIds: number[],
    updateData: {
      accessScope?: AccessScopeEnum;
      state?: AccessStateEnum;
      effectiveFrom?: Date;
      effectiveTill?: Date;
      reason?: string;
      meta?: Record<string, any>;
    },
    updatedBy: number,
    manager?: EntityManager,
  ): Promise<{ updated: number; failed: number; errors: string[] }> {
    try {
      const accessMapRepo = manager ? manager.getRepository(ProgramAccessUserMap) : this.accessMapRepo;
      
      let updated = 0;
      let failed = 0;
      const errors: string[] = [];

      for (const userId of userIds) {
        try {
          const mapping = await accessMapRepo.findOne({
            where: {
              programId,
              userId,
              deletedAt: IsNull(),
              state: AccessStateEnum.ACTIVE,
            },
          });

          if (!mapping) {
            failed++;
            errors.push(`No access mapping found for user ${userId}`);
            continue;
          }

          // Update fields
          if (updateData.accessScope !== undefined) {
            mapping.accessScope = updateData.accessScope;
          }
          if (updateData.state !== undefined) {
            mapping.state = updateData.state;
          }
          if (updateData.effectiveFrom !== undefined) {
            mapping.effectiveFrom = updateData.effectiveFrom;
          }
          if (updateData.effectiveTill !== undefined) {
            mapping.effectiveTill = updateData.effectiveTill;
          }
          if (updateData.reason !== undefined) {
            mapping.reason = updateData.reason ?? undefined;
          }
          if (updateData.meta !== undefined) {
            mapping.meta = updateData.meta ?? undefined;
          }
          mapping.updatedBy = updatedBy;

          await accessMapRepo.save(mapping);
          updated++;
        } catch (err) {
          failed++;
          errors.push(`Failed to update user ${userId}: ${err.message}`);
        }
      }

      this.logger.log(`Bulk update: ${updated} succeeded, ${failed} failed for program ${programId}`);
      return { updated, failed, errors };
    } catch (error) {
      this.logger.error(`Error in bulk update program access: ${error.message}`, error.stack);
      throw error;
    }
  }

  /**
   * Bulk remove program access for multiple users
   */
  async bulkRemoveProgramAccessUsers(
    programId: number,
    userIds: number[],
    reason: string | undefined,
    deletedBy: number,
    manager?: EntityManager,
  ): Promise<{ removed: number; failed: number; errors: string[] }> {
    try {
      const accessMapRepo = manager ? manager.getRepository(ProgramAccessUserMap) : this.accessMapRepo;
      
      let removed = 0;
      let failed = 0;
      const errors: string[] = [];

      for (const userId of userIds) {
        try {
          const mapping = await accessMapRepo.findOne({
            where: {
              programId,
              userId,
              deletedAt: IsNull(),
              state: AccessStateEnum.ACTIVE,
            },
          });

          if (!mapping) {
            failed++;
            errors.push(`No access mapping found for user ${userId}`);
            continue;
          }

          // Soft delete
          mapping.state = AccessStateEnum.REMOVED;
          mapping.updatedBy = deletedBy;
          mapping.deletedAt = new Date();
          if (reason) {
            mapping.reason = reason;
          }

          await accessMapRepo.save(mapping);
          removed++;
        } catch (err) {
          failed++;
          errors.push(`Failed to remove user ${userId}: ${err.message}`);
        }
      }

      this.logger.log(`Bulk remove: ${removed} succeeded, ${failed} failed for program ${programId}`);
      return { removed, failed, errors };
    } catch (error) {
      this.logger.error(`Error in bulk remove program access: ${error.message}`, error.stack);
      throw error;
    }
  }

  /**
   * Get only active user IDs with access to a program
   * @param programId - Program ID
   * @param manager - Optional entity manager for transaction
   * @returns Array of user IDs with active access
   */
  async getProgramAccessUserIds(
    programId: number,
    manager?: EntityManager,
  ): Promise<number[]> {
    try {
      const accessMapRepo = manager ? manager.getRepository(ProgramAccessUserMap) : this.accessMapRepo;

      const accessMaps = await accessMapRepo.find({
        where: {
          programId,
          state: AccessStateEnum.ACTIVE,
          deletedAt: IsNull(),
        },
        select: ['userId'],
      });

      return accessMaps.map((map) => map.userId);
    } catch (error) {
      this.logger.error(`Error fetching program access user IDs for program ${programId}`, error.stack);
      return [];
    }
  }
}
