<?php

namespace apexl\Io\modules\user\entities;

use apexl\entityCore\enums\Casts;
use apexl\entityCore\interfaces\EntityOperatorInterface;
use apexl\entityCore\traits\hasCasts;
use apexl\Io\exceptions\RecordNotFoundException;
use apexl\Io\includes\Entity;
use apexl\Io\modules\user\collections\permissionCollection;
use apexl\Io\modules\user\collections\roleCollection;
use apexl\Io\modules\user\collections\userMetaCollection;
use apexl\Io\modules\user\entities\operators\userOperator;
use apexl\Io\modules\user\EntityFactories\UserEntityFactory;
use apexl\Io\modules\user\enums\permissions\Common;
use apexl\Io\modules\user\enums\permissions\User;
use apexl\Io\modules\user\enums\Role;
use apexl\Io\modules\user\exceptions\PermissionException;
use apexl\Io\modules\user\Factories\UserMetaManagerFactory;
use apexl\Io\modules\user\Factories\UserPermissionManagerFactory;
use apexl\Io\modules\user\Factories\UserRoleManagerFactory;
use apexl\Io\modules\user\interfaces\CanBeMasqueradedInterface;
use apexl\Io\modules\user\interfaces\CanMasqueradeInterface;
use apexl\Io\modules\user\interfaces\PermissionInterface;
use apexl\Io\modules\user\services\CurrentUserFactory;
use apexl\Io\modules\user\services\UserMetaManagerService;
use apexl\Io\modules\user\services\UserPermissionManagerService;
use apexl\Io\modules\user\services\UserRoleManagerService;
use DateTimeImmutable;
use Exception;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use function apexl\Io\container;

/**
 * @property ?int $id
 * @property string $first_name
 * @property string $last_name
 * @property bool $active
 * @property string $email
 * @property string $password
 * @property ?DateTimeImmutable $last_login
 * @property ?DateTimeImmutable $created
 * @property ?string $created_by
 * @property ?DateTimeImmutable $modified
 * @property ?string $modified_by
 * @property string nfc_key
 * @mixin userOperator
 *
 * @method static UserEntityFactory factory()
 */
class userEntity extends Entity implements CanMasqueradeInterface, CanBeMasqueradedInterface
{
    protected const array PROTECTED_FIELDS = [
        'password',
        'yubikey_id',
        'nfc_key',
    ];

    use hasCasts {
        casts as traitCasts;
    }

    final public const string TABLE = 'users';

    /** @var userOperator */
    protected EntityOperatorInterface $operator;
    private CurrentUserFactory $currentUserFactory;
    private UserMetaManagerService $userMetaManager;
    private UserPermissionManagerService $permissionManager;
    private UserRoleManagerService $roleManager;

    /**
     * @throws NotFoundExceptionInterface
     * @throws ContainerExceptionInterface
     * @throws Exception
     */
    public function __construct()
    {
        parent::__construct(self::TABLE);
        $this->setOperator(new userOperator(self::TABLE));
        $this->setSensitiveData(['password', 'salt']);
        $this->setRequiredData(['email', 'first_name', 'last_name', 'password']);
        $this->addReadOnlyField('salt')->addReadOnlyField('created');

        $this->currentUserFactory = container()->get(CurrentUserFactory::class);

        $this->roleManager = container()->get(UserRoleManagerFactory::class)->make($this);
        $this->permissionManager = container()->get(UserPermissionManagerFactory::class)->make($this);
        $this->userMetaManager = container()->get(UserMetaManagerFactory::class)->make($this);
    }

    /**
     * @throws RecordNotFoundException
     */
    public static function fromEmail(string $email): userEntity
    {
        return self::from('email', $email);
    }

    /**
     * @throws RecordNotFoundException
     */
    public static function fromNfcKey(string $nfcKey): userEntity
    {
        return self::from('nfc_key', $nfcKey);
    }

    /** @noinspection PhpUnused */
    public static function entityFactoryClass(): string
    {
        return UserEntityFactory::class;
    }

