<?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\includes\Entity;
use apexl\Io\includes\System;
use apexl\Io\includes\Utils;
use apexl\Io\modules\user\entities\operators\userOperator;
use apexl\Io\modules\user\services\currentUser;
use DateTimeImmutable;
use Exception;

/**
 * @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 $salt
 * @property string $roles
 * @mixin userOperator
 */
class userEntity extends Entity
{
    use hasCasts;

    final public const TABLE = 'users';

    protected EntityOperatorInterface $operator;

    /** @var System */
    protected $system;

    /** @var array - Cache loaded roles so when doing access checks we don't hammer the database. */
    protected array $cachedRoles = [];

    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->system = System::getInstance();
    }

    public function casts(): array
    {
        return [
            'id' => Casts::INT,
            'active' => Casts::BOOL,
            'last_login' => Casts::DATETIME_STAMP,
            'created' => Casts::DATETIME_STAMP,
            'modified' => Casts::DATETIME_STAMP,
        ];
    }

    /**
     * @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 {
        //we need to allow users with permission to bypass the default access control.
        $currentUser = (currentUser::getInstance())::getCurrentUser();
        if ($currentUser->isAllowed('ViewUsers')) {
            $conditions = array_merge($conditions, ["id" => ["users.id", null, "IS NOT"]]);
        }

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

    public function isAllowed($permission): bool
    {
        //allow user 1 to always access everything (if set in config)
        if (isset($this->config->app->permissions->enableGodUser) && $this->config->app->permissions->enableGodUser && $this->id == 1) {
            // We're God user, and we're allowed access to everything.
            return true;
        }
        //we have a few core permissions to account for 'standard' functionality
        switch ($permission) {
            case 'canDoNothing':
                return false;
            case 'AllowAll':
                return true;
            case 'IsLoggedIn':
                return $this->isLoggedIn();
            default:
                if (!empty($this->data['roles'])) {
                    foreach ($this->data['roles'] as $id) {
                        if (!isset($this->cachedRoles[$id])) {
                            $this->cacheRoles();
                        }
                        //if we have access in any of the provided roles, then return TRUE.
                        if ($this->cachedRoles[$id]->access($permission)) {
                            return true;
                        }
                    }
                }
        }


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

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

    /** @noinspection PhpUnused */

    protected function cacheRoles(): userEntity
    {
        foreach ($this->data['roles'] as $id) {
            if (!isset($this->cachedRoles[$id])) {
                $roles = new roleEntity();
                try {
                    $this->cachedRoles[$id] = $roles->load($id, true);
                } catch (Exception $e) {
                    user_error(
                        sprintf('Error caching roles: %s', $e->getMessage()),
                        E_USER_WARNING
                    );
                }
            }
        }

        return $this;
    }

    /**
     * override the magic load method, so we can intercept roles and un-sterilise them.
     * @param $id
     * @throws Exception
     */
    public function load($id, bool $skipAccessCheck = false): array
    {
        if ($this->system->isInstalled()) {
            parent::__call('load', [$id, $skipAccessCheck]);
            //gives us an array of role id's for easy use later.
            if (!empty($this->data['roles']) && is_string($this->data['roles'])) {
                $this->data['roles'] = unserialize($this->data['roles']);
                $this->cacheRoles();
            }
        }

        return parent::load($id, $skipAccessCheck);
    }

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

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

    /**
     * Function to add an ID to an array. PHP now throws "Indirect modification of overloaded property" when trying to set Array's using magic methods
     * @param $id
     */
    public function addRole($id): userEntity
    {
        $this->data['roles'][] = $id;

        return $this;
    }

    public function removeRoles(): userEntity
    {
        $this->data['roles'] = [];

        return $this;
    }

    /**
     * override the magic store method to make sure we serialise role data before saving.
     * @throws Exception
     */
    public function store(): Entity
    {
        $this->data['roles'] = $this->data['roles'] ?? [];
        //we need to revert the serialised data after storing, so it is still available as if we loaded the data
        if (isset($this->data['roles']) && !empty($this->data['roles']) && is_array($this->data['roles'])) {
            $cleanVersion = $this->data['roles'];
            $this->data['roles'] = serialize($this->data['roles']);
            parent::store();
            $this->data['roles'] = $cleanVersion;

            return $this;
        }
        // if this is an array at this point, then its empty and has no values, so set it to null.
        if (is_array($this->data['roles'])) {
            $this->data['roles'] = null;
        }
        parent::store();

        return $this;
    }

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

    /** @noinspection PhpUnused */

    public function hasRole($roleName): bool
    {
        return $this->data['roles'] && in_array($roleName, $this->data['roles']);
    }

    public function newForgottenPasswordLink($string)
    {
        $this->operator->newForgottenPasswordLink($this->email, $string);
    }

    protected function fieldConfig(): array
    {
        return [
            "id" => [
                'name' => "ID",
            ],
            "first_name" => [
                'name' => "First Name",
            ],
            "last_name" => [
                'name' => "Last Name",
            ],
            "active" => [
                'name' => "Active",
                'display' => function ($active) {
                    return $active ? 'Yes' : 'No';
                },
            ],
            "email" => [
                'name' => "Email Address",
            ],
            "password" => [
                'name' => "Password",
                'display' => function (): string {
                    return 'Hidden';
                },
            ],
            "last_login" => [
                'name' => "Last Login",
                'display' => function ($lastLogin) {
                    return $lastLogin ? date('d-m-Y H:i:s', $lastLogin) : 'Never';
                },
            ],
            "salt" => [
                'name' => "Salt",
                'display' => function (): string {
                    return 'Hidden';
                },
            ],
            "modified" => [
                'name' => "Modified",
                'display' => function () {
                    return Utils::HIDE_FROM_DISPLAY;
                },
            ],
            "modified_by" => [
                'name' => "Modified By",
                'display' => function () {
                    return Utils::HIDE_FROM_DISPLAY;
                },
            ],
            "roles" => [
                'name' => "Roles",
                'display' => function ($roles) {
                    $niceNames = [];
                    $roleEntity = new roleEntity();
                    if (is_string($roles)) {
                        $roles = unserialize($roles) ?: [];
                    }
                    if (!empty($roles) && is_array($roles)) {
                        foreach ($roles as $rid) {
                            $roleEntity->load($rid);
                            $niceNames[] = $roleEntity->name;
                        }
                    }

                    return $niceNames === [] ? 'No Roles' : implode(', ', $niceNames);
                },
            ],
        ];
    }
}
