<?php
namespace Nibiru\Module\Users\Plugins;
/**
 * Class         Users
 * @package      Nibiru\Module\Users\Plugins
 * @author:      Stephan Kasdorf
 * @date:        04.09.20
 * @copyright:   2020 Nibiru Framework, you may copy the code,
 *               but have to inform the author about where it
 *               is used. So happy copying.
 * @licence:     BSD 4-Old License
 */
use Nibiru\Auth;
use Nibiru\Controller;
use Nibiru\Config;
use Nibiru\Module\Error\Plugins\Message;
use Nibiru\Module\Memcached\Plugins\Memcached;
use Nibiru\Module\Users\Traits\UserForm;
use Nibiru\Module\Users\Users;
use Nibiru\Factory\Db;
use Nibiru\IView;
use Nibiru\Pdo;
use Nibiru\Model\WarehouseLoach;

class User extends Users
{
    use UserForm;
    //check from auth in order to determine if user may login to the system
    private bool $_auth;
    private object $userRegistry;
    private string $status;
    private int $userId;
    public function __construct()
    {
        parent::__construct();
        return $this;
    }

    /**
     * @desc authenticates user in the system and stores the session in order to verify the
     *       the login, then set the session and return true if the user is logged in.
     *       Will update the timeanddate_time field in the timeanddate table with the current time.
     * @return bool
     */
    public function validate( ): bool
    {
        if(array_key_exists('login', Controller::getInstance()->getRequest('', true)) && array_key_exists('password', Controller::getInstance()->getRequest('', true)))
        {
            $this->_auth = Auth::getInstance()->auth(
                Controller::getInstance()->getRequest('login'),
                Controller::getInstance()->getRequest('password'));
            if($this->_auth)
            {
                $timeanddateToUser = Db::loadModel('WarehouseLoach\timeanddateToUser')->selectDatasetByFieldWhere(['name' => 'user_id', 'value' => Controller::getInstance()->getSession('auth')['user_id']]);
                $timeanddate_id = array_shift($timeanddateToUser)['timeanddate_id'];
                Db::loadModel('WarehouseLoach\timeanddate')->updateRowByFieldWhere('timeanddate_id', $timeanddate_id, 'timeanddate_time', date('Y-m-d H:i:s'));
                $this->setUserIdToSessionCookie();
            }
        }
        else
        {
             $this->_auth = $this->isAuthorized();
        }
        return $this->_auth;
    }

    /**
     * @desc will return true if the user is authorized otherwise false
     * @return bool
     */
    public function isAuthorized(): bool
    {
        if(array_key_exists('auth', Controller::getInstance()->getSession('', true)))
        {
            if(array_key_exists('login', Controller::getInstance()->getSession('auth')))
            {
                return true;
            }
            else
            {
                return false;
            }
        }
        else
        {
            return false;
        }
    }

    /**
     * @desc will log out the user and destroy the session
     * @return bool
     */
    public function revokeAuthorized(): bool
    {
        setcookie('user_id', '', [
            'expires' => time() - 3600, // set the cookie to expire one hour ago
            'path' => '/',
            'secure' => false,
            'httponly' => true,
        ]);
        Memcached::init()->runServer()->delete($_SESSION['auth']['user_id']);
        unset($_SESSION['auth']);
        return true;
    }

    /**
     * @desc will update the password of the user, check if the value $user_id is set, if not it will use the current logged in user.
     * @param int $user_id
     * @param string $password
     * @return bool
     */
    public function updatePassword(string $password, int $user_id): bool
    {
        if($user_id == 0)
        {
            $user_id = $_SESSION['auth']['user_id'];
        }
        return Pdo::query('UPDATE user SET user_pass = DES_ENCRYPT("'.$password.'", "'.Config::getInstance()->getConfig()[IView::NIBIRU_SECURITY]["password_hash"].'") WHERE user_id = '.$user_id.';');
    }

    /**
     * @desc will return true if the standard user is present otherwise false
     * @return bool
     * @throws \Exception
     */
    public function checkForStandardUser(): bool
    {
        $userTable = Db::loadModel('WarehouseLoach\user')->loadTableAsArray();
        if(sizeof($userTable)==0)
        {
            return false;
        }
        else
        {
            $standardUser = array_search(self::getUsersRegistry()->user_login, array_column($userTable, 'user_login'));
            if($standardUser === false)
            {
                return false;
            }
            else
            {
                return true;
            }
        }
    }

    /**
     * @desc will check if the user exists in the database
     * @param string $user_login
     * @return bool
     */
    public function checkIfUserExists(string $user_login): bool
    {
        $userTable = Db::loadModel('WarehouseLoach\user')->loadTableAsArray();
        $user = array_search($user_login, array_column($userTable, 'user_login'));
        if($user === false)
        {
            return false;
        }
        else
        {
            return true;
        }
    }

