<?php

namespace App\Services\Authorization;

use App\Models\Permission;
use App\Models\Role;
use App\Models\User;
use App\Support\Authorization\PermissionName;
use Illuminate\Support\Collection;

class AuthorizationDelegationService
{
    public function assignableRoles(User $actor): Collection
    {
        return Role::query()
            ->with('permissions:id,name')
            ->orderBy('display_name')
            ->get()
            ->filter(fn (Role $role): bool => $this->canDelegateRole($actor, $role))
            ->values();
    }

    public function protectedRoles(User $actor, User $target): Collection
    {
        return $target->roles()
            ->with('permissions:id,name')
            ->orderBy('display_name')
            ->get()
            ->reject(fn (Role $role): bool => $this->canDelegateRole($actor, $role))
            ->values();
    }

    public function canDelegateRole(User $actor, Role $role): bool
    {
        if ($this->canDelegateWithoutCeiling($actor)) {
            return true;
        }

        if (! $actor->can(PermissionName::ROLES_ASSIGN->value)) {
            return false;
        }

        if ($role->name === 'super-administrator'
            && ! $actor->can(PermissionName::ROLES_ASSIGN_SUPER_ADMIN->value)) {
            return false;
        }

        if ($role->is_system
            && ! $actor->can(PermissionName::ROLES_ASSIGN_SYSTEM->value)) {
            return false;
        }

        $actorPermissions = $actor->getAllPermissions()->pluck('name')->all();
        $rolePermissions = ($role->relationLoaded('permissions') ? $role->permissions : $role->permissions()->get())
            ->pluck('name')
            ->all();

        return array_diff($rolePermissions, $actorPermissions) === [];
    }

    public function isProtectedSuperAdministrator(User $user): bool
    {
        return $user->roles()
            ->where('name', 'super-administrator')
            ->exists();
    }

    /**
     * Preserve current roles that the actor is not permitted to remove.
     *
     * @param array<int, int|string> $requestedRoleIds
     * @return array<int, int>
     */
    public function normalizeRequestedRoleIds(User $actor, ?User $target, array $requestedRoleIds): array
    {
        $requested = collect($requestedRoleIds)
            ->map(fn ($id): int => (int) $id)
            ->filter(fn (int $id): bool => $id > 0)
            ->unique();

        if (! $target) {
            return $requested->values()->all();
        }

        $protected = $this->protectedRoles($actor, $target)->pluck('id')->map(fn ($id): int => (int) $id);

        return $requested
            ->merge($protected)
            ->unique()
            ->values()
            ->all();
    }

    /**
     * @param array<int, int|string> $requestedRoleIds
     */
    public function assertRoleTransitionAllowed(
        User $actor,
        ?User $target,
        array $requestedRoleIds,
        string $requestedStatus = 'active',
    ): void {
        $requested = collect($requestedRoleIds)
            ->map(fn ($id): int => (int) $id)
            ->filter(fn (int $id): bool => $id > 0)
            ->unique()
            ->values();

        $roles = Role::query()
            ->with('permissions:id,name')
            ->whereKey($requested)
            ->get();

        abort_if(
            $roles->count() !== $requested->count(),
            422,
            'One or more selected roles are invalid.',
        );

        $currentIds = $target
            ? $target->roles()->pluck('roles.id')->map(fn ($id): int => (int) $id)->values()
            : collect();

        $changedIds = $currentIds->diff($requested)
            ->merge($requested->diff($currentIds))
            ->unique()
            ->values();

        if ($changedIds->isNotEmpty()) {
            abort_unless(
                $actor->can(PermissionName::ROLES_ASSIGN->value),
                403,
                'You are not allowed to change role assignments.',
            );

            $changedRoles = Role::query()
                ->with('permissions:id,name')
                ->whereKey($changedIds)
                ->get();

            foreach ($changedRoles as $role) {
                abort_unless(
                    $this->canDelegateRole($actor, $role),
                    403,
                    'You cannot assign or remove a role with authority outside your delegation ceiling.',
                );
            }
        }

        if ($target && $target->id === $actor->id && ! $actor->can(PermissionName::ROLES_ASSIGN_SUPER_ADMIN->value)) {
            $added = $requested->diff($currentIds);
            abort_if(
                $added->isNotEmpty(),
                403,
                'You cannot add roles to your own account.',
            );
        }

        if ($target && $this->isProtectedSuperAdministrator($target)) {
            $roleChanged = $changedIds->isNotEmpty();
            $statusChanged = $requestedStatus !== (string) $target->status;

            if ($roleChanged || $statusChanged) {
                abort_unless(
                    $actor->can(PermissionName::ROLES_ASSIGN_SUPER_ADMIN->value),
                    403,
                    'Only authorized super-administrator delegation may change a Super Administrator account.',
                );
            }

            $this->assertLastActiveSuperAdministratorPreserved($target, $requested->all(), $requestedStatus);
        }
    }

    /** @param array<int, int|string> $permissionIds */
    public function assertPermissionsDelegable(User $actor, array $permissionIds): void
    {
        if ($this->canDelegateWithoutCeiling($actor)) {
            return;
        }

        $requested = collect($permissionIds)
            ->map(fn ($id): int => (int) $id)
            ->filter(fn (int $id): bool => $id > 0)
            ->unique();

        $actorPermissionIds = $actor->getAllPermissions()->pluck('id')->map(fn ($id): int => (int) $id);

        abort_unless(
            $requested->diff($actorPermissionIds)->isEmpty(),
            403,
            'You cannot grant permissions that you do not hold yourself.',
        );
    }

    public function permissionOptions(User $actor): Collection
    {
        $query = Permission::query()->orderBy('group')->orderBy('display_name');

        if (! $this->canDelegateWithoutCeiling($actor)) {
            $query->whereIn('id', $actor->getAllPermissions()->pluck('id'));
        }

        return $query->get();
    }

    public function assertCustomRoleMutable(Role $role): void
    {
        abort_if(
            $role->is_system,
            422,
            'System roles are protected and cannot be modified through role administration.',
        );
    }

    /** @param array<int, int> $requestedRoleIds */
    private function assertLastActiveSuperAdministratorPreserved(
        User $target,
        array $requestedRoleIds,
        string $requestedStatus,
    ): void {
        if ($target->status !== 'active') {
            return;
        }

        $superRoleId = Role::query()->where('name', 'super-administrator')->value('id');
        if (! $superRoleId) {
            return;
        }

        $remainsActiveSuperAdmin = $requestedStatus === 'active'
            && in_array((int) $superRoleId, array_map('intval', $requestedRoleIds), true);

        if ($remainsActiveSuperAdmin) {
            return;
        }

        $anotherActiveSuperAdminExists = User::query()
            ->whereKeyNot($target->id)
            ->where('status', 'active')
            ->whereHas('roles', fn ($query) => $query->where('name', 'super-administrator'))
            ->exists();

        abort_unless(
            $anotherActiveSuperAdminExists,
            422,
            'The last active Super Administrator cannot be removed or deactivated.',
        );
    }

    private function canDelegateWithoutCeiling(User $actor): bool
    {
        return $actor->can(PermissionName::ROLES_ASSIGN->value)
            && $actor->can(PermissionName::ROLES_ASSIGN_SYSTEM->value)
            && $actor->can(PermissionName::ROLES_ASSIGN_SUPER_ADMIN->value);
    }
}
