<?php

namespace MintyPHP\Service\Access;

use MintyPHP\Repository\Access\RoleAssignableRoleRepositoryInterface;
use MintyPHP\Repository\Access\RoleRepositoryInterface;
use MintyPHP\Repository\Access\UserRoleRepositoryInterface;
use MintyPHP\Service\Audit\AuditRecorderInterface;

class AssignableRoleService
{
    public function __construct(
        private readonly RoleAssignableRoleRepositoryInterface $assignableRoleRepository,
        private readonly UserRoleRepositoryInterface $userRoleRepository,
        private readonly PermissionService $permissionService,
        private readonly RoleRepositoryInterface $roleRepository,
        private readonly AuditRecorderInterface $systemAuditService
    ) {
    }

    /**
     * Returns role IDs the given actor is allowed to assign.
     * Users with 'roles.assign_all' get all active role IDs.
     * Otherwise: union of assignable_role_ids from all of the actor's roles.
     *
     * @return int[]
     */
    public function listAssignableRoleIdsForActor(int $actorUserId): array
    {
        if ($actorUserId <= 0) {
            return [];
        }

        if ($this->permissionService->userHas($actorUserId, PermissionService::ROLES_ASSIGN_ALL)) {
            return $this->roleRepository->listActiveIds();
        }

        $actorRoleIds = $this->userRoleRepository->listRoleIdsByUserId($actorUserId);
        if (!$actorRoleIds) {
            return [];
        }

        return $this->assignableRoleRepository->listAssignableRoleIdsByRoleIds($actorRoleIds);
    }

    /**
     * Computes the effective role set respecting the actor's assignable scope.
     *
     * Roles the actor cannot assign are "frozen" — they remain as-is.
     *   frozen  = currentIds ∩ NOT-assignable
     *   touched = submittedIds ∩ assignable
     *   result  = frozen ∪ touched
     *
     * @param int[] $submittedIds Role IDs the form submitted
     * @param int[] $currentIds   Role IDs the target user currently has
     * @return int[]
     */
    public function computeEffectiveRoleIds(int $actorUserId, array $submittedIds, array $currentIds): array
    {
        $assignable = $this->listAssignableRoleIdsForActor($actorUserId);
        $assignableMap = array_fill_keys($assignable, true);

        // Roles the actor cannot touch — keep them as they are.
        $frozen = array_values(array_filter(
            $currentIds,
            static fn (int $id): bool => !isset($assignableMap[$id])
        ));

        // From the submitted set, only keep roles the actor is allowed to assign.
        $touched = array_values(array_filter(
            $submittedIds,
            static fn (int $id): bool => isset($assignableMap[$id])
        ));

        return array_values(array_unique(array_merge($frozen, $touched)));
    }

    /**
     * Saves which roles this role can assign (admin UI) and logs the change.
     *
     * @param int[] $assignableRoleIds
     */
    public function replaceAssignableRoles(int $roleId, array $assignableRoleIds, int $actorUserId): bool
    {
        $beforeIds = $this->assignableRoleRepository->listAssignableRoleIdsByRoleId($roleId);
        $result = $this->assignableRoleRepository->replaceForRole($roleId, $assignableRoleIds);

        $afterIds = $this->assignableRoleRepository->listAssignableRoleIdsByRoleId($roleId);
        if ($beforeIds !== $afterIds) {
            $this->systemAuditService->record('admin.roles.assignable_roles.update', 'success', [
                'actor_user_id' => $actorUserId > 0 ? $actorUserId : null,
                'target_type' => 'role',
                'target_id' => $roleId,
                'before' => ['assignable_role_ids' => $beforeIds],
                'after' => ['assignable_role_ids' => $afterIds],
            ]);
        }

        return $result;
    }
}