    /**
     * @param array $conditions
     * @param array $orderBy
     * @param false $limit
     * @param false $offset
     * @param bool $asEntities
     * @return array
     * @throws Exception
     */
    public function loadMultiple(
        array $conditions = [],
        array $orderBy = [],
        $limit = false,
        $offset = false,
        bool $asEntities = true
    ): array {
//        if ($currentUser->isAllowed('ViewUsers')) {
        $conditions = array_merge($conditions, ["id" => ["users.id", null, "IS NOT NULL"]]);

//        }

        return parent::loadMultiple($conditions, $orderBy, $limit, $offset, $asEntities);
    }

    public function casts(): array
    {
        return [
            ...$this->traitCasts(),
            'last_login' => Casts::DATETIME_STAMP,
        ];
    }

    /**
     * @throws PermissionException
     */
    public function roles(): roleCollection
    {
        return $this->roleManager->roles();
    }

    public function getUsersByRoleName($name)
    {
        $roles = new roleEntity();
        $roles->loadByName($name);

        return $this->operator->getUsersByRoleId($roles->id);
    }

    /**
     * @throws PermissionException
     */
    public function addRole(string|Role|roleEntity|int $role): userEntity
    {
        $this->roleManager->add($role);

        return $this;
    }

    /**
     * @throws PermissionException
     */
    public function removeRoles(): userEntity
    {
        $this->roleManager->clear();
        $this->data['roles'] = [];

        return $this;
    }

    /**
     * @throws PermissionException
     */
    public function removeRole(string|Role|roleEntity|int $role): userEntity
    {
        $this->roleManager->remove($role);

        return $this;
    }

    public function getNiceName(): string
    {
        return $this->id !== null ? sprintf('%s %s', $this->first_name, $this->last_name) : 'Missing User';
    }

    public function allMeta(): userMetaCollection
    {
        return $this->userMetaManager->allMeta();
    }

    public function hasMeta(string $key): bool
    {
        return $this->userMetaManager->hasMeta($key);
    }

    public function meta(string $key): ?userMetaEntity
    {
        return $this->userMetaManager->meta($key);
    }

    public function removeMeta(string $key): void
    {
        $this->userMetaManager->removeMeta($key);
    }

    public function addMeta($key, $value): void
    {
        $this->userMetaManager->addMeta($key, $value);
    }

    /**
     * @throws PermissionException
     */
    public function permissions(bool $includeRoles = true): permissionCollection
    {
        return $this->permissionManager->permissions($includeRoles);
    }

    /**
     * @throws PermissionException
     */
    public function addPermission(PermissionInterface $permission): void
    {
        $this->permissionManager->add($permission);
    }

    /**
     * @throws PermissionException
     */
    public function removePermission(PermissionInterface $permission): void
    {
        $this->permissionManager->remove($permission);
    }

    /**
     * @throws PermissionException
     */
    public function canMasqueradeAs(CanBeMasqueradedInterface $to): bool
    {
        return $this->isAllowed(User::CAN_MASQUERADE);
    }

    /**
     * @throws PermissionException
     */
    public function isAllowed(PermissionInterface|array $permissions): bool
    {
        $permissions = is_array($permissions) ? $permissions : [$permissions];

        foreach ($permissions as $permission) {
            return match ($permission) {
                Common::CAN_DO_NOTHING => false,
                Common::ALLOW_ALL => true,
                Common::IS_LOGGED_IN => $this->isLoggedIn(),
                default => $this->permissionManager->can($permission),
            };
        }

        //no access? Return FALSE;
        return false;
    }

    public function isLoggedIn(): bool
    {
        return !!$this->id && ($this->currentUserFactory->getWithoutMasquerade()->id === $this->id);
    }

    /**
     * @throws PermissionException
     */
    public function hasRole(string|int|roleEntity $roleName): bool
    {
        return $this->roleManager->has($roleName);
    }

    public function getId(): int
    {
        return $this->id;
    }
}
