package de.effigenix.application.usermanagement;

import de.effigenix.application.usermanagement.command.AssignRoleCommand;
import de.effigenix.application.usermanagement.dto.UserDTO;
import de.effigenix.domain.usermanagement.*;
import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.persistence.UnitOfWork;
import de.effigenix.shared.security.AuthorizationPort;

import java.util.Optional;

/**
 * Use Case: Assign a role to a user.
 */
public class AssignRole {

    private final UserRepository userRepository;
    private final RoleRepository roleRepository;
    private final AuditLogger auditLogger;
    private final AuthorizationPort authPort;
    private final UnitOfWork unitOfWork;

    public AssignRole(
            UserRepository userRepository,
            RoleRepository roleRepository,
            AuditLogger auditLogger,
            AuthorizationPort authPort,
            UnitOfWork unitOfWork
    ) {
        this.userRepository = userRepository;
        this.roleRepository = roleRepository;
        this.auditLogger = auditLogger;
        this.authPort = authPort;
        this.unitOfWork = unitOfWork;
    }

    public Result<UserError, UserDTO> execute(AssignRoleCommand cmd, ActorId performedBy) {
        // 0. Authorization
        if (!authPort.can(performedBy, UserManagementAction.ROLE_ASSIGN)) {
            return Result.failure(new UserError.Unauthorized("Not authorized to assign roles"));
        }

        // 1. Input validation
        if (cmd.userId() == null || cmd.userId().isBlank()) {
            return Result.failure(new UserError.InvalidInput("User ID must not be blank"));
        }
        if (cmd.roleName() == null) {
            return Result.failure(new UserError.InvalidInput("Role name must not be null"));
        }

        UserId userId = UserId.of(cmd.userId());
        return findUser(userId).flatMap(user -> findRoleAndAssign(user, cmd, performedBy));
    }

    private Result<UserError, UserDTO> findRoleAndAssign(User user, AssignRoleCommand cmd, ActorId performedBy) {
        return roleRepository.findByName(cmd.roleName())
                .mapError(err -> (UserError) new UserError.RepositoryFailure(err.message()))
                .flatMap(optRole -> {
                    if (optRole.isEmpty()) {
                        return Result.failure(new UserError.RoleNotFound(cmd.roleName()));
                    }
                    Role role = optRole.get();
                    return user.assignRole(role)
                            .flatMap(updated -> unitOfWork.executeAtomically(() ->
                                    userRepository.save(updated)
                                            .mapError(err -> (UserError) new UserError.RepositoryFailure(err.message()))
                                            .map(ignored -> {
                                                auditLogger.log(AuditEvent.ROLE_ASSIGNED, updated.id().value(), "Role: " + role.name(), performedBy);
                                                return UserDTO.from(updated);
                                            })));
                });
    }

    private Result<UserError, User> findUser(UserId userId) {
        return userRepository.findById(userId)
                .mapError(err -> (UserError) new UserError.RepositoryFailure(err.message()))
                .flatMap(opt -> opt
                        .map(Result::<UserError, User>success)
                        .orElse(Result.failure(new UserError.UserNotFound(userId))));
    }
}
