<?php

/**
 * Personal info
 * @property $LastName string
 * @property $FirstName string
 * @property $SecondName string
 * @property Model_Faculty $Faculty
 * @property $FacultyName string
 * @property $FacultyAbbr string
 *
 * Account info
 * @property $ID int
 * @property $Login string
 * @property $EMail string
 * @property $Type string  teacher / student
 * @property $Role string  description
 * @property $RoleMark int
 * @property $IsEnabled bool
 * @property $Code
 * @property $UserAgent
 *
 * Session
 * @property      $SemesterID int
 * @property-read $last_active int
 * @property-read $LoggedIn bool
 * @property-read $UserHash string
 * @property-read $PasswordHash string
 * @property-read $start_time int
 * @property      $RecordBookID int
 */
class User implements ArrayAccess
{
    const RIGHTS_AUTHORIZED = -2;  // no guests
    const RIGHTS_ANYBODY = 1;      // guests too
    const RIGHTS_STUDENT = 2;
    const RIGHTS_TEACHER = 4;
    const RIGHTS_ADMIN = 8;
    const RIGHTS_DEAN = 16;

    /** @var Session */
    protected $_session;

    /** @var Kohana_Config_Group */
    protected $_config;

    protected $_general;

    /** @var  bool responsible for renewal of session lifetime (see instance() )  */
    protected static $_updateSession;

    /** @var  self */
    protected static $_instance;


    /**
     * todo: mark as deprecated cause singleton is an anti-pattern!
     * @param bool $updateSession set false, if session lifetime shouldn't be renewed
     * @return self class instance (singleton-pattern)
     */
    public static function instance($updateSession = true) {
        self::$_updateSession = $updateSession;
        if (!isset(self::$_instance)) {
            $config = Kohana::$config->load('account');
            self::$_instance = new self($config);
        }
        return self::$_instance;
    }


    private function __construct($config = []) {
        $this->_config = $config;
        $this->_general = Model_System::loadConfig("general.json");

        $this->_config['hash_key'] = $this->_general->HashKey;
        $this->_config['hash_method'] = 'sha256';

        $session = $this->_session = Session::instance();

        if (!isset($session['RoleMark']))
            $session['RoleMark'] = 1;

        if (self::$_updateSession) {
            $session->regenerate();
            $session->set('start_time', time());
        }
    }


    /**
     * @param $mask
     * @throws LogicException
     */
    public function checkAccess($mask) {
        $goodBoy = $this->RoleMark & $mask;
        if (!$goodBoy) throw new LogicException(Error::ACCESS_DENIED);
    }

    public function isDean() {
        return (bool) ($this->RoleMark & self::RIGHTS_DEAN);
    }

    public function isAdmin() {
        return (bool) ($this->RoleMark & self::RIGHTS_ADMIN);
    }

    public function isTeacher() {
        return (bool) ($this->RoleMark & self::RIGHTS_TEACHER);
    }

    public function isStudent() {
        return (bool) ($this->RoleMark & self::RIGHTS_STUDENT);
    }

    public function isAuthorized() {
        return (bool) ($this->RoleMark & ~self::RIGHTS_ANYBODY);
    }


    /**
     * Регистрирует нового пользователя и осуществляет вход.
     * Проверяет корректность кода активации и существование
     * аккаунтов с такими же авторизационными данными.
     *
     * @param string $code   Код активации
     * @param string $email  E-Mail адрес
     * @param string $login
     * @param string $password
     * @return string|bool текст ошибки, иначе false
     */
    public function signUp($code, $email, $login, $password) {
        $id = Model_Account::activateAccount($login, $password, $email, $code);

        switch ($id) {
            case -1:
                return 'something went wrong';
            case -2:
                return 'invalid activation code';
            case -3:
                return 'mail already exists';
            case -4:
                return 'login already exists';
        }
        
        $this->initSession($id, $this->hash($password));
        return false;
    }

    /**
     * Проверяет корректность авторизационных данных.
     *
     * @param string $login
     * @param string $password
     * @return bool  true, если авторизация прошла успешно,
     * и false, если данные являются некорректными.
     */
    public function signIn($login, $password) {
        $id = (int) Model_Account::checkAuth($login, $password);
        return $this->initSession($id, $this->hash($password));
    }

    /**
     * Проверяет существования пользователя с заданным globalKey и авторизует его
     *
     * @param string $globalKey
     * @return bool  true, если авторизация прошла успешно,
     * и false, если данные являются некорректными.
     */
    public function signInByOpenID($globalKey) {
        $id = (int) Model_Account::checkAuthOpenID($globalKey);
        return $this->initSession($id, $this->hash($globalKey));
    }

    public function signInByToken($token) {
        $id = (int) Model_Account::checkAuthToken($token);
        return $this->initSession($id, $this->hash($token));
    }

