<?php

namespace apexl\Io\modules\user\entities;

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 Exception;

/**
 * @property string|bool $id
 * @property string $first_name
 * @property string $last_name
 * @property string $active
 * @property string $email
 * @property string $password
 * @property string $last_login
 * @property string $created
 * @property string $created_by
 * @property string $modified
 * @property string $modified_by
 * @property string $salt
 * @property string|array $roles
 * @method object getUserByEmail ($email)
 * @method userEntity load(string $id, bool $skipAccessCheck = false)
 * @mixin userOperator
 */
class userEntity extends Entity
{
    public const TABLE = 'users';

    /** @var userOperator */
    protected $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();
    }

    /**
     * __call overload to allow for managing roles.
     * @param $name
     * @param $arguments
     * @return $this
     * @throws Exception
     */
    public function __call($name, $arguments){
        switch($name) {
            //modify load calls to include processed roles
            case 'load':
                if ($this->system->isInstalled()) {
                    parent::__call($name, $arguments);
                    //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();
                    }
                    if (!empty($this->data['settings']) && is_string($this->data['settings'])) {
                        $this->data['settings'] = json_decode($this->data['settings']);
                    }
                }
                return $this;
                //add permissions check to load multiple
            case 'loadMultiple':
                $currentUser = (currentUser::getInstance())::getCurrentUser();
                if ($currentUser->isAllowed('ViewUsers')) {
                    $arguments[0] = array_merge($arguments[0], ["id" => ["users.id", null, "IS NOT"]]);
                }
                break;
        }
        return parent::__call($name, $arguments);
    }

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

        return $this;
    }

    /** @noinspection PhpUnused */
    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
     * @return $this
     */
    public function addRole($id): userEntity
    {
        $this->data['roles'][] = $id;

        return $this;
    }

    /**
     * @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'])) {
            $rolesCleanVersion = $this->data['roles'];
            $this->data['roles'] = serialize($this->data['roles']);
        }
        // if this is an array at this point, then its empty and has no values, so set it to null.
        elseif (is_array($this->data['roles'])) {
            $this->data['roles'] = null;
        }

        if (isset($this->data['settings']) && !empty($this->data['settings'])) {
            $settingsCleanVersion = $this->data['settings'];
            $this->data['settings'] = json_encode($this->data['settings']);
        }

        parent::store();
        if (isset($rolesCleanVersion)) $this->data['roles'] = $rolesCleanVersion;
        if (isset($settingsCleanVersion)) $this->data['settings'] = $settingsCleanVersion;

        return $this;
    }

    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 getNiceName(): string
    {
        return $this->id !== false ? sprintf('%s %s', $this->first_name, $this->last_name) : 'Missing User';
    }

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

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

    /** @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('H:i - d M Y', $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_array($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);
                },
            ],
        ];
    }
}
