<?php

namespace Velis\Bpm;

use Exception;
use Psr\SimpleCache\InvalidArgumentException;
use RuntimeException;
use Velis\Acl;
use Velis\Api\Token;
use Velis\App;
use Velis\App\User\Authentication;
use Velis\Filter;
use Velis\Lang;
use Velis\Model\DataObject\NoColumnsException;
use Velis\Model\ItemCacheable;

/**
 * BPM Application user
 * @author Olek Procki <olo@velis.pl>
 */
class User extends \Velis\User
{
    /**
     * Minimum value to check list of last password
     */
    public const MIN_PASSWORD_CHANGE_TO_CHECK = 10;

    /**
     * Related person object
     */
    protected ?Person $_person = null;

    private array $recentPasswordHashes = [];


    /**
     * Returns user department
     * @return Department
     */
    public function getDepartment()
    {
        return Department::get($this->department_id);
    }


    /**
     * Returns company
     * @return Company|void
     */
    public function getCompany()
    {
        if ($this->company_id) {
            return new Company($this->company_id);
        }
    }


    /**
     * Returns company id
     * @return int
     */
    public function getCompanyId()
    {
        return $this->company_id;
    }


    /**
     * Returns true if user is company or has company role
     * @return bool
     */
    public function hasCompany()
    {
        return $this->getCompanyId();
    }

    /**
     * Returns true if user is a customer employee
     */
    public function isCustomer()
    {
        return $this->hasCompany() && $this->hasRole('Customer');
    }

    /**
     * Checks if survey tab should be visible for the currently logged user.
     */
    public function canAccessSurvey(): bool
    {
        return $this->isCustomer() || $this->hasPriv('Survey', 'SurveyView');
    }

    /**
     * Returns related person object
     * @return Person
     */
    public function getPerson()
    {
        if (!isset($this->_person)) {
            $this->_person = new Person($this->id());
            $this->_person->append($this);
        }

        return $this->_person;
    }


    /**
     * {@inheritDoc}
     */
    public function modify($checkDiff = false)
    {
        $commit = self::$_db->startTrans();

        try {
            $currInstance = $this->getCurrentInstance();
            parent::modify($checkDiff);

            if (
                $currInstance['company_id']
                && $this['address_no']
                && $currInstance['company_id'] == $this['company_id']
                && $currInstance['address_no'] != $this['address_no']
            ) {
                $person = $this->getPerson();
                $person->setAddress($this['company_id'], $this['address_no']);
            }

            if ($commit) {
                self::$_db->commit();
            }
            return $this;
        } catch (Exception $e) {
            if ($commit) {
                self::$_db->rollback();
            }
            throw $e;
        }
    }


    /**
     * Returns company employees
     *
     * @param Company|int $company
     * @param bool $includeInactive
     * @return static[]
     */
    public static function byCompany($company, $includeInactive = false)
    {
        $companyId = $company instanceof Company ? $company->id() : $company;

        $employees = array();
        foreach (static::listCached() as $user) {
            if ($user->company_id == $companyId) {
                if ($user['active'] || $includeInactive) {
                    $employees[$user->id()] = $user;
                }
            }
        }

        return $employees;
    }


    /**
     * Finds user by email address
     *
     * @param string $email
     * @param Company|int $company
     * @return \Velis\User|void
     */
    public static function byEmail($email, $company = null)
    {
        foreach (static::listCached() as $user) {
            if ($user['email'] == $email) {
                if ($company != null) {
                    $companyId = $company instanceof Company ? $company->id() : $company;
                    if ($user['company_id'] == $companyId) {
                        return $user;
                    }
                } else {
                    return $user;
                }
            }
        }
    }


    /**
     * Returns servicemen
     *
     * @param bool $includeInactive
     * @return static[]
     */
    public static function listServicemen($includeInactive = false)
    {
        $servicemen = array();

        foreach (static::listCached() as $user) {
            if (!$user->company_id) {
                if ($user['active'] || $includeInactive) {
                    $servicemen[$user->id()] = $user;
                }
            }
        }

        return $servicemen;
    }

    /**
     * Returns true if user is a demo user
     * @return bool
     */
    public function isDemoAccount()
    {
        if ($this['is_demo_account'] == '1') {
            return true;
        } else {
            return false;
        }
    }


    /**
     * Returns password as hash md5
     * @param string $password
     * @param string $login
     * @return string
     * @throws NoColumnsException
     */
    public static function getPasswordHash(string $password, string $login): string
    {
        $instance = new static([
            'login' => $login,
            'hash_algorithm' => Authentication::HASH_BCRYPT,
        ]);

        return $instance->_getPasswordHash($password);
    }


    /**
     * Sets activation token
     * @param string $token
     * @return User
     * @throws Exception
     */
    public function setResetPasswordToken($token): User
    {
        $this->applyResetPasswordToken($token);

        if (App::$config->settings->removeTokensOnPasswordChange) {
            Token::invalidateToken([$this->id()]);
        }

        return $this->modify();
    }

