package com.saas.admin.service;

import com.saas.admin.entity.Permission;
import com.saas.admin.entity.Role;
import com.saas.admin.entity.User;
import com.saas.admin.entity.UserRole;
import com.saas.admin.repository.PermissionRepository;
import com.saas.admin.repository.RolePermissionRepository;
import com.saas.admin.repository.RoleRepository;
import com.saas.admin.repository.UserRoleRepository;
import com.saas.shared.audit.Auditable;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
@RequiredArgsConstructor
public class RBACService {
    
    private final RoleRepository roleRepository;
    private final PermissionRepository permissionRepository;
    private final RolePermissionRepository rolePermissionRepository;
    private final UserRoleRepository userRoleRepository;
    
    private final Map<Long, CachedPermissions> permissionCache = new ConcurrentHashMap<>();
    private static final long CACHE_TTL_MS = TimeUnit.MINUTES.toMillis(5);
    
    // ==================== ROLE MANAGEMENT ====================
    
    @Transactional
    @Auditable(action = "CREATE_ROLE", entityType = "ROLE")
    public Role createRole(String name, String description, boolean isSystem) {
        log.info("Creating role: {}", name);
        
        if (roleRepository.existsByName(name)) {
            throw new IllegalArgumentException("Role with name '" + name + "' already exists");
        }
        
        Role role = Role.builder()
                .name(name.toUpperCase())
                .description(description)
                .isActive(true)
                .isSystem(isSystem)
                .build();
        
        Role saved = roleRepository.save(role);
        log.info("Role created with ID: {}", saved.getId());
        
        return saved;
    }
    
    @Transactional(readOnly = true)
    public Optional<Role> getRoleById(Long id) {
        return roleRepository.findById(id);
    }
    
    @Transactional(readOnly = true)
    public Optional<Role> getRoleByName(String name) {
        return roleRepository.findByName(name);
    }
    
    @Transactional(readOnly = true)
    public Optional<Role> getRoleByIdWithPermissions(Long id) {
        return roleRepository.findByIdWithPermissions(id);
    }
    
    @Transactional(readOnly = true)
    public List<Role> getAllRoles() {
        return roleRepository.findAll();
    }
    
    @Transactional(readOnly = true)
    public List<Role> getAllActiveRoles() {
        return roleRepository.findByIsActiveTrue();
    }
    
    @Transactional
    public Role updateRole(Long id, String description, Boolean isActive) {
        log.info("Updating role ID: {}", id);
        
        Role role = roleRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Role not found with ID: " + id));
        
        if (description != null) {
            role.setDescription(description);
        }
        
        if (isActive != null) {
            role.setIsActive(isActive);
        }
        
        Role updated = roleRepository.save(role);
        log.info("Role updated");
        
        return updated;
    }
    
    @Transactional
    @Auditable(action = "DELETE_ROLE", entityType = "ROLE")
    public void deleteRole(Long id) {
        log.info("Deleting role ID: {}", id);
        
        Role role = roleRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Role not found with ID: " + id));
        
        if (role.getIsSystem()) {
            throw new IllegalStateException("Cannot delete system role: " + role.getName());
        }
        
        long userCount = userRoleRepository.countByRole(role);
        if (userCount > 0) {
            throw new IllegalStateException("Cannot delete role assigned to " + userCount + " users. Remove user assignments first.");
        }
        
        roleRepository.delete(role);
        log.info("Role deleted");
    }
    
    // ==================== ROLE-PERMISSION ASSIGNMENT ====================
    
    @Transactional
    public void assignPermissionToRole(Long roleId, Long permissionId) {
        log.info("Assigning permission {} to role {}", permissionId, roleId);
        
        Role role = roleRepository.findById(roleId)
                .orElseThrow(() -> new IllegalArgumentException("Role not found with ID: " + roleId));
        
        Permission permission = permissionRepository.findById(permissionId)
                .orElseThrow(() -> new IllegalArgumentException("Permission not found with ID: " + permissionId));
        
        if (rolePermissionRepository.existsByRoleAndPermission(role, permission)) {
            log.warn("Permission already assigned to role");
            return;
        }
        
        role.addPermission(permission);
        roleRepository.save(role);
        
        invalidateCacheByRole(roleId);
        
        log.info("Permission assigned to role");
    }
    
