<?php
/**
 * Copyright © 2016 maijindou.com All rights reserved.
 * See COPYING.txt for license details.
 */

namespace Magento\User\Model\ResourceModel;

use Magento\Authorization\Model\Acl\Role\Group as RoleGroup;
use Magento\Authorization\Model\Acl\Role\User as RoleUser;
use Magento\Authorization\Model\UserContextInterface;
use Magento\User\Model\User as ModelUser;

/**
 * ACL user resource
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 */
class User extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
{
    /**
     * @var \Magento\Framework\Acl\CacheInterface
     */
    protected $_aclCache;

    /**
     * Role model
     *
     * @var \Magento\Authorization\Model\RoleFactory
     */
    protected $_roleFactory;

    /**
     * @var \Magento\Framework\Stdlib\DateTime
     */
    protected $dateTime;

    /**
     * Users table
     *
     * @var string
     */
    protected $_usersTable;

    /**
     * Construct
     *
     * @param \Magento\Framework\Model\ResourceModel\Db\Context $context
     * @param \Magento\Framework\Acl\CacheInterface $aclCache
     * @param \Magento\Authorization\Model\RoleFactory $roleFactory
     * @param \Magento\Framework\Stdlib\DateTime $dateTime
     * @param string $connectionName
     */
    public function __construct(
        \Magento\Framework\Model\ResourceModel\Db\Context $context,
        \Magento\Framework\Acl\CacheInterface $aclCache,
        \Magento\Authorization\Model\RoleFactory $roleFactory,
        \Magento\Framework\Stdlib\DateTime $dateTime,
        $connectionName = null
    ) {
        parent::__construct($context, $connectionName);
        $this->_aclCache = $aclCache;
        $this->_roleFactory = $roleFactory;
        $this->dateTime = $dateTime;
        $this->_usersTable = $this->getTable('admin_user');
    }

    /**
     * Define main table
     *
     * @return void
     */
    protected function _construct()
    {
        $this->_init('admin_user', 'user_id');
    }

    /**
     * Initialize unique fields
     *
     * @return $this
     */
    protected function _initUniqueFields()
    {
        $this->_uniqueFields = [
            ['field' => 'email', 'title' => __('Email')],
            ['field' => 'username', 'title' => __('User Name')],
        ];
        return $this;
    }

    /**
     * Authenticate user by $username and $password
     *
     * @param ModelUser $user
     * @return $this
     */
    public function recordLogin(ModelUser $user)
    {
        $connection = $this->getConnection();

        $data = [
            'logdate' => (new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT),
            'lognum' => $user->getLognum() + 1,
        ];

        $condition = ['user_id = ?' => (int)$user->getUserId()];

        $connection->update($this->getMainTable(), $data, $condition);

        return $this;
    }

    /**
     * Load data by specified username
     *
     * @param string $username
     * @return array
     */
    public function loadByUsername($username)
    {
        $connection = $this->getConnection();

        $select = $connection->select()->from($this->getMainTable())->where('username=:username');

        $binds = ['username' => $username];

        return $connection->fetchRow($select, $binds);
    }

    /**
     * Check if user is assigned to any role
     *
     * @param int|ModelUser $user
     * @return null|array
     */
    public function hasAssigned2Role($user)
    {
        if (is_numeric($user)) {
            $userId = $user;
        } elseif ($user instanceof \Magento\Framework\Model\AbstractModel) {
            $userId = $user->getUserId();
        } else {
            return null;
        }

        if ($userId > 0) {
            $connection = $this->getConnection();

            $select = $connection->select();
            $select->from($this->getTable('authorization_role'))
                ->where('parent_id > :parent_id')
                ->where('user_id = :user_id');

            $binds = ['parent_id' => 0, 'user_id' => $userId];

            return $connection->fetchAll($select, $binds);
        } else {
            return null;
        }
    }

