import { Repository, In } from 'typeorm';
import { AppDataSource } from '../../config/typeorm.js';
import { Role, Permission } from '../auth/entities/index.js';
import { PaginationParams, NotFoundError, ValidationError, ForbiddenError } from '../../shared/types/index.js';
import { logger } from '../../shared/utils/logger.js';

// ===== Interfaces =====

export interface CreateRoleDto {
  name: string;
  code: string;
  description?: string;
  color?: string;
  permissionIds?: string[];
}

export interface UpdateRoleDto {
  name?: string;
  description?: string;
  color?: string;
}

export interface RoleWithPermissions extends Role {
  permissions: Permission[];
}

// ===== RolesService Class =====

class RolesService {
  private roleRepository: Repository<Role>;
  private permissionRepository: Repository<Permission>;

  constructor() {
    this.roleRepository = AppDataSource.getRepository(Role);
    this.permissionRepository = AppDataSource.getRepository(Permission);
  }

  /**
   * Get all roles for a tenant with pagination
   */
  async findAll(
    tenantId: string,
    params: PaginationParams
  ): Promise<{ roles: Role[]; total: number }> {
    try {
      const { page, limit, sortBy = 'name', sortOrder = 'asc' } = params;
      const skip = (page - 1) * limit;

      const queryBuilder = this.roleRepository
        .createQueryBuilder('role')
        .leftJoinAndSelect('role.permissions', 'permissions')
        .where('role.tenantId = :tenantId', { tenantId })
        .andWhere('role.deletedAt IS NULL')
        .orderBy(`role.${sortBy}`, sortOrder.toUpperCase() as 'ASC' | 'DESC')
        .skip(skip)
        .take(limit);

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

      logger.debug('Roles retrieved', { tenantId, count: roles.length, total });

      return { roles, total };
    } catch (error) {
      logger.error('Error retrieving roles', {
        error: (error as Error).message,
        tenantId,
      });
      throw error;
    }
  }

  /**
   * Get a specific role by ID
   */
  async findById(tenantId: string, roleId: string): Promise<RoleWithPermissions> {
    try {
      const role = await this.roleRepository.findOne({
        where: {
          id: roleId,
          tenantId,
          deletedAt: undefined,
        },
        relations: ['permissions'],
      });

      if (!role) {
        throw new NotFoundError('Rol no encontrado');
      }

      return role as RoleWithPermissions;
    } catch (error) {
      logger.error('Error finding role', {
        error: (error as Error).message,
        tenantId,
        roleId,
      });
      throw error;
    }
  }

  /**
   * Get a role by code
   */
  async findByCode(tenantId: string, code: string): Promise<Role | null> {
    try {
      return await this.roleRepository.findOne({
        where: {
          code,
          tenantId,
          deletedAt: undefined,
        },
        relations: ['permissions'],
      });
    } catch (error) {
      logger.error('Error finding role by code', {
        error: (error as Error).message,
        tenantId,
        code,
      });
      throw error;
    }
  }

  /**
   * Create a new role
   */
  async create(
    tenantId: string,
    data: CreateRoleDto,
    createdBy: string
  ): Promise<Role> {
    try {
      // Validate code uniqueness within tenant
      const existing = await this.findByCode(tenantId, data.code);
      if (existing) {
        throw new ValidationError('Ya existe un rol con este código');
      }

      // Validate code format
      if (!/^[a-z_]+$/.test(data.code)) {
        throw new ValidationError('El código debe contener solo letras minúsculas y guiones bajos');
      }

      // Create role
      const role = this.roleRepository.create({
        tenantId,
        name: data.name,
        code: data.code,
        description: data.description || null,
        color: data.color || null,
        isSystem: false,
        createdBy,
      });

      await this.roleRepository.save(role);

      // Assign initial permissions if provided
      if (data.permissionIds && data.permissionIds.length > 0) {
        await this.assignPermissions(tenantId, role.id, data.permissionIds, createdBy);
      }

      // Reload with permissions
      const savedRole = await this.findById(tenantId, role.id);

      logger.info('Role created', {
        roleId: role.id,
        tenantId,
        code: role.code,
        createdBy,
      });

      return savedRole;
    } catch (error) {
      logger.error('Error creating role', {
        error: (error as Error).message,
        tenantId,
        data,
      });
      throw error;
    }
  }

  /**
   * Update a role
   */
  async update(
    tenantId: string,
    roleId: string,
    data: UpdateRoleDto,
    updatedBy: string
  ): Promise<Role> {
    try {
      const role = await this.findById(tenantId, roleId);

      // Prevent modification of system roles
      if (role.isSystem) {
        throw new ForbiddenError('No se pueden modificar roles del sistema');
      }

      // Update allowed fields
      if (data.name !== undefined) role.name = data.name;
      if (data.description !== undefined) role.description = data.description;
      if (data.color !== undefined) role.color = data.color;
      role.updatedBy = updatedBy;
      role.updatedAt = new Date();

      await this.roleRepository.save(role);

      logger.info('Role updated', {
        roleId,
        tenantId,
        updatedBy,
      });

      return await this.findById(tenantId, roleId);
    } catch (error) {
      logger.error('Error updating role', {
        error: (error as Error).message,
        tenantId,
        roleId,
      });
      throw error;
    }
  }