    /**
     * @param null $token
     * @return $this
     */
    public function applyResetPasswordToken($token = null): User
    {
        $token = !$token ? Acl::generateToken() : $token;

        if (date($this['reset_password_date_expiry']) < date('Y-m-d H:i:s')) {
            $this['reset_password_token'] = $token;
        }

        $expiry = App::$config->settings->resetPasswordExpiry ?: '8';
        $this['reset_password_date_expiry'] = date('Y-m-d H:i:s', strtotime("+$expiry hours", strtotime(date('Y-m-d H:i:s'))));

        return $this;
    }

    /**
     * Sets new password
     *
     * @param string $password
     * @param bool $onlyHist
     * @return $this
     * @throws Exception
     */
    public function updatePassword($password, $onlyHist = false)
    {
        if (strlen(trim($password))) {
            $this['password'] = self::getPasswordHash($password, $this->getLogin());

            if ($this->_hasField('hash_algorithm')) {
                $this['hash_algorithm'] = Authentication::HASH_BCRYPT;
            }

            if (App::$config->node->pushNotification) {
                $this->_updateNodeToken();
            }

            $this->modify();

            if ($onlyHist && App::$config->settings->strongPassword) {
                $this->_savePasswordHistory($password);
            }

            return $this;
        } else {
            throw new RuntimeException(Lang::get('USER_NO_PASSWORD'));
        }
    }


    /**
     * Generates new user node token
     */
    protected function _updateNodeToken()
    {
        $this['node_token'] = Acl::generateToken();
    }


    /**
     * Validates user's password
     * @param string $password
     * @param bool $mobile login from mobile
     * @param bool $oneTimePass logging in with one time password
     * @return bool
     * @throws Exception
     */
    public function validatePassword($password, $mobile = false, $oneTimePass = false): bool
    {
        if (Authentication::HASH_BCRYPT == $this->hash_algorithm) {
            $result = password_verify($password, $this->password);
        } else {
            $result = $this->password == $this->_getPasswordHash($password);
        }

        if ($mobile && !$result) {
            $mobileExpire = date('Y-m-d H:i:s', strtotime($this->mobile_expire));
            if ($password == $this->mobile_pass && date('Y-m-d H:i:s') < $mobileExpire) {
                $result = true;
                $this->mobile_pass = null;
                $this->modify();
            }
        }

        if ($oneTimePass && !$result) {
            $oneTimePassExpire = date('Y-m-d H:i:s', strtotime($this->one_time_pass_date_expiry));
            if ($password == $this->one_time_pass && date('Y-m-d H:i:s') < $oneTimePassExpire) {
                $result = true;
                $this->one_time_pass = null;
                $this->modify();
            }
        }

        return $result;
    }

    /**
     * Saves password history
     * @string $rawPassword
     * @return int|null
     */
    protected function _savePasswordHistory($rawPassword)
    {
        $result = null;

        if (!$this->selectFromPasswordChanges($rawPassword)) {
            $result = self::$_db->insert(
                'acl.user_password_change_tab',
                [
                    'user_id' => $this['user_id'],
                    'hash' => $this['password'],
                ]
            );

            $this->recentPasswordHashes = [];
        }

        return $result;
    }

    /**
     * Checks if given password had already been used
     * @param string $rawPassword
     * @return bool
     */
    public function selectFromPasswordChanges(string $rawPassword): bool
    {
        if (empty($this->recentPasswordHashes)) {
            $this->recentPasswordHashes = self::$_db->getAll('
                SELECT upc.hash
                FROM acl.user_password_change_tab upc
                WHERE upc.user_id = :user_id
                ORDER BY user_password_change_id DESC
                LIMIT :limit
            ', [
                'user_id' => $this->id(),
                'limit' => self::MIN_PASSWORD_CHANGE_TO_CHECK,
            ]);
        }

        foreach ($this->recentPasswordHashes as $row) {
            if (password_verify($rawPassword, $row['hash'])) {
                return true;
            }
        }

        return false;
    }

    /**
     * Return user's last password change date
     *
     * @return mixed
     */
    public function getLastPasswordChangeDate()
    {
        $params = [
            'user_id' => $this['user_id'],
        ];

        $query = '
            SELECT MAX(date_created)
            FROM acl.user_password_change_tab upc
            WHERE user_id = :user_id
        ';

        return self::$_db->getOne($query, $params);
    }


    /**
     * Checks if we should use a custom mail template for the user
     * @return bool
     */
    public function hasCustomMailTemplate()
    {
        return App::$config->notifications->customerTemplate && $this->hasCompany();
    }


    /**
     * Return account activation route
     */
    public function getActivationRoute()
    {
        return 'activation';
    }
}
