<?php

declare(strict_types=1);

namespace apexl\Io\modules\user\services;

use apexl\Io\modules\user\collections\permissionCollection;
use apexl\Io\modules\user\entities\roleEntity;
use apexl\Io\modules\user\entities\userEntity;
use apexl\Io\modules\user\enums\permissions\Common;
use apexl\Io\modules\user\exceptions\PermissionException;
use apexl\Io\modules\user\interfaces\PermissionInterface;
use apexl\Io\modules\user\Operator\UserPermissionOperator;
use apexl\Vault\exceptions\ExecutionException;
use apexl\Vault\Vault;

final readonly class UserPermissionManagerService
{
    public function __construct(
        private userEntity $user,
        private Vault $vault,
    ) {}

    /**
     * @throws PermissionException
     */
    public function can(PermissionInterface $permission): bool
    {
        if ($this->permissions()->contains(Common::BYPASS)) {
            return true;
        }

        if ($this->permissions()->contains($permission)) {
            return true;
        }

        return false;
    }

    /**
     * @throws PermissionException
     */
    public function permissions(bool $includeRoles = true): permissionCollection
    {
        // An anonymous/guest user (see userEntity's `@property ?int $id`) has
        // no user row to look permissions or roles up against.
        // UserPermissionOperator::listForUser() (and UserRoleOperator::listForUser(),
        // reached below via $includeRoles) both declare `int $userId` — passing
        // a null id throws a TypeError before it, which is not an ExecutionException
        // and so isn't caught by the try/catch below, or wrapped as a
        // PermissionException. That turns every anonymous request to a
        // permission-gated route into an uncaught 500 instead of the normal
        // unauthorised response. Resolve straight to an empty set instead of
        // ever reaching either operator.
        if ($this->user->id === null) {
            return permissionCollection::createFromQueryResult([]);
        }

        try {
            $permissions = permissionCollection::createFromQueryResult(
                $this->operator()->listForUser($this->user->id)
            );
            if ($includeRoles) {
                $this->user->roles()->each(fn(roleEntity $role) => $permissions->merge(
                    $role->permissions()
                ));
            }

            return $permissions->unique();
        } catch (ExecutionException $e) {
            throw new PermissionException($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * @throws PermissionException
     */
    public function add(PermissionInterface $permission): void
    {
        try {
            $this->operator()->insertForUser($this->user->id, serialize($permission));
        } catch (ExecutionException $e) {
            throw new PermissionException($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * @throws PermissionException
     */
    public function remove(PermissionInterface $permission): void
    {
        try {
            $this->operator()->deleteForUser($this->user->id, serialize($permission));
        } catch (ExecutionException $e) {
            throw new PermissionException($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * @throws PermissionException
     */
    public function clear(): void
    {
        try {
            $this->operator()->clearForUser($this->user->id);
        } catch (ExecutionException $e) {
            throw new PermissionException($e->getMessage(), $e->getCode(), $e);
        }
    }

    private function operator(): UserPermissionOperator
    {
        return new UserPermissionOperator($this->vault);
    }
}
