package de.effigenix.application.usermanagement;

import de.effigenix.application.usermanagement.command.CreateUserCommand;
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.HashSet;
import java.util.Set;

/**
 * Use Case: Create a new user account.
 */
public class CreateUser {

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

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

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

        // 1. Input validation
        if (cmd.username() == null || cmd.username().isBlank()) {
            return Result.failure(new UserError.InvalidInput("Username must not be blank"));
        }
        if (cmd.email() == null || cmd.email().isBlank()) {
            return Result.failure(new UserError.InvalidInput("Email must not be blank"));
        }
        if (cmd.password() == null || cmd.password().isBlank()) {
            return Result.failure(new UserError.InvalidInput("Password must not be blank"));
        }
        if (cmd.roleNames() == null || cmd.roleNames().isEmpty()) {
            return Result.failure(new UserError.InvalidInput("At least one role must be specified"));
        }

        // 2. Validate password
        if (!passwordHasher.isValidPassword(cmd.password())) {
            return Result.failure(new UserError.InvalidPassword("Password must be at least 8 characters and contain uppercase, lowercase, digit, and special character"));
        }

        // 3. Check username uniqueness
        return userRepository.existsByUsername(cmd.username())
                .mapError(err -> (UserError) new UserError.RepositoryFailure(err.message()))
                .flatMap(exists -> {
                    if (exists) return Result.failure(new UserError.UsernameAlreadyExists(cmd.username()));
                    return checkEmailAndCreate(cmd, performedBy);
                });
    }

    private Result<UserError, UserDTO> checkEmailAndCreate(CreateUserCommand cmd, ActorId performedBy) {
        return userRepository.existsByEmail(cmd.email())
                .mapError(err -> (UserError) new UserError.RepositoryFailure(err.message()))
                .flatMap(exists -> {
                    if (exists) return Result.failure(new UserError.EmailAlreadyExists(cmd.email()));
                    return loadRolesAndCreate(cmd, performedBy);
                });
    }

    private Result<UserError, UserDTO> loadRolesAndCreate(CreateUserCommand cmd, ActorId performedBy) {
        PasswordHash passwordHash = passwordHasher.hash(cmd.password());

        Set<Role> roles = new HashSet<>();
        for (RoleName roleName : cmd.roleNames()) {
            switch (roleRepository.findByName(roleName)) {
                case Result.Failure<RepositoryError, java.util.Optional<Role>> f ->
                        { return Result.failure(new UserError.RepositoryFailure(f.error().message())); }
                case Result.Success<RepositoryError, java.util.Optional<Role>> s -> {
                    if (s.value().isEmpty()) {
                        return Result.failure(new UserError.RoleNotFound(roleName));
                    }
                    roles.add(s.value().get());
                }
            }
        }

        return User.create(cmd.username(), cmd.email(), passwordHash, roles, cmd.branchId())
                .flatMap(user -> unitOfWork.executeAtomically(() ->
                        userRepository.save(user)
                                .mapError(err -> (UserError) new UserError.RepositoryFailure(err.message()))
                                .map(ignored -> {
                                    auditLogger.log(AuditEvent.USER_CREATED, user.id().value(), performedBy);
                                    return UserDTO.from(user);
                                })));
    }
}