    @Transactional
    @Auditable(action = "REMOVE_PERMISSION_FROM_ROLE", entityType = "ROLE")
    public void removePermissionFromRole(Long roleId, Long permissionId) {
        log.info("Removing permission {} from role {}", permissionId, roleId);
        
        Role role = roleRepository.findById(roleId)
                .orElseThrow(() -> new IllegalArgumentException("Role not found with ID: " + roleId));
        
        Permission permission = permissionRepository.findById(permissionId)
                .orElseThrow(() -> new IllegalArgumentException("Permission not found with ID: " + permissionId));
        
        role.removePermission(permission);
        roleRepository.save(role);
        
        invalidateCacheByRole(roleId);
        
        log.info("Permission removed from role");
    }
    
    @Transactional
    @Auditable(action = "ASSIGN_PERMISSIONS", entityType = "ROLE")
    public void assignPermissionsToRole(Long roleId, List<Long> permissionIds) {
        log.info("Assigning {} permissions to role {}", permissionIds.size(), roleId);
        
        Role role = roleRepository.findById(roleId)
                .orElseThrow(() -> new IllegalArgumentException("Role not found with ID: " + roleId));
        
        List<Permission> permissions = permissionRepository.findByIdIn(permissionIds);
        
        if (permissions.size() != permissionIds.size()) {
            throw new IllegalArgumentException("Some permissions not found");
        }
        
        for (Permission permission : permissions) {
            if (!rolePermissionRepository.existsByRoleAndPermission(role, permission)) {
                role.addPermission(permission);
            }
        }
        
        roleRepository.save(role);
        
        invalidateCacheByRole(roleId);
        
        log.info("{} permissions assigned to role", permissions.size());
    }
    
    @Transactional(readOnly = true)
    public List<String> getRolePermissions(Long roleId) {
        return rolePermissionRepository.findPermissionStringsByRoleId(roleId);
    }
    
    // ==================== USER-ROLE ASSIGNMENT ====================
    
    @Transactional
    public void assignRoleToUser(User user, Role role, String assignedBy) {
        log.info("Assigning role {} to user {}", role.getName(), user.getEmail());
        
        if (userRoleRepository.existsByUserAndRole(user, role)) {
            log.warn("Role already assigned to user");
            return;
        }
        
        UserRole userRole = UserRole.builder()
                .user(user)
                .role(role)
                .assignedBy(assignedBy)
                .build();
        
        userRoleRepository.save(userRole);
        
        invalidateCacheForUser(user.getId());
        
        log.info("Role assigned to user");
    }
    
    @Transactional
    public void removeRoleFromUser(User user, Role role) {
        log.info("Removing role {} from user {}", role.getName(), user.getEmail());
        
        userRoleRepository.findByUserAndRole(user, role)
                .ifPresent(userRoleRepository::delete);
        
        invalidateCacheForUser(user.getId());
        
        log.info("Role removed from user");
    }
    
    // ==================== PERMISSION MANAGEMENT ====================
    
    @Transactional
    @Auditable(action = "CREATE_PERMISSION", entityType = "PERMISSION")
    public Permission createPermission(String resource, String action, String description, boolean isSystem) {
        log.info("Creating permission: {}:{}", resource, action);
        
        String normalizedResource = resource.toLowerCase();
        String normalizedAction = action.toLowerCase();
        
        if (permissionRepository.existsByResourceAndAction(normalizedResource, normalizedAction)) {
            throw new IllegalArgumentException("Permission '" + normalizedResource + ":" + normalizedAction + "' already exists");
        }
        
        Permission permission = Permission.builder()
                .resource(normalizedResource)
                .action(normalizedAction)
                .description(description)
                .isActive(true)
                .isSystem(isSystem)
                .build();
        
        Permission saved = permissionRepository.save(permission);
        log.info("Permission created with ID: {}", saved.getId());
        
        return saved;
    }
    
    @Transactional
    public Permission createPermissionFromString(String permissionString, String description, boolean isSystem) {
        Permission permission = Permission.fromString(permissionString, description);
        permission.setIsSystem(isSystem);
        
        return createPermission(permission.getResource(), permission.getAction(), description, isSystem);
    }
    
    @Transactional(readOnly = true)
    public Optional<Permission> getPermissionById(Long id) {
        return permissionRepository.findById(id);
    }
    
    @Transactional(readOnly = true)
    public Optional<Permission> getPermissionByResourceAndAction(String resource, String action) {
        return permissionRepository.findByResourceAndAction(resource.toLowerCase(), action.toLowerCase());
    }
    
    @Transactional(readOnly = true)
    public List<Permission> getAllPermissions() {
        return permissionRepository.findAll();
    }
    
    @Transactional(readOnly = true)
    public List<Permission> getAllActivePermissions() {
        return permissionRepository.findByIsActiveTrue();
    }
    
    @Transactional(readOnly = true)
    public List<Permission> getPermissionsByResource(String resource) {
        return permissionRepository.findByResource(resource.toLowerCase());
    }
    