    /**
     * @desc create an object that loads the userdata from the registry, if the user data is not passed by the method
     *       parameter. Also prepares the user data for user updates in the database.
     * @param array $user_data
     * @param bool $isUpdate
     * @return bool
     */
    public function loadUser( array $user_data = [], bool $isUpdate = false ): bool
    {
        $this->userRegistry = new \stdClass();
        if(sizeof($user_data) == 0)
        {
            foreach(self::REGISTRY_USER_KEYS as $key)
            {
                $this->userRegistry->$key = self::getUsersRegistry()->$key;
            }
            return Message::loadInfo('User loaded', ['info' => json_decode(json_encode($this->userRegistry), true)]);
        }
        else
        {
            try {
                if(!$this->checkIfUserExists($user_data['user_login']) || $isUpdate)
                {
                    foreach(self::REGISTRY_USER_KEYS as $key)
                    {
                        if($user_data[$key] == self::FORM_CHECKBOX_ON || $user_data[$key] == self::FORM_CHECKBOX_OFF)
                        {
                            $user_data[$key] = self::FORM_CHECKBOX_SWITCH[$user_data[$key]];
                        }
                        $this->userRegistry->$key = $user_data[$key];
                    }
                    if($isUpdate)
                    {
                        $this->userRegistry->user_id = $user_data['user_id'];
                    }
                    return Message::loadInfo('User loaded', ['info' => json_decode(json_encode($this->userRegistry), true)]);
                } else
                {
                    throw new \Exception('User already exists');
                }

            } catch (\Exception $e) {
                return Message::loadError($e);
            }
        }
    }

    /**
     * @desc will create the initial user for the system, if no user is setup.
     *       Can be found in the application/module/users/settings/users.ini
     *       file.
     * @return int|bool
     * @throws \Exception
     */
    public function createStandardUser(): int|bool
    {
        try {
            Db::loadModel('WarehouseLoach\user')->insertArrayIntoTable([
                'user_firstname' => $this->userRegistry->user_firstname,
                'user_lastname' => $this->userRegistry->user_lastname,
                'user_login' => $this->userRegistry->user_login,
                'user_account_active' => $this->userRegistry->user_active
            ]);
            $lastUserId = Db::loadModel('WarehouseLoach\user')->lastInsertId();

            Db::loadModel('WarehouseLoach\timeanddate')->insertArrayIntoTable([
            ]);
            $lastTimeanddateId = Db::loadModel('WarehouseLoach\timeanddate')->lastInsertId();
            Db::loadModel('WarehouseLoach\timeanddateToUser')->insertArrayIntoTable([
                'timeanddate_id' => $lastTimeanddateId,
                'user_id'        => $lastUserId
            ]);

            Db::loadModel('WarehouseLoach\userToAcl')->insertArrayIntoTable([
                'user_id' => $lastUserId,
                'acl_id' => $this->userRegistry->user_acl_id
            ]);

            Db::loadModel('WarehouseLoach\account')->insertArrayIntoTable([
                'account_name' => $this->userRegistry->user_account_name,
                'account_email' => $this->userRegistry->user_account_email,
                'account_active' => $this->userRegistry->user_account_active
            ]);
            $lastAccountId = Db::loadModel('WarehouseLoach\account')->lastInsertId();
            Db::loadModel('WarehouseLoach\timeanddate')->insertArrayIntoTable([
            ]);
            $lastTimeanddateId = Db::loadModel('WarehouseLoach\timeanddate')->lastInsertId();
            Db::loadModel('WarehouseLoach\timeanddateToAccount')->insertArrayIntoTable([
                'account_id' => $lastAccountId,
                'timeanddate_id' => $lastTimeanddateId
            ]);
            Db::loadModel('WarehouseLoach\userToAccount')->insertArrayIntoTable([
                'account_id' => $lastAccountId,
                'user_id' => $lastUserId
            ]);
            $setPassword = Pdo::query('UPDATE user SET user_pass = DES_ENCRYPT("'.$this->userRegistry->user_pass.'", "'.Config::getInstance()->getConfig()[IView::NIBIRU_SECURITY]["password_hash"].'") WHERE user_id = '.$lastUserId.';');
            if(empty($setPassword))
            {
                return $lastUserId;
            }
            else
            {
                throw new \Exception('Could not set password for user '.$lastUserId . ' with exception value: ' . $setPassword . '!');
            }
        } catch (\Exception $e) {
            return Message::loadError($e);
        }
    }