    /**
     * Unserialize user extra data after user save
     *
     * @param \Magento\Framework\Model\AbstractModel $user
     * @return $this
     */
    protected function _afterSave(\Magento\Framework\Model\AbstractModel $user)
    {
        $user->setExtra(unserialize($user->getExtra()));
        if ($user->hasRoleId()) {
            $this->_clearUserRoles($user);
            $this->_createUserRole($user->getRoleId(), $user);
        }
        return $this;
    }

    /**
     * Clear all user-specific roles of provided user
     *
     * @param ModelUser $user
     * @return void
     */
    public function _clearUserRoles(ModelUser $user)
    {
        $conditions = ['user_id = ?' => (int)$user->getId()];
        $this->getConnection()->delete($this->getTable('authorization_role'), $conditions);
    }

    /**
     * Create role for provided user of provided type
     *
     * @param int $parentId
     * @param ModelUser $user
     * @return void
     */
    protected function _createUserRole($parentId, ModelUser $user)
    {
        if ($parentId > 0) {
            /** @var \Magento\Authorization\Model\Role $parentRole */
            $parentRole = $this->_roleFactory->create()->load($parentId);
        } else {
            $role = new \Magento\Framework\DataObject();
            $role->setTreeLevel(0);
        }

        if ($parentRole->getId()) {
            $data = new \Magento\Framework\DataObject(
                [
                    'parent_id' => $parentRole->getId(),
                    'tree_level' => $parentRole->getTreeLevel() + 1,
                    'sort_order' => 0,
                    'role_type' => RoleUser::ROLE_TYPE,
                    'user_id' => $user->getId(),
                    'user_type' => UserContextInterface::USER_TYPE_ADMIN,
                    'role_name' => $user->getFirstname(),
                ]
            );

            $insertData = $this->_prepareDataForTable($data, $this->getTable('authorization_role'));
            $this->getConnection()->insert($this->getTable('authorization_role'), $insertData);
            $this->_aclCache->clean();
        }
    }

    /**
     * Unserialize user extra data after user load
     *
     * @param \Magento\Framework\Model\AbstractModel $user
     * @return $this
     */
    protected function _afterLoad(\Magento\Framework\Model\AbstractModel $user)
    {
        if (is_string($user->getExtra())) {
            $user->setExtra(unserialize($user->getExtra()));
        }
        return parent::_afterLoad($user);
    }

    /**
     * Delete user role record with user
     *
     * @param \Magento\Framework\Model\AbstractModel $user
     * @return bool
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function delete(\Magento\Framework\Model\AbstractModel $user)
    {
        $this->_beforeDelete($user);
        $connection = $this->getConnection();

        $uid = $user->getId();
        $connection->beginTransaction();
        try {
            $conditions = ['user_id = ?' => $uid];

            $connection->delete($this->getMainTable(), $conditions);
            $connection->delete($this->getTable('authorization_role'), $conditions);
        } catch (\Magento\Framework\Exception\LocalizedException $e) {
            throw $e;
            return false;
        } catch (\Exception $e) {
            $connection->rollBack();
            return false;
        }
        $connection->commit();
        $this->_afterDelete($user);
        return true;
    }

    /**
     * Get user roles
     *
     * @param \Magento\Framework\Model\AbstractModel $user
     * @return array
     */
    public function getRoles(\Magento\Framework\Model\AbstractModel $user)
    {
        if (!$user->getId()) {
            return [];
        }

        $table = $this->getTable('authorization_role');
        $connection = $this->getConnection();

        $select = $connection->select()->from(
            $table,
            []
        )->joinLeft(
            ['ar' => $table],
            "(ar.role_id = {$table}.parent_id and ar.role_type = '" . RoleGroup::ROLE_TYPE . "')",
            ['role_id']
        )->where(
            "{$table}.user_id = :user_id"
        );

        $binds = ['user_id' => (int)$user->getId()];

        $roles = $connection->fetchCol($select, $binds);

        if ($roles) {
            return $roles;
        }

        return [];
    }

