<?php

declare(strict_types=1);

namespace App\Administration\Application\Command\UpdateUserRoles;

use App\Administration\Application\Port\ActiveRoleStore;
use App\Administration\Domain\Exception\UserNotFoundException;
use App\Administration\Domain\Model\User\Role;
use App\Administration\Domain\Model\User\User;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Domain\Repository\UserRepository;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;

use function array_filter;
use function array_map;
use function array_values;
use function in_array;

use InvalidArgumentException;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler(bus: 'command.bus')]
final readonly class UpdateUserRolesHandler
{
    public function __construct(
        private UserRepository $userRepository,
        private Clock $clock,
        private ActiveRoleStore $activeRoleStore,
    ) {
    }

    public function __invoke(UpdateUserRolesCommand $command): User
    {
        $userId = UserId::fromString($command->userId);
        $user = $this->userRepository->get($userId);

        if ($command->tenantId !== '' && !$user->tenantId->equals(TenantId::fromString($command->tenantId))) {
            throw UserNotFoundException::withId($userId);
        }

        if ($command->roles === []) {
            throw new InvalidArgumentException('Au moins un rôle est requis.');
        }

        $targetRoles = array_map(static function (string $r): Role {
            $role = Role::tryFrom($r);
            if ($role === null) {
                throw new InvalidArgumentException("Rôle invalide : \"{$r}\".");
            }

            return $role;
        }, $command->roles);

        $now = $this->clock->now();

        // Add new roles first to avoid "last role" exception
        foreach ($targetRoles as $targetRole) {
            if (!$user->aLeRole($targetRole)) {
                $user->attribuerRole($targetRole, $now);
            }
        }

        // Collect roles to remove, then remove them (avoid mutating during iteration)
        /** @var list<Role> $rolesToRemove */
        $rolesToRemove = array_values(array_filter(
            $user->roles,
            static fn (Role $currentRole) => !in_array($currentRole, $targetRoles, true),
        ));

        foreach ($rolesToRemove as $roleToRemove) {
            $user->retirerRole($roleToRemove, $now);
        }

        $this->userRepository->save($user);

        // Clear cached active role to avoid stale reference to a removed role
        $this->activeRoleStore->clear($user);

        return $user;
    }
}