  /**
   * Soft delete a role
   */
  async delete(tenantId: string, roleId: string, deletedBy: string): Promise<void> {
    try {
      const role = await this.findById(tenantId, roleId);

      // Prevent deletion of system roles
      if (role.isSystem) {
        throw new ForbiddenError('No se pueden eliminar roles del sistema');
      }

      // Check if role has users assigned
      const usersCount = await this.roleRepository
        .createQueryBuilder('role')
        .leftJoin('role.users', 'user')
        .where('role.id = :roleId', { roleId })
        .andWhere('user.deletedAt IS NULL')
        .getCount();

      if (usersCount > 0) {
        throw new ValidationError(
          `No se puede eliminar el rol porque tiene ${usersCount} usuario(s) asignado(s)`
        );
      }

      // Soft delete
      role.deletedAt = new Date();
      role.deletedBy = deletedBy;

      await this.roleRepository.save(role);

      logger.info('Role deleted', {
        roleId,
        tenantId,
        deletedBy,
      });
    } catch (error) {
      logger.error('Error deleting role', {
        error: (error as Error).message,
        tenantId,
        roleId,
      });
      throw error;
    }
  }

  /**
   * Assign permissions to a role
   */
  async assignPermissions(
    tenantId: string,
    roleId: string,
    permissionIds: string[],
    updatedBy: string
  ): Promise<Role> {
    try {
      const role = await this.findById(tenantId, roleId);

      // Prevent modification of system roles
      if (role.isSystem) {
        throw new ForbiddenError('No se pueden modificar permisos de roles del sistema');
      }

      // Validate all permissions exist
      const permissions = await this.permissionRepository.find({
        where: { id: In(permissionIds) },
      });

      if (permissions.length !== permissionIds.length) {
        throw new ValidationError('Uno o más permisos no existen');
      }

      // Replace permissions
      role.permissions = permissions;
      role.updatedBy = updatedBy;
      role.updatedAt = new Date();

      await this.roleRepository.save(role);

      logger.info('Role permissions updated', {
        roleId,
        tenantId,
        permissionCount: permissions.length,
        updatedBy,
      });

      return await this.findById(tenantId, roleId);
    } catch (error) {
      logger.error('Error assigning permissions', {
        error: (error as Error).message,
        tenantId,
        roleId,
      });
      throw error;
    }
  }

  /**
   * Add a single permission to a role
   */
  async addPermission(
    tenantId: string,
    roleId: string,
    permissionId: string,
    updatedBy: string
  ): Promise<Role> {
    try {
      const role = await this.findById(tenantId, roleId);

      if (role.isSystem) {
        throw new ForbiddenError('No se pueden modificar permisos de roles del sistema');
      }

      // Check if permission exists
      const permission = await this.permissionRepository.findOne({
        where: { id: permissionId },
      });

      if (!permission) {
        throw new NotFoundError('Permiso no encontrado');
      }

      // Check if already assigned
      const hasPermission = role.permissions.some(p => p.id === permissionId);
      if (hasPermission) {
        throw new ValidationError('El permiso ya está asignado a este rol');
      }

      // Add permission
      role.permissions.push(permission);
      role.updatedBy = updatedBy;
      role.updatedAt = new Date();

      await this.roleRepository.save(role);

      logger.info('Permission added to role', {
        roleId,
        permissionId,
        tenantId,
        updatedBy,
      });

      return await this.findById(tenantId, roleId);
    } catch (error) {
      logger.error('Error adding permission', {
        error: (error as Error).message,
        tenantId,
        roleId,
        permissionId,
      });
      throw error;
    }
  }

  /**
   * Remove a permission from a role
   */
  async removePermission(
    tenantId: string,
    roleId: string,
    permissionId: string,
    updatedBy: string
  ): Promise<Role> {
    try {
      const role = await this.findById(tenantId, roleId);

      if (role.isSystem) {
        throw new ForbiddenError('No se pueden modificar permisos de roles del sistema');
      }

      // Filter out the permission
      const initialLength = role.permissions.length;
      role.permissions = role.permissions.filter(p => p.id !== permissionId);

      if (role.permissions.length === initialLength) {
        throw new NotFoundError('El permiso no está asignado a este rol');
      }

      role.updatedBy = updatedBy;
      role.updatedAt = new Date();

      await this.roleRepository.save(role);

      logger.info('Permission removed from role', {
        roleId,
        permissionId,
        tenantId,
        updatedBy,
      });

      return await this.findById(tenantId, roleId);
    } catch (error) {
      logger.error('Error removing permission', {
        error: (error as Error).message,
        tenantId,
        roleId,
        permissionId,
      });
      throw error;
    }
  }

  /**
   * Get all permissions for a role
   */
  async getRolePermissions(tenantId: string, roleId: string): Promise<Permission[]> {
    const role = await this.findById(tenantId, roleId);
    return role.permissions;
  }

  /**
   * Get system roles (super_admin, admin, etc.)
   */
  async getSystemRoles(tenantId: string): Promise<Role[]> {
    return await this.roleRepository.find({
      where: {
        tenantId,
        isSystem: true,
        deletedAt: undefined,
      },
      relations: ['permissions'],
    });
  }
}

// ===== Export Singleton Instance =====

export const rolesService = new RolesService();
