<?php

declare(strict_types=1);

namespace apexl\Io\modules\user\services;

use apexl\Io\exceptions\RecordNotFoundException;
use apexl\Io\modules\user\collections\roleCollection;
use apexl\Io\modules\user\entities\roleEntity;
use apexl\Io\modules\user\entities\userEntity;
use apexl\Io\modules\user\enums\Role;
use apexl\Io\modules\user\exceptions\PermissionException;
use apexl\Io\modules\user\Operator\UserRoleOperator;
use apexl\Vault\exceptions\ExecutionException;
use apexl\Vault\Vault;

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

    /**
     * @throws PermissionException
     */
    public function add(string|int|Role|roleEntity $role): void
    {
        try {
            $roleId = $this->ensureRoleId($role);
        } catch (RecordNotFoundException) {
            return;
        }

        if ($this->has($roleId)) {
            return;
        }

        try {
            $this->operator()->insertForUser($this->user->id, $roleId);
        } catch (ExecutionException $e) {
            throw new PermissionException($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * @throws RecordNotFoundException
     */
    private function ensureRoleId(string|Role|roleEntity|int $role): int
    {
        if (is_numeric($role)) {
            return (int) $role;
        }

        if ($role instanceof Role) {
            $role = $role->value;
        }

        if (is_string($role)) {
            $_role = roleEntity::findWhere('name', $role);
            if (!$_role->id) {
                throw new RecordNotFoundException(sprintf('Role %s not found', $role));
            }

            return $_role->id;
        }

        if ($role instanceof roleEntity) {
            return $role->id;
        }

        return $role;
    }

    /**
     * @throws PermissionException
     */
    public function has(string|int|roleEntity $role): bool
    {
        try {
            $roleId = $this->ensureRoleId($role);
        } catch (RecordNotFoundException) {
            return false;
        }

        return $this->roles()->exists(fn(roleEntity $role): bool => $role->id === $roleId);
    }

    /**
     * @throws PermissionException
     */
    public function roles(): roleCollection
    {
        // An anonymous/guest user (see userEntity's `@property ?int $id`) has
        // no user row to look roles up against. UserRoleOperator::listForUser()
        // declares `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. Resolve
        // straight to an empty set instead of ever reaching the operator.
        // Mirrors the identical guard added to
        // UserPermissionManagerService::permissions() (see commit 4a4677c).
        if ($this->user->id === null) {
            return roleCollection::createFromQueryResult([]);
        }

        try {
            return roleCollection::createFromQueryResult(
                $this->operator()->listForUser($this->user->id)
            );
        } 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);
        }
    }

    /**
     * @throws PermissionException
     */
    public function remove(int|roleEntity $role): void
    {
        try {
            $roleId = $this->ensureRoleId($role);
            $this->operator()->deleteForUser($this->user->id, $roleId);
        } catch (ExecutionException|RecordNotFoundException $e) {
            throw new PermissionException($e->getMessage(), $e->getCode(), $e);
        }
    }

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