    @Transactional(readOnly = true)
    public List<String> getAllResources() {
        return permissionRepository.findAllResources();
    }
    
    @Transactional(readOnly = true)
    public List<String> getAllActions() {
        return permissionRepository.findAllActions();
    }
    
    @Transactional
    @Auditable(action = "UPDATE_PERMISSION", entityType = "PERMISSION")
    public Permission updatePermission(Long id, String description, Boolean isActive) {
        log.info("Updating permission ID: {}", id);
        
        Permission permission = permissionRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Permission not found with ID: " + id));
        
        if (description != null) {
            permission.setDescription(description);
        }
        
        if (isActive != null) {
            permission.setIsActive(isActive);
            invalidateCacheByPermission(id);
        }
        
        Permission updated = permissionRepository.save(permission);
        log.info("Permission updated");
        
        return updated;
    }
    
    @Transactional
    @Auditable(action = "DELETE_PERMISSION", entityType = "PERMISSION")
    public void deletePermission(Long id) {
        log.info("Deleting permission ID: {}", id);
        
        Permission permission = permissionRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Permission not found with ID: " + id));
        
        if (permission.getIsSystem()) {
            throw new IllegalStateException("Cannot delete system permission: " + permission.getPermissionString());
        }
        
        long roleCount = rolePermissionRepository.findByPermission(permission).size();
        if (roleCount > 0) {
            throw new IllegalStateException("Cannot delete permission assigned to " + roleCount + " roles. Remove role assignments first.");
        }
        
        permissionRepository.delete(permission);
        
        invalidateCacheByPermission(id);
        
        log.info("Permission deleted");
    }
    
    @Transactional
    public List<Permission> bulkCreatePermissions(List<String> permissionStrings, boolean isSystem) {
        log.info("Bulk creating {} permissions", permissionStrings.size());
        
        return permissionStrings.stream()
                .map(permString -> {
                    try {
                        Permission perm = Permission.fromString(permString, "Auto-created permission");
                        perm.setIsSystem(isSystem);
                        
                        Optional<Permission> existing = permissionRepository
                                .findByResourceAndAction(perm.getResource(), perm.getAction());
                        
                        if (existing.isPresent()) {
                            log.debug("Permission already exists: {}", permString);
                            return existing.get();
                        }
                        
                        return permissionRepository.save(perm);
                    } catch (Exception e) {
                        log.error("Failed to create permission: {}", permString, e);
                        return null;
                    }
                })
                .filter(java.util.Objects::nonNull)
                .toList();
    }
    
    // ==================== PERMISSION CACHE ====================
    
    public List<String> getCachedPermissionsForUser(Long userId) {
        CachedPermissions cached = permissionCache.get(userId);
        
        if (cached != null && !cached.isExpired()) {
            log.debug("Cache HIT for user {} ({} permissions)", userId, cached.permissions.size());
            return cached.permissions;
        }
        
        log.debug("Cache MISS for user {} - loading from database", userId);
        List<String> permissions = userRoleRepository.findAllPermissionsByUserId(userId);
        
        permissionCache.put(userId, new CachedPermissions(permissions));
        log.debug("Cached {} permissions for user {}", permissions.size(), userId);
        
        return permissions;
    }
    
    public void invalidateCacheForUser(Long userId) {
        permissionCache.remove(userId);
        log.info("Invalidated permission cache for user {}", userId);
    }
    
    public void invalidateCacheAll() {
        int size = permissionCache.size();
        permissionCache.clear();
        log.info("Invalidated all permission caches ({} entries)", size);
    }
    
    public void invalidateCacheByRole(Long roleId) {
        permissionCache.clear();
        log.info("Invalidated all permission caches due to role {} change", roleId);
    }
    
    public void invalidateCacheByPermission(Long permissionId) {
        permissionCache.clear();
        log.info("Invalidated all permission caches due to permission {} change", permissionId);
    }
    
    public int getCacheSize() {
        return permissionCache.size();
    }
    
    public void evictExpiredCache() {
        int before = permissionCache.size();
        permissionCache.entrySet().removeIf(entry -> entry.getValue().isExpired());
        int evicted = before - permissionCache.size();
        if (evicted > 0) {
            log.debug("Evicted {} expired cache entries", evicted);
        }
    }
    
    private static class CachedPermissions {
        final List<String> permissions;
        final long createdAt;
        
        CachedPermissions(List<String> permissions) {
            this.permissions = Collections.unmodifiableList(permissions);
            this.createdAt = System.currentTimeMillis();
        }
        
        boolean isExpired() {
            return System.currentTimeMillis() - createdAt > CACHE_TTL_MS;
        }
    }
}