    /**
     * @desc will return the role of the logged in user
     * @return array
     * @throws \Exception
     */
    public function getRoleByLoggedInUser(): array
    {
        $userToAcl = Db::loadModel('WarehouseLoach\userToAcl')->selectRowByFieldWhere([
            'field' => 'user_id',
            'value' => $_SESSION['auth']['user_id']
        ]);
        return Db::loadModel('WarehouseLoach\acl')->selectRowByFieldWhere([
            'field' => 'acl_id',
            'value' => $userToAcl['acl_id']
        ]);
    }

    /**
     * @desc will load all users from the database and return them as an array
     * @return array
     */
    public function loadUsers(): array
    {
       return Pdo::queryString('select u.user_id as uid,
                                   u.user_firstname as vorname,
                                   u.user_lastname as nachname,
                                   u.user_login as login,
                                   u.user_account_active as aktiv,
                                   a.acl_role as berechtigung,
                                   tad.timeanddate_date as datum_benutzer,
                                   tad.timeanddate_time as zeit_benutzer,
                                   ac.account_name as konto,
                                   ac.account_email as konto_email,
                                   ac.account_active as konto_aktiv,
                                   tadc.timeanddate_date as datum_konto,
                                   tadc.timeanddate_time as zeit_konto
                            from warehouse_loach.user as u
                                join warehouse_loach.user_to_acl as uta on u.user_id = uta.user_id
                                join warehouse_loach.acl as a on uta.acl_id = a.acl_id
                                join warehouse_loach.timeanddate_to_user as ttu on u.user_id = ttu.user_id
                                join warehouse_loach.timeanddate as tad on ttu.timeanddate_id = tad.timeanddate_id
                                join user_to_account uta2 on u.user_id = uta2.user_id
                                join account ac on uta2.account_id = ac.account_id
                                join timeanddate_to_account tta on ac.account_id = tta.account_id
                                join timeanddate tadc on tta.timeanddate_id = tadc.timeanddate_id;', true);
    }

    /**
     * @desc will load a user by the given id
     * @param int $user_id
     * @return array
     */
    public function loadUserById( int $user_id ): array
    {
        $user = Pdo::queryString('select u.user_id as uid,
                                   u.user_firstname as user_firstname,
                                   u.user_lastname as user_lastname,
                                   u.user_login as user_login,
                                   DES_DECRYPT(u.user_pass, "'.Config::getInstance()->getConfig()[IView::NIBIRU_SECURITY]["password_hash"].'") as user_pass,
                                   u.user_account_active as user_account_active,
                                   a.acl_id as user_role_id,
                                   ac.account_name as user_account_name,
                                   ac.account_active as user_account_active,
                                   ac.account_email as user_account_email
                            from warehouse_loach.user as u
                                join warehouse_loach.user_to_acl as uta on u.user_id = uta.user_id
                                join warehouse_loach.acl as a on uta.acl_id = a.acl_id
                                join warehouse_loach.timeanddate_to_user as ttu on u.user_id = ttu.user_id
                                join warehouse_loach.timeanddate as tad on ttu.timeanddate_id = tad.timeanddate_id
                                join user_to_account uta2 on u.user_id = uta2.user_id
                                join account ac on uta2.account_id = ac.account_id
                                join timeanddate_to_account tta on ac.account_id = tta.account_id
                                join timeanddate tadc on tta.timeanddate_id = tadc.timeanddate_id
                            where u.user_id = '.$user_id.';', true);
        $user = array_shift($user);
        return $user;
    }

    /**
     * @desc will delete a user by the given id
     * @param int $user_id
     * @return bool
     */
    public function deleteUserById( int $user_id ): bool
    {
        try {
            $user = $this->loadUserById($user_id);
            if($user['user_role_id'] == 1)
            {
                unset($user);
                throw new \Exception('You can not delete the superuser!');
            }
            else
            {
                $userToAccount = Pdo::queryString('SELECT account_id FROM user_to_account WHERE user_id = '.$user_id.';', true);
                $account_id = array_shift($userToAccount)["account_id"];
                $timeanddateToUser = Pdo::queryString('SELECT timeanddate_id FROM timeanddate_to_user WHERE user_id = '.$user_id.';', true);
                $timeanddate_id = array_shift($timeanddateToUser)["timeanddate_id"];
                Pdo::query('DELETE FROM user_to_acl WHERE user_id = '.$user_id.';');
                Pdo::query('DELETE FROM user_to_account WHERE user_id = '.$user_id.';');
                Pdo::query('DELETE FROM timeanddate_to_user WHERE user_id = '.$user_id.';');
                Pdo::query('DELETE FROM timeanddate_to_account WHERE account_id = '.$account_id.';');
                Pdo::query('DELETE FROM user WHERE user_id = '.$user_id.';');
                Pdo::query('DELETE FROM account WHERE account_id = '.$account_id.';');
                Pdo::query('DELETE FROM timeanddate WHERE timeanddate_id = '.$timeanddate_id.';');
                unset($user['user_pass']);
                return Message::loadInfo("User deleted successfully", $user);
            }
        } catch (\Exception $e) {
            return Message::loadError($e);
        }
    }

    /**
     * @desc will set the user active or inactive
     * @param int $user_id
     * @param int $active
     * @return bool
     */
    public function setUserActive( int $user_id, int $active ): bool
    {
        try {
            if($active)
            {
                $state = '<span class="badge badge-success">Aktiv</span>';
            }
            else
            {
                $state = '<span class="badge badge-danger">Inaktiv</span>';
            }
            $user = $this->loadUserById($user_id);
            if($user['user_role_id'] == 1)
            {
                unset($user);
                throw new \Exception('You can not deactivate the superuser!');
            }
            else
            {
                Db::loadModel('WarehouseLoach\User')->updateRowByFieldWhere('user_id', $user_id, 'user_account_active', $active);
                unset($user['user_pass']);
                return Message::loadInfo("User updated active state successfully: $state", $user);
            }
        } catch (\Exception $e) {
            return Message::loadError($e);
        }
    }

    /**
     * @desc will update the user by the given user array
     * @return bool
     * @throws \Exception
     */
    public function updateUser(): bool
    {
        try {
            $user = (array) $this->userRegistry;
            $userString = '';
            foreach(WarehouseLoach\User::TABLE['fields'] as $field)
            {
                if(array_key_exists($field, $user))
                {
                    if($field == 'user_pass')
                    {
                        $userString .= $field.' = DES_ENCRYPT("'.$user[$field].'", "'.Config::getInstance()->getConfig()[IView::NIBIRU_SECURITY]["password_hash"].'"), ';
                    }
                    else
                    {
                        if($field != 'user_id')
                        {
                            $userString .= $field.' = "'.$user[$field].'", ';
                        }
                    }
                }
            }
            Pdo::query('UPDATE user SET '.substr($userString, 0, -2).' WHERE user_id = '.$user['user_id'].';');
            $userToAccount = Db::loadModel('WarehouseLoach\userToAccount')->selectDatasetByFieldWhere(['name' => 'user_id', 'value' => $user['user_id']]);
            $account_id = array_shift($userToAccount)['account_id'];
            if(isset($account_id))
            {
                $accountString = '';
                foreach(WarehouseLoach\Account::TABLE['fields'] as $field)
                {
                    foreach($user as $key => $value)
                    {
                        if(strstr($key, $field))
                        {
                            $accountString .= $field.' = "'.$user[$key].'", ';
                        }
                    }
                }
                Pdo::query('UPDATE account SET '.substr($accountString, 0, -2).' WHERE account_id = '.$account_id.';');
            }
            else
            {
                throw new \Exception('Did not get valid Account ID, the database could be corrupted for that user! user_id: ' . $user['user_id']);
            }
            Db::loadModel('WarehouseLoach\userToAcl')->updateRowByFieldWhere('user_id', $user['user_id'], 'acl_id', $user['user_acl_id']);
        } catch (\Exception $e) {
            Message::loadError($e);
        }
    }

    /**
     * @desc will set the user_id in a cookie, in order to find the memcached session
     * @return void
     */
    public function setUserIdToSessionCookie(): void
    {
        if(!array_key_exists('user_id', $_COOKIE) && array_key_exists('auth', $_SESSION))
        {
            $ivLength = openssl_cipher_iv_length($this->getUsersRegistry()->{self::USER_ID_ENCRYPTION});
            $iv = openssl_random_pseudo_bytes($ivLength);
            $encryptedUserId = openssl_encrypt($_SESSION['auth']['user_id'], $this->getUsersRegistry()->{self::USER_ID_ENCRYPTION}, $this->getUsersRegistry()->{self::USER_ID_ENCRYPTION_KEY}, 0, $iv);
            $cookieValue = base64_encode($encryptedUserId . '::' . $iv);
            setcookie('user_id', $cookieValue, [
                'expires' => time() + (86400 * 30),
                'path' => '/',
                'secure' => false,      // only send cookie over https
                'httponly' => true,    // make the cookie inaccessible to javascript
            ]);
        }
    }

    /**
     * @desc will get the user_id from the cookie
     * @return int
     */
    public function getUserIdFromSessionCookie(): int
    {
        if(array_key_exists('user_id', $_COOKIE))
        {
            list($encryptedUserId, $iv) = explode('::', base64_decode($_COOKIE['user_id']), 2);
            $decryptedUserId = openssl_decrypt($encryptedUserId, $this->getUsersRegistry()->{self::USER_ID_ENCRYPTION}, $this->getUsersRegistry()->{self::USER_ID_ENCRYPTION_KEY}, 0, $iv);
            return $decryptedUserId;
        }
        else
        {
            return 0;
        }
    }
}