    protected function initSession($id, $passHash) {
        if ($id <= 0) return false;

        $source = $id . Request::$user_agent . Request::$client_ip;
        $userHash = $this->hash($source) . $this->_config['hash_key'];
        $passwordHash = $this->hash($passHash . $this->_config['hash_key']);
        Cookie::set('userhash', $passwordHash);
        
        $userInfo = Model_Account::with($id, $this->_general->SemesterID);

        $session = $this->_session;
        $session->regenerate();
        $session->set('ID', $id);
        $session->set('SemesterID', $this->_general->SemesterID);
        $session->set('LoggedIn', true);
        $session->set('UserHash', $this->hash($userHash));
        $session->set('PasswordHash', $passwordHash);
        $session->set('start_time', time());
        $session->set('RecordBookID', null);
        
        foreach ($userInfo as $key => $value)
            $session->set($key, $value);

        return true;
    }

    /**
     * Проверяет авторизационный статус пользователя и, если
     * пользователь имеет UserAgent и IP, отличные от хранимых
     * в сессии, осуществляет выход из текущего сеанса.
     *
     * @return bool  true, если пользователь авторизован
     */
    public function isSignedIn() {
        if ($this->_session->get('LoggedIn') && !$this->checkHash()) {
            $this->completeSignOut();
        }
        return $this->_session->get('LoggedIn');
    }

    protected function checkHash() {
        $id = $this->_session->get('ID');
        $source = $id . Request::$user_agent . Request::$client_ip;
        $userHash = $this->hash($source) . $this->_config['hash_key'];
        $userCheck = $this->_session->get('UserHash') == $this->hash($userHash);
        $passCheck = Cookie::get('userhash') == $this->_session->get('PasswordHash');
        return $userCheck AND $passCheck;
    }

    /**
     * Завершает текущий сеанс пользователя.
     * @return bool
     */
    public function signOut() {
        if ($this->isSignedIn()) {
            return $this->completeSignOut();
        }
        return false;
    }

    protected function completeSignOut() {
        $this->_session
            ->set('ID', false)
            ->set('LoggedIn', false)
            ->set('UserHash', false);

        Cookie::delete('userhash');
        $this->_session->restart();
        return true;
    }

    /**
     * Проверяет корректность данного пароля для текущего пользователя.
     *
     * @param string $password
     * @return bool
     */
    public function checkPassword($password) {
        if (!$this->isSignedIn())
            return false;

        $passHash = $this->hash($password);
        $computed = $this->hash($passHash . $this->_config['hash_key']);
        return $computed === $this->_session->get('PasswordHash');
    }

    // todo: move to Model_Account
    public function changePassword($old, $new) {
        if (!$this->checkPassword($old))
            return false;

        if (Model_Account::changePassword($this->ID, $new)) {
            $passHash = $this->hash($this->hash($new) . $this->_config['hash_key']);
            $this->_session->set('PasswordHash', $passHash);
            Cookie::set('userhash', $passHash);
            return true;
        }

        return false;
    }

    # todo: move to account
    public function changeProfile($data) {
        $this->checkAccess(User::RIGHTS_TEACHER);

        $res = Model_Teacher::with($this['TeacherID'])->changeInfo(
            $data['lastName'], $data['firstName'], $data['secondName'],
            $data['jobPositionID'], $data['departmentID']
        );

        if ($res) {
            $this->LastName = $data['lastName'];
            $this->FirstName = $data['firstName'];
            $this->SecondName = $data['secondName'];
        }
    }

    /* Info */

    /**
     * Возвращает массив, содержащий пользовательские данные.
     *
     * @return  array
     */
    public function toArray() {
        if ($this->isSignedIn()) {
            return $this->_session->as_array();
        } else {
            return [];
        }
    }


    /* Fields access */

    function __set($name, $value) {
        $this->offsetSet($name, $value);
    }

    function __get($name) {
        # todo: move to $_SESSION
        if ($name == 'Faculty')
            return Model_Faculty::with($this->_session['FacultyID']);

        return $this->offsetGet($name);
    }

    public function offsetSet($offset, $value) {
        $this->_session[$offset] = $value;
    }

    public function offsetGet($offset) {
        if (isset($offset, $this->_session))
            return $this->_session[$offset];

        throw new ErrorException('No such field');
    }

    public function offsetUnset($offset) {
        unset($this->_session[$offset]);
    }

    public function offsetExists($offset) {
        return isset($this->_session[$offset]);
    }

    /**
     * Perform a hmac hash, using the configured method.
     *
     * @param   string $str string to hash
     * @return  string
     */
    protected function hash($str) {
        if (!$this->_config['hash_key']) {
            $this->_config['hash_key'] = $key = md5(time() . Request::$client_ip);
            # TODO: implement
            Model_Account::setHashKey($key);
        }
        return hash_hmac($this->_config['hash_method'], $str, $this->_config['hash_key']);
    }
}