    /**
     * Delete user role
     *
     * @param \Magento\Framework\Model\AbstractModel $user
     * @return $this
     */
    public function deleteFromRole(\Magento\Framework\Model\AbstractModel $user)
    {
        if ($user->getUserId() <= 0) {
            return $this;
        }
        if ($user->getRoleId() <= 0) {
            return $this;
        }

        $dbh = $this->getConnection();

        $condition = ['user_id = ?' => (int)$user->getId(), 'parent_id = ?' => (int)$user->getRoleId()];

        $dbh->delete($this->getTable('authorization_role'), $condition);
        return $this;
    }

    /**
     * Check if role user exists
     *
     * @param \Magento\Framework\Model\AbstractModel $user
     * @return array
     */
    public function roleUserExists(\Magento\Framework\Model\AbstractModel $user)
    {
        if ($user->getUserId() > 0) {
            $roleTable = $this->getTable('authorization_role');

            $dbh = $this->getConnection();

            $binds = ['parent_id' => $user->getRoleId(), 'user_id' => $user->getUserId()];

            $select = $dbh->select()->from($roleTable)->where('parent_id = :parent_id')->where('user_id = :user_id');

            return $dbh->fetchCol($select, $binds);
        } else {
            return [];
        }
    }

    /**
     * Check if user exists
     *
     * @param \Magento\Framework\Model\AbstractModel $user
     * @return array
     */
    public function userExists(\Magento\Framework\Model\AbstractModel $user)
    {
        $connection = $this->getConnection();
        $select = $connection->select();

        $binds = [
            'username' => $user->getUsername(),
            'email' => $user->getEmail(),
            'user_id' => (int)$user->getId(),
        ];

        $select->from(
            $this->getMainTable()
        )->where(
            '(username = :username OR email = :email)'
        )->where(
            'user_id <> :user_id'
        );

        return $connection->fetchRow($select, $binds);
    }

    /**
     * Whether a user's identity is confirmed
     *
     * @param \Magento\Framework\Model\AbstractModel $user
     * @return bool
     */
    public function isUserUnique(\Magento\Framework\Model\AbstractModel $user)
    {
        return !$this->userExists($user);
    }

    /**
     * Save user extra data
     *
     * @param \Magento\Framework\Model\AbstractModel $object
     * @param string $data
     * @return $this
     */
    public function saveExtra($object, $data)
    {
        if ($object->getId()) {
            $this->getConnection()->update(
                $this->getMainTable(),
                ['extra' => $data],
                ['user_id = ?' => (int)$object->getId()]
            );
        }

        return $this;
    }

    /**
     * Retrieve the total user count bypassing any filters applied to collections
     *
     * @return int
     */
    public function countAll()
    {
        $connection = $this->getConnection();
        $select = $connection->select();
        $select->from($this->getMainTable(), 'COUNT(*)');
        $result = (int)$connection->fetchOne($select);
        return $result;
    }

    /**
     * Add validation rules to be applied before saving an entity
     *
     * @return \Zend_Validate_Interface $validator
     */
    public function getValidationRulesBeforeSave()
    {
        $userIdentity = new \Zend_Validate_Callback([$this, 'isUserUnique']);
        $userIdentity->setMessage(
            __('A user with the same user name or email already exists.'),
            \Zend_Validate_Callback::INVALID_VALUE
        );

        return $userIdentity;
    }

    /**
     * Update role users ACL
     *
     * @param \Magento\Authorization\Model\Role $role
     * @return bool
     */
    public function updateRoleUsersAcl(\Magento\Authorization\Model\Role $role)
    {
        $connection = $this->getConnection();
        $users = $role->getRoleUsers();
        $rowsCount = 0;

        if (sizeof($users) > 0) {
            $bind = ['reload_acl_flag' => 1];
            $where = ['user_id IN(?)' => $users];
            $rowsCount = $connection->update($this->_usersTable, $bind, $where);
        }

        return $rowsCount > 0;
    }

    /**
     * Unlock specified user record(s)
     *
     * @param int|int[] $userIds
     * @return int number of affected rows
     */
    public function unlock($userIds)
    {
        if (!is_array($userIds)) {
            $userIds = [$userIds];
        }
        return $this->getConnection()->update(
            $this->getMainTable(),
            ['failures_num' => 0, 'first_failure' => null, 'lock_expires' => null],
            $this->getIdFieldName() . ' IN (' . $this->getConnection()->quote($userIds) . ')'
        );
    }

