package com.saas.admin.service.impl;

import com.saas.admin.entity.Role;
import com.saas.admin.entity.User;
import com.saas.admin.entity.UserRole;
import com.saas.admin.repository.RoleRepository;
import com.saas.admin.repository.UserRepository;
import com.saas.admin.repository.UserRoleRepository;
import com.saas.shared.exception.BusinessException;
import com.saas.shared.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Additional RBAC operations with ID-based methods.
 * 
 * Manages role-based access control operations using UserRole junction table.
 * All operations are transactional and auditable.
 * 
 * NOTE: This is NOT an implementation of RBACService interface,
 * but a complementary service providing ID-based methods.
 */
@Service("rbacServiceImpl")
@Slf4j
@RequiredArgsConstructor
@Transactional
public class RBACServiceImpl {

    private final RoleRepository roleRepository;
    private final UserRepository userRepository;
    private final UserRoleRepository userRoleRepository;

    public Role createRole(String roleName, String description) {
        log.debug("Creating role: {}", roleName);

        if (roleRepository.findByName(roleName).isPresent()) {
            log.warn("Role already exists: {}", roleName);
            throw new BusinessException(
                ErrorCode.INVALID_INPUT,
                "Role '" + roleName + "' already exists"
            );
        }

        Role role = new Role();
        role.setName(roleName);
        role.setDescription(description);
        role.setIsActive(true);

        Role savedRole = roleRepository.save(role);
        log.info("Role created successfully: {} (ID: {})", roleName, savedRole.getId());

        return savedRole;
    }

    public User assignRolesToUser(Long userId, Set<Long> roleIds) {
        log.debug("Assigning {} roles to user: {}", roleIds.size(), userId);

        User user = userRepository.findById(userId)
            .orElseThrow(() -> {
                log.warn("User not found for role assignment: {}", userId);
                return new BusinessException(
                    ErrorCode.USER_NOT_FOUND,
                    "User with ID " + userId + " does not exist"
                );
            });

        // Fetch all roles
        Set<Role> rolesToAssign = new HashSet<>(roleRepository.findAllById(roleIds));

        if (rolesToAssign.size() != roleIds.size()) {
            log.warn("Some roles not found for user: {}, requested: {}, found: {}", userId, roleIds.size(), rolesToAssign.size());
            throw new BusinessException(
                ErrorCode.INVALID_INPUT,
                "One or more roles do not exist"
            );
        }

        // Get existing role IDs for this user
        Set<Long> existingRoleIds = user.getUserRoles().stream()
            .map(ur -> ur.getRole().getId())
            .collect(Collectors.toSet());

        // Add new roles (avoiding duplicates)
        for (Role role : rolesToAssign) {
            if (!existingRoleIds.contains(role.getId())) {
                UserRole userRole = UserRole.builder()
                    .user(user)
                    .role(role)
                    .build();
                userRoleRepository.save(userRole);
                log.debug("Added role '{}' to user: {}", role.getName(), userId);
            }
        }

        // Refresh user to get updated userRoles
        User updatedUser = userRepository.findById(userId).orElseThrow();
        log.info("Roles assigned to user {}: {}", userId, roleIds);

        return updatedUser;
    }

    public User removeRoleFromUser(Long userId, Long roleId) {
        log.debug("Removing role {} from user: {}", roleId, userId);

        User user = userRepository.findById(userId)
            .orElseThrow(() -> {
                log.warn("User not found for role removal: {}", userId);
                return new BusinessException(
                    ErrorCode.USER_NOT_FOUND,
                    "User with ID " + userId + " does not exist"
                );
            });

        Role role = roleRepository.findById(roleId)
            .orElseThrow(() -> {
                log.warn("Role not found for removal: {}", roleId);
                return new BusinessException(
                    ErrorCode.INVALID_INPUT,
                    "Role with ID " + roleId + " does not exist"
                );
            });

        // Find and delete the UserRole junction entry
        UserRole userRole = userRoleRepository.findByUserIdAndRoleId(userId, roleId)
            .orElseThrow(() -> {
                log.warn("User {} does not have role: {}", userId, roleId);
                return new BusinessException(
                    ErrorCode.INVALID_INPUT,
                    "User does not have this role assigned"
                );
            });

        userRoleRepository.delete(userRole);
        log.info("Role {} removed from user: {}", roleId, userId);

        // Refresh user to get updated userRoles
        return userRepository.findById(userId).orElseThrow();
    }

    @Transactional(readOnly = true)
    public Set<Role> getUserRoles(Long userId) {
        log.debug("Fetching roles for user: {}", userId);

        User user = userRepository.findById(userId)
            .orElseThrow(() -> {
                log.warn("User not found for role query: {}", userId);
                return new BusinessException(
                    ErrorCode.USER_NOT_FOUND,
                    "User with ID " + userId + " does not exist"
                );
            });

        // Extract roles from UserRole junction table
        return user.getUserRoles().stream()
            .map(UserRole::getRole)
            .collect(Collectors.toSet());
    }

    @Transactional(readOnly = true)
    public boolean hasRole(Long userId, String roleName) {
        log.debug("Checking if user {} has role: {}", userId, roleName);

        return userRepository.findById(userId)
            .map(user -> user.getUserRoles().stream()
                .anyMatch(ur -> ur.getRole().getName().equals(roleName)))
            .orElse(false);
    }

    @Transactional(readOnly = true)
    public boolean hasAnyRole(Long userId, Set<String> roleNames) {
        log.debug("Checking if user {} has any role in: {}", userId, roleNames);

        return userRepository.findById(userId)
            .map(user -> user.getUserRoles().stream()
                .map(UserRole::getRole)
                .anyMatch(role -> roleNames.contains(role.getName())))
            .orElse(false);
    }
}
