package de.effigenix.application.usermanagement;

import de.effigenix.application.usermanagement.command.ChangePasswordCommand;
import de.effigenix.domain.usermanagement.*;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.persistence.UnitOfWork;
import de.effigenix.shared.security.AuthorizationPort;

/**
 * Use Case: Change user password.
 *
 * Requires current password verification for security.
 * Self-service: users can change their own password without PASSWORD_CHANGE permission.
 */
public class ChangePassword {

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

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

    public Result<UserError, Void> execute(ChangePasswordCommand cmd, ActorId performedBy) {
        // 0. Input validation
        if (cmd.userId() == null || cmd.userId().isBlank()) {
            return Result.failure(new UserError.InvalidInput("User ID must not be blank"));
        }
        if (cmd.currentPassword() == null || cmd.currentPassword().isBlank()) {
            return Result.failure(new UserError.InvalidInput("Current password must not be blank"));
        }
        if (cmd.newPassword() == null || cmd.newPassword().isBlank()) {
            return Result.failure(new UserError.InvalidInput("New password must not be blank"));
        }

        // 1. Authorization: self-service allowed, otherwise need PASSWORD_CHANGE permission
        boolean isSelfService = performedBy.value().equals(cmd.userId());
        if (!isSelfService && !authPort.can(performedBy, UserManagementAction.PASSWORD_CHANGE)) {
            return Result.failure(new UserError.Unauthorized("Not authorized to change password for other users"));
        }

        // 2. Find user
        UserId userId = UserId.of(cmd.userId());
        return findUser(userId).flatMap(user -> changeUserPassword(user, cmd, performedBy));
    }

    private Result<UserError, Void> changeUserPassword(User user, ChangePasswordCommand cmd, ActorId performedBy) {
        // 3. Verify current password
        if (!passwordHasher.verify(cmd.currentPassword(), user.passwordHash())) {
            auditLogger.log(AuditEvent.PASSWORD_CHANGE_FAILED, user.id().value(), performedBy);
            return Result.failure(new UserError.InvalidCredentials());
        }

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

        // 5. Hash and update (immutable)
        PasswordHash newPasswordHash = passwordHasher.hash(cmd.newPassword());
        return user.changePassword(newPasswordHash)
                .flatMap(updated -> unitOfWork.executeAtomically(() ->
                        userRepository.save(updated)
                                .mapError(err -> (UserError) new UserError.RepositoryFailure(err.message()))
                                .map(ignored -> {
                                    auditLogger.log(AuditEvent.PASSWORD_CHANGED, updated.id().value(), performedBy);
                                    return null;
                                })));
    }

    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))));
    }
}