    /**
     * Lock specified user record(s)
     *
     * @param int|int[] $userIds
     * @param int $exceptId
     * @param int $lifetime
     * @return int number of affected rows
     */
    public function lock($userIds, $exceptId, $lifetime)
    {
        if (!is_array($userIds)) {
            $userIds = [$userIds];
        }
        $exceptId = (int)$exceptId;
        return $this->getConnection()->update(
            $this->getMainTable(),
            ['lock_expires' => $this->dateTime->formatDate(time() + $lifetime)],
            "{$this->getIdFieldName()} IN (" . $this->getConnection()->quote(
                $userIds
            ) . ")\n            AND {$this->getIdFieldName()} <> {$exceptId}"
        );
    }

    /**
     * Increment failures count along with updating lock expire and first failure dates
     *
     * @param ModelUser $user
     * @param int|bool $setLockExpires
     * @param int|bool $setFirstFailure
     * @return void
     */
    public function updateFailure($user, $setLockExpires = false, $setFirstFailure = false)
    {
        $update = ['failures_num' => new \Zend_Db_Expr('failures_num + 1')];
        if (false !== $setFirstFailure) {
            $update['first_failure'] = $this->dateTime->formatDate($setFirstFailure);
            $update['failures_num'] = 1;
        }
        if (false !== $setLockExpires) {
            $update['lock_expires'] = $this->dateTime->formatDate($setLockExpires);
        }
        $this->getConnection()->update(
            $this->getMainTable(),
            $update,
            $this->getConnection()->quoteInto("{$this->getIdFieldName()} = ?", $user->getId())
        );
    }

    /**
     * Purge and get remaining old password hashes
     *
     * @param ModelUser $user
     * @param int $retainLimit
     * @return array
     */
    public function getOldPasswords($user, $retainLimit = 4)
    {
        $userId = (int)$user->getId();
        $table = $this->getTable('admin_passwords');

        // purge expired passwords, except that should retain
        $retainPasswordIds = $this->getConnection()->fetchCol(
            $this->getConnection()
                ->select()
                ->from($table, 'password_id')
                ->where('user_id = :user_id')
                ->order('expires ' . \Magento\Framework\DB\Select::SQL_DESC)
                ->order('password_id ' . \Magento\Framework\DB\Select::SQL_DESC)
                ->limit($retainLimit),
            [':user_id' => $userId]
        );
        $where = ['user_id = ?' => $userId, 'expires <= ?' => time()];
        if ($retainPasswordIds) {
            $where['password_id NOT IN (?)'] = $retainPasswordIds;
        }
        $this->getConnection()->delete($table, $where);

        // now get all remained passwords
        return $this->getConnection()->fetchCol(
            $this->getConnection()
                ->select()
                ->from($table, 'password_hash')
                ->where('user_id = :user_id'),
            [':user_id' => $userId]
        );
    }

    /**
     * Remember a password hash for further usage
     *
     * @param ModelUser $user
     * @param string $passwordHash
     * @param int $lifetime
     * @return void
     */
    public function trackPassword($user, $passwordHash, $lifetime)
    {
        $now = time();
        $this->getConnection()->insert(
            $this->getTable('admin_passwords'),
            [
                'user_id' => $user->getId(),
                'password_hash' => $passwordHash,
                'expires' => $now + $lifetime,
                'last_updated' => $now
            ]
        );
    }

    /**
     * Get latest password for specified user id
     * Possible false positive when password was changed several times with different lifetime configuration
     *
     * @param int $userId
     * @return array
     */
    public function getLatestPassword($userId)
    {
        return $this->getConnection()->fetchRow(
            $this->getConnection()
                ->select()
                ->from($this->getTable('admin_passwords'))
                ->where('user_id = :user_id')
                ->order('password_id ' . \Magento\Framework\DB\Select::SQL_DESC)
                ->limit(1),
            [':user_id' => $userId]
        );
    }
}
