import { InjectRepository } from '@nestjs/typeorm';
import { BadRequestException, ConflictException, forwardRef, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { In, Repository } from 'typeorm';

import { PermissionEntity } from './entities/permission.entity';
import { BasePermissionService } from './abstract/permission.abstract';
import { CreatePermissionDto } from './dto/create-permission.dto';
import {
  mapPermissionResponseDto,
  mapPermissionsResponseDto,
  PaginatedPermissionResponseDto,
  PermissionResponseDto,
} from './dto/permission-response.dto';
import { UpdatePermissionDto } from './dto/update-permission.dto';
import { RoleService } from '../roles/role.service';
import { SubPermissionEntity } from './entities/sub-permission.entity';
import { CreateSubPermissionDto } from './dto/create-sub-permisison.dto';
import { UpdateSubPermissionDto } from './dto/update-sub-permission.dto';

@Injectable()
export class PermissionService extends BasePermissionService {
  private readonly logger = new Logger(PermissionService.name);
  constructor(
    @InjectRepository(PermissionEntity)
    private readonly permissionRepository: Repository<PermissionEntity>,
    @InjectRepository(SubPermissionEntity)
    private readonly subPermissionRepository: Repository<SubPermissionEntity>,
    @Inject(forwardRef(() => RoleService))
    private readonly roleService: RoleService,
  ) {
    super();
  }

  async createPermission(createPermissionDto: CreatePermissionDto): Promise<PermissionResponseDto> {
    try {
      // Check if permission with same name already exists
      const existingPermission = await this.permissionRepository.findOne({
        where: { name: createPermissionDto.name },
      });

      if (existingPermission) {
        throw new ConflictException(`Permission with name '${createPermissionDto.name}' already exists`);
      }

      const permission = this.permissionRepository.create(createPermissionDto);
      const savedPermission = await this.permissionRepository.save(permission);

      return mapPermissionResponseDto(savedPermission);
    } catch (error) {
      this.logger.error(`Error creating permission: ${error.message}`, error.stack);
      if (error instanceof ConflictException) {
        throw error; // Re-throw conflict exceptions
      }
      throw new BadRequestException('Failed to create permission: ' + error.message);
    }
  }

  async updatePermission(permissionId: string, updatePermissionDto: UpdatePermissionDto): Promise<PermissionResponseDto> {
    try {
      const permission = await this.permissionRepository.findOne({
        where: { id: permissionId },
        relations: ['rolePermissionStates'],
      });

      if (!permission) {
        throw new NotFoundException(`Permission with ID ${permissionId} not found`);
      }

      // Check if it's a system permission and prevent critical updates
      if (permission.isSystemPermission && (updatePermissionDto.name || updatePermissionDto.permissionType)) {
        throw new BadRequestException('Cannot modify name or type of system permissions');
      }

      // Check for name conflicts if name is being updated
      if (updatePermissionDto.name && updatePermissionDto.name !== permission.name) {
        const existingPermission = await this.permissionRepository.findOne({
          where: { name: updatePermissionDto.name },
        });

        if (existingPermission) {
          throw new ConflictException(`Permission with name '${updatePermissionDto.name}' already exists`);
        }
      }

      Object.assign(permission, updatePermissionDto);
      const updatedPermission = await this.permissionRepository.save(permission);

      return mapPermissionResponseDto(updatedPermission);
    } catch (error) {
      this.logger.error(`Error updating permission: ${error.message}`, error.stack);
      if (error instanceof NotFoundException || error instanceof ConflictException) {
        throw error; // Re-throw specific exceptions
      }
      throw new BadRequestException('Failed to update permission: ' + error.message);
    }
  }

  async deletePermission(permissionId: string): Promise<void> {
    try {
      const permission = await this.permissionRepository.findOne({
        where: { id: permissionId },
        relations: ['rolePermissionStates'],
      });

      if (!permission) {
        throw new NotFoundException(`Permission with ID ${permissionId} not found`);
      }

      // Prevent deletion of system permissions
      if (permission.isSystemPermission) {
        throw new BadRequestException('Cannot delete system permissions');
      }      

      await this.permissionRepository.remove(permission);
    } catch (error) {
      this.logger.error(`Error deleting permission: ${error.message}`, error.stack);
      if (error instanceof NotFoundException || error instanceof BadRequestException) {
        throw error; // Re-throw specific exceptions
      }
      throw new BadRequestException('Failed to delete permission: ' + error.message);
    }
  }

  async findAllClientPermissions(): Promise<PermissionResponseDto[]> {
    try {
      const permissions = await this.permissionRepository.find({
        where: { isActive: true, isSystemPermission: false },
        relations: ['rolePermissionStates'],
        order: { createdAt: 'DESC' },
      });

      if (permissions.length === 0) {
        throw new NotFoundException('No permissions found');
      }

      return mapPermissionsResponseDto(permissions);
    } catch (error) {
      this.logger.error(`Error fetching permissions: ${error.message}`, error.stack);
      throw new BadRequestException('Failed to fetch permissions: ' + error.message);
    }
  }

  async findPermissionById(permissionId: string): Promise<PermissionResponseDto> {
    try {
      const permission = await this.permissionRepository.findOne({
        where: { id: permissionId },
        relations: ['rolePermissionStates'],
      });

      if (!permission) {
        throw new NotFoundException(`Permission with ID ${permissionId} not found`);
      }

      return mapPermissionResponseDto(permission);
    } catch (error) {
      this.logger.error(`Error fetching permission by ID: ${error.message}`, error.stack);
      if (error instanceof NotFoundException) {
        throw error; // Re-throw not found exceptions
      }
      throw new BadRequestException('Failed to fetch permission: ' + error.message);
    }
  }

  async findPermissionsByIds(permissionIds: string[]): Promise<PermissionResponseDto[]> {
    try {
      const permissions = await this.permissionRepository.find({
        where: { id: In(permissionIds) },
        relations: ['rolePermissionStates', 'rolePermissionStates.role'],
      });
      if (permissions.length === 0) {
        throw new NotFoundException(`No permissions found for IDs: ${permissionIds.join(', ')}`);
      }
      return mapPermissionsResponseDto(permissions);
    } catch (error) {
      this.logger.error(`Error fetching permissions by IDs: ${error.message}`, error.stack);
      if (error instanceof NotFoundException) {
        throw error; // Re-throw not found exceptions
      }
      throw new BadRequestException('Failed to fetch permissions: ' + error.message);
    }
  }

  // NOTE: This method is similar to findPermissionsByIds but returns entities instead of DTOs
  // It can be used internally where entities are needed, while the public API uses DTOs
  async findPermissionsEntitysByIds(permissionIds: string[]): Promise<PermissionEntity[]> {
    try {
      const permissions = await this.permissionRepository.find({
        where: { id: In(permissionIds) },
        relations: ['rolePermissionStates', 'rolePermissionStates.role'],
      });
      if (permissions.length === 0) {
        throw new NotFoundException(`No permissions found for IDs: ${permissionIds.join(', ')}`);
      }
      return permissions;
    } catch (error) {
      this.logger.error(`Error fetching permissions by IDs: ${error.message}`, error.stack);
      if (error instanceof NotFoundException) {
        throw error; // Re-throw not found exceptions
      }
      throw new BadRequestException('Failed to fetch permissions: ' + error.message);
    }
  }

  async findPermissionByName(permissionName: string): Promise<PermissionResponseDto> {
    try {
      const permission = await this.permissionRepository.findOne({
        where: { name: permissionName },
        relations: ['rolePermissionStates', 'rolePermissionStates.role'],
      });

      if (!permission) {
        throw new NotFoundException(`Permission with name '${permissionName}' not found`);
      }

      return mapPermissionResponseDto(permission);
    } catch (error) {
      this.logger.error(`Error fetching permission by name: ${error.message}`, error.stack);
      if (error instanceof NotFoundException) {
        throw error; // Re-throw not found exceptions
      }
      throw new BadRequestException('Failed to fetch permission: ' + error.message);
    }
  }

  async removePermissionsFromRole(roleId: string, permissionIds: string[]): Promise<void> {
    try {
      const role = await this.roleService.findRoleById(roleId);

      if (!role) {
        throw new NotFoundException(`Role with ID ${roleId} not found`);
      }

      if (!role.permissions) {
        role.permissions = [];
      }

      // Remove specified permissions from role
      role.permissions = role.permissions.filter(permission => !permissionIds.includes(permission.id));

      // Save the updated role
      await this.roleService.updateRole(roleId, {});
    } catch (error) {
      this.logger.error(`Error removing permissions from role: ${error.message}`, error.stack);
      if (error instanceof NotFoundException) {
        throw error; // Re-throw not found exceptions
      }
      throw new BadRequestException('Failed to remove permissions from role: ' + error.message);
    }
  }

  async getPermissionsByRoleId(roleId: string, page: number, limit: number): Promise<PaginatedPermissionResponseDto> {
    try {
      const role = await this.roleService.findRoleById(roleId);

      if (!role) {
        throw new NotFoundException(`Role with ID ${roleId} not found`);
      }

      const permissions = role.permissions;
      const total = permissions.length;
      const items = role.permissions;
      const totalPages = Math.ceil(total / limit);
      return {
        items,
        totalItems: total,
        currentPage: page,
        itemsPerPage: limit,
        totalPages,
      };
    } catch (error) {
      this.logger.error(`Error fetching permissions by role ID: ${error.message}`, error.stack);
      if (error instanceof NotFoundException) {
        throw error; // Re-throw not found exceptions
      }
      throw new BadRequestException('Failed to fetch permissions by role ID: ' + error.message);
    }
  }

  async getPermissionsByRoleName(roleName: string, page: number, limit: number): Promise<PaginatedPermissionResponseDto> {
    try {
      const role = await this.roleService.findRoleByName(roleName);

      if (!role) {
        throw new NotFoundException(`Role with name '${roleName}' not found`);
      }

      const permissions = role.permissions || [];
      const activePermissions = permissions.filter(p => p.isActive);

      // Apply pagination
      const startIndex = (page - 1) * limit;
      const endIndex = startIndex + limit;
      const paginatedPermissions = activePermissions.slice(startIndex, endIndex);

      const total = activePermissions.length;
      const items = paginatedPermissions;
      const totalPages = Math.ceil(total / limit);
      return {
        items,
        totalItems: total,
        currentPage: page,
        itemsPerPage: limit,
        totalPages,
      };
    } catch (error) {
      this.logger.error(`Error fetching permissions by role name: ${error.message}`, error.stack);
      if (error instanceof NotFoundException) {
        throw error; // Re-throw not found exceptions
      }
      throw new BadRequestException('Failed to fetch permissions by role name: ' + error.message);
    }
  }

  // create sub-permission
  async createSubPermission(createSubPermissionDto: CreateSubPermissionDto): Promise<PermissionResponseDto> {
    try {
      // Check if sub-permission with same name already exists
      const existingSubPermission = await this.subPermissionRepository.findOne({
        where: { name: createSubPermissionDto.name },
      });

      if (existingSubPermission) {
        throw new ConflictException(`Sub-permission with name '${createSubPermissionDto.name}' already exists`);
      }

      // Find the parent permission
      const permission = await this.permissionRepository.findOne({
        where: { id: createSubPermissionDto.permissionId },
      });

      if (!permission) {
        throw new NotFoundException(`Parent permission with ID ${createSubPermissionDto.permissionId} not found`);
      }

      const subPermission = this.subPermissionRepository.create({
        name: createSubPermissionDto.name,
        description: createSubPermissionDto.description,
        isActive: createSubPermissionDto.isActive,
        displayName: createSubPermissionDto.displayName,
        permissionType: createSubPermissionDto.permissionType,
        permission,
        permissionId: createSubPermissionDto.permissionId,
      });
      subPermission.permission = permission; // Set the parent permission

      const savedSubPermission = await this.subPermissionRepository.save(subPermission);
      this.logger.log(`Sub-permission created successfully: ${savedSubPermission.name}`);
      // Return the saved sub-permission as a PermissionResponseDto
      const updatedPermission = await this.permissionRepository.findOne({
        where: { id: createSubPermissionDto.permissionId },
      });
      if (!updatedPermission) {
        throw new NotFoundException(`Parent permission with ID ${createSubPermissionDto.permissionId} not found after creation`);
      }
      return mapPermissionResponseDto(updatedPermission);
    } catch (error) {
      this.logger.error(`Error creating sub-permission: ${error.message}`, error.stack);
      if (error instanceof ConflictException || error instanceof NotFoundException) {
        throw error; // Re-throw specific exceptions
      }
      throw new BadRequestException('Failed to create sub-permission: ' + error.message);
    }
  }

  async updateSubPermission(subPermissionId: string, updateSubPermissionDto: UpdateSubPermissionDto): Promise<PermissionResponseDto> {
    try {
      const subPermission = await this.subPermissionRepository.findOne({
        where: { id: subPermissionId },
        relations: ['permission'],
      });

      if (!subPermission) {
        throw new NotFoundException(`Sub-permission with ID ${subPermissionId} not found`);
      }

      // Check for name conflicts if name is being updated
      if (updateSubPermissionDto.name && updateSubPermissionDto.name !== subPermission.name) {
        const existingSubPermission = await this.subPermissionRepository.findOne({
          where: { name: updateSubPermissionDto.name },
        });

        if (existingSubPermission) {
          throw new ConflictException(`Sub-permission with name '${updateSubPermissionDto.name}' already exists`);
        }
      }

      Object.assign(subPermission, updateSubPermissionDto);
      const updatedSubPermission = await this.subPermissionRepository.save(subPermission);

      this.logger.log(`Sub-permission updated successfully: ${updatedSubPermission.name}`);
      // Return the updated sub-permission as a PermissionResponseDto
      const updatedPermission = await this.permissionRepository.findOne({
        where: { id: subPermission.permission.id },
      });
      if (!updatedPermission) {
        throw new NotFoundException(`Parent permission with ID ${subPermission.permission.id} not found after update`);
      }
      return mapPermissionResponseDto(updatedPermission);
    } catch (error) {
      this.logger.error(`Error updating sub-permission: ${error.message}`, error.stack);
      if (error instanceof NotFoundException || error instanceof ConflictException) {
        throw error; // Re-throw specific exceptions
      }
      throw new BadRequestException('Failed to update sub-permission: ' + error.message);
    }
  }

  async deleteSubPermission(subPermissionId: string): Promise<void> {
    try {
      const subPermission = await this.subPermissionRepository.findOne({
        where: { id: subPermissionId },
        relations: ['permission'],
      });

      if (!subPermission) {
        throw new NotFoundException(`Sub-permission with ID ${subPermissionId} not found`);
      }

      await this.subPermissionRepository.remove(subPermission);
    } catch (error) {
      this.logger.error(`Error deleting sub-permission: ${error.message}`, error.stack);
      if (error instanceof NotFoundException || error instanceof BadRequestException) {
        throw error; // Re-throw specific exceptions
      }
      throw new BadRequestException('Failed to delete sub-permission: ' + error.message);
    }
  }

  async findSubPermissionById(subPermissionId: string): Promise<PermissionResponseDto> {
    try {
      const subPermission = await this.subPermissionRepository.findOne({
        where: { id: subPermissionId },
        relations: ['permission'],
      });

      if (!subPermission) {
        throw new NotFoundException(`Sub-permission with ID ${subPermissionId} not found`);
      }

      return mapPermissionResponseDto(subPermission.permission);
    } catch (error) {
      this.logger.error(`Error fetching sub-permission by ID: ${error.message}`, error.stack);
      if (error instanceof NotFoundException) {
        throw error; // Re-throw not found exceptions
      }
      throw new BadRequestException('Failed to fetch sub-permission: ' + error.message);
    }
  }

  async findSubPermissionByIds(subPermissionIds: string[]): Promise<SubPermissionEntity[]> {
    try {
      const subPermissions = await this.subPermissionRepository.find({
        where: { id: In(subPermissionIds) },
        // relations: ['permission'],
      });

      if (subPermissions.length === 0) {
        throw new NotFoundException(`No sub-permissions found for IDs: ${subPermissionIds.join(', ')}`);
      }

      return subPermissions;
    } catch (error) {
      this.logger.error(`Error fetching sub-permissions by IDs: ${error.message}`, error.stack);
      if (error instanceof NotFoundException) {
        throw error; // Re-throw not found exceptions
      }
      throw new BadRequestException('Failed to fetch sub-permissions: ' + error.message);
    }
  }

  async findSubPermissionByName(subPermissionName: string): Promise<PermissionResponseDto> {
    try {
      const subPermission = await this.subPermissionRepository.findOne({
        where: { name: subPermissionName },
        relations: ['permission'],
      });

      if (!subPermission) {
        throw new NotFoundException(`Sub-permission with name '${subPermissionName}' not found`);
      }

      return mapPermissionResponseDto(subPermission.permission);
    } catch (error) {
      this.logger.error(`Error fetching sub-permission by name: ${error.message}`, error.stack);
      if (error instanceof NotFoundException) {
        throw error; // Re-throw not found exceptions
      }
      throw new BadRequestException('Failed to fetch sub-permission: ' + error.message);
    }
  }

  async findAllSubPermissions(): Promise<SubPermissionEntity[]> {
    try {
      const subPermissions = await this.subPermissionRepository.find({
        relations: ['permission'],
        // order: { createdAt: 'DESC' },
      });

      if (subPermissions.length === 0) {
        throw new NotFoundException('No sub-permissions found');
      }

      return subPermissions;
    } catch (error) {
      this.logger.error(`Error fetching sub-permissions: ${error.message}`, error.stack);
      throw new BadRequestException('Failed to fetch sub-permissions: ' + error.message);
    }
  }

  async removeSubPermissionsFromRole(roleId: string, subPermissionIds: string[]): Promise<void> {
    try {
      const role = await this.roleService.findRoleById(roleId);

      if (!role) {
        throw new NotFoundException(`Role with ID ${roleId} not found`);
      }

      if (!role.permissions) {
        role.permissions = [];
      }

      // Remove specified sub-permissions from role
      role.permissions = role.permissions.filter(subPermission => !subPermissionIds.includes(subPermission.id.toString()));

      // Save the updated role
      await this.roleService.updateRole(roleId, {});
    } catch (error) {
      this.logger.error(`Error removing sub-permissions from role: ${error.message}`, error.stack);
      if (error instanceof NotFoundException) {
        throw error; // Re-throw not found exceptions
      }
      throw new BadRequestException('Failed to remove sub-permissions from role: ' + error.message);
    }
  }

  async getSubPermissionsByPermissionId(permissionId: string): Promise<SubPermissionEntity[]> {
    try {
      const permission = await this.permissionRepository.findOne({
        where: { id: permissionId },
        relations: ['subPermissions'],
      });

      if (!permission) {
        throw new NotFoundException(`Permission with ID ${permissionId} not found`);
      }

      if (!permission.subPermissions || permission.subPermissions.length === 0) {
        throw new NotFoundException(`No sub-permissions found for permission with ID ${permissionId}`);
      }

      return permission.subPermissions;
    } catch (error) {
      this.logger.error(`Error fetching sub-permissions by permission ID: ${error.message}`, error.stack);
      if (error instanceof NotFoundException) {
        throw error; // Re-throw not found exceptions
      }
      throw new BadRequestException('Failed to fetch sub-permissions by permission ID: ' + error.message);
    }
  }
}
