<?php

namespace Velis;

use Velis\Acl\Module;
use Velis\Acl\Priv;
use Velis\Acl\PrivsReloader;
use Velis\Acl\Role;
use Velis\App\MobileNotice;
use Velis\Exception\BusinessLogicException;
use Velis\Model\BaseModel;
use Velis\Model\DataObject\NoColumnsException;

/**
 * ACL model
 * @author Olek Procki <olo@velis.pl>
 */
class Acl extends BaseModel
{
    public const PASSWORD_SPECIAL_CHARS = '!@#$%|_\-';

    public const ROLE_PRIV_MATRIX = 'role-priv';
    public const USER_ROLE_MATRIX = 'user-role';
    public const USER_PRIV_MATRIX = 'user-priv';


    protected static $_privilegedUsers;


    /**
     * Returns matrix types array
     * @return array
     */
    public static function getMatrixTypes()
    {
        return [
            self::ROLE_PRIV_MATRIX => Lang::get('GENERAL_ACL_MATRIX_ROLES_PRIV'),
            self::USER_ROLE_MATRIX => Lang::get('GENERAL_ACL_MATRIX_USR_ROLES'),
            self::USER_PRIV_MATRIX => Lang::get('GENERAL_ACL_MATRIX_USR_PRIV')
        ];
    }


    /**
     * Returns roles to privs matrix
     *
     * @return array
     */
    public static function getRolesToPrivs()
    {
        $matrix = [];
        foreach (self::$_db->getAll("SELECT * FROM acl.role_priv_tab") as $row) {
            $priv = array_key_exists('priv_acro', $row) ? ['app_module_id' => $row['app_module_id'], 'priv_acro' => $row['priv_acro']] : $row['priv_id'];
            $matrix[$row['role_id']][] = $priv;
        }

        return $matrix;
    }


    /**
     * Saves roles/privs matrix
     * @param array $matrix
     * @param string|null $moduleId
     * @throws Exception
     */
    public static function saveRolesToPrivs(array $matrix, $moduleId = null)
    {
        if (empty($matrix) && !$moduleId) {
            return;
        }

        $commit = self::$_db->startTrans();

        try {
            if (App::settings('AclChangelog')) {
                self::$_db->execDML(
                    "CREATE TEMPORARY TABLE role_priv_current_tab
                     AS SELECT * FROM acl.role_priv_tab"
                );
            }

            if (App::$config->session->adapter == 'Redis' || App::hasModule('Mobile') && !empty($matrix)) {
                $modifiedUsers = self::getUsersToRolesToPrivsDiff($matrix);
            }

            if (App::$config->session->adapter == 'Redis') {
                $privsReloader = new PrivsReloader();
                $privsReloader->addUsers($modifiedUsers);
            }

            $queryFields = Priv::hasField('priv_acro') ? "p.priv_acro = rp.priv_acro AND p.app_module_id = rp.app_module_id" : "p.priv_id = rp.priv_id";
            $appModuleId = !(Filter::filterInt($moduleId)) ? $moduleId : Filter::filterInt($moduleId);

            $query = "DELETE FROM acl.role_priv_tab AS rp
                      WHERE EXISTS(
                         SELECT 1 FROM
                            acl.priv_tab AS p
                         WHERE $queryFields
                           " . ($moduleId != null ? " AND p.app_module_id='" . $appModuleId . "'" : "") . "
                      )";

            $params = ['rp.role_id' => array_keys($matrix)];

            if (!Module::hasField('acro')) {
                $params['app_module_id'] = array_keys(App::$config->getEnabledModules());
            }

            $query .= self::$_db->conditions($params);

            self::$_db->execDML($query, $params);

            foreach ($matrix as $roleId => $privs) {
                if (Filter::filterInt($roleId) > 0 || Filter::filterAlnum($roleId)) {
                    foreach ($privs as $module => $priv) {
                        if (Filter::filterInt($priv) > 0 || is_array($priv)) {
                            $params = array(
                                'role_id' => $roleId
                            );
                            if (is_array($priv)) {
                                $params['app_module_id'] = $module;
                                foreach ($priv as $privAcro) {
                                    $params['priv_acro'] = $privAcro;
                                    self::$_db->insert('acl.role_priv_tab', $params, [], true, false, true);
                                }
                            } else {
                                $params['priv_id'] = $priv;
                                self::$_db->insert('acl.role_priv_tab', $params, [], true, false, true);
                            }
                        }
                    }
                }
            }
            if (App::settings('AclChangelog')) {
                $fields = [
                    'invoked_by_user_id',
                    'matrix_acro',
                    'changelog_action_id',
                    'role_id',
                ];
                if (Priv::hasField('priv_acro')) {
                    $fields[] = 'app_module_id';
                    $fields[] = 'priv_acro';
                } else {
                    $fields[] = 'priv_id';
                }

                $sql = "INSERT INTO acl.changelog_tab(" . implode(',', $fields) . ")
                        SELECT :invoked_by_user_id,
                               :matrix_acro,
                               t.*
                        FROM (
                            SELECT 'PrivGrant' AS changelog_action_id, t1.* FROM (
                                SELECT * FROM acl.role_priv_tab
                                EXCEPT
                                SELECT * FROM role_priv_current_tab
                            ) t1
                            UNION ALL
                            SELECT 'PrivRevoke' AS changelog_action_id, t2.* FROM (
                                SELECT * FROM role_priv_current_tab
                                EXCEPT
                                SELECT * FROM acl.role_priv_tab
                            ) t2
                        ) t";

                $params = [
                    'invoked_by_user_id' => App::$user->id(),
                    'matrix_acro' => self::ROLE_PRIV_MATRIX
                ];

                self::$_db->execDML($sql, $params);
                self::$_db->execDML("DROP TABLE role_priv_current_tab");
            }

            if (App::$config->session->adapter == 'Redis') {
                $privsReloader->reload();
            }

            if ($commit) {
                self::$_db->commit();
            }

            if (App::hasModule('Mobile')) {
                $mobileNotice = new MobileNotice(MobileNotice::USER_EDIT);
                $mobileNotice
                    ->addUsers($modifiedUsers)
                    ->send();
            }
        } catch (Exception $e) {
            if ($commit) {
                self::$_db->rollback();
            }

            throw $e;
        }
    }


    /**
     * Zwraca matryce ACL: uzytkownicy-grupy
     *
     * @param int|null $roleId
     * @return array
     */
    public static function getUsersToRoles($roleId = null)
    {
        $params = [];
        $query = "SELECT * FROM acl.user_role_tab AS ur, acl.role_tab AS r
                  WHERE ur.role_id = r.role_id";

        if ($roleId > 0) {
            $params['role_id'] = $roleId;
            $query .= " AND ur.role_id=:role_id";
        }

        $matrix = [];

        foreach (self::$_db->getAll($query, $params) as $row) {
            $matrix[$row['user_id']][] = $row['role_id'];
        }

        return $matrix;
    }

    public static function getRolesToPrivsDiff($matrix)
    {
        $modifiedRoles = [];
        $currentMatrix = self::getRolesToPrivs();

        $activeRoles = Role::listActive();
        $activeModules = Module::listActive();

        foreach ($activeRoles as $roleId => $role) {
            $privs = $matrix[$roleId] ?? [];
            $currentPrivs = $currentMatrix[$roleId] ?? [];

            foreach ($activeModules as $moduleId => $module) {
                $modulePrivs = $privs[$moduleId] ?? [];
                $currentModulePrivs = Arrays::getColumn(
                    Arrays::byValue($currentPrivs, 'app_module_id', $moduleId),
                    'priv_acro'
                ) ?? [];

                if (array_diff($currentModulePrivs, $modulePrivs) || array_diff($modulePrivs, $currentModulePrivs)) {
                    $modifiedRoles[] = $roleId;
                    continue(2);
                }
            }
        }

        return $modifiedRoles;
    }

    public static function getUsersToRolesToPrivsDiff(array $matrix): array
    {
        $modifiedRoles = self::getRolesToPrivsDiff($matrix);
        $usersByRole = [];

        if ($modifiedRoles) {
            /** @var UserRepository */
            $repository = App::$di['db']->getRepository(User::class);
            $usersByRole = $repository->getIdsByRoles($modifiedRoles);
        }

        return $usersByRole;
    }


    /**
     * Saves users/roles matrix
     * @param array $matrix
     * @throws \Exception
     */
    public static function saveUsersToRoles(array $matrix)
    {
        if (sizeof($matrix)) {
            self::$_db->startTrans();
            self::_prepareUsersRolesChange();
            if (App::$config->session->adapter == 'Redis') {
                $privsReloader = new PrivsReloader();
                $privsReloader->addUsers(array_keys($matrix));
            }

            self::$_db->execDML("DELETE FROM acl.user_role_tab");

            foreach ($matrix as $userId => $roles) {
                if (Filter::filterInt($userId) > 0) {
                    foreach ($roles as $roleId) {
                        if (Filter::filterInt($roleId) > 0) {
                            $params = [
                               'user_id' => $userId,
                               'role_id' => $roleId
                            ];
                            self::$_db->insert('acl.user_role_tab', $params);
                        }
                    }
                }
            }

            if (App::$config->session->adapter == 'Redis') {
                $privsReloader->reload();
            }
            self::_executeUsersRolesChange();
            self::$_db->commit();

            if (App::hasModule('Mobile')) {
                $mobileNotice = new MobileNotice(MobileNotice::USER_EDIT);
                $mobileNotice
                    ->addUsers(array_keys($matrix))
                    ->send();
            }
        }
    }


    /**
     * Creates temporary table for tracking changes in roles assigned to a user.
     */
    protected static function _prepareUsersRolesChange()
    {
        if (App::settings('AclChangelog')) {
            self::$_db->execDML(
                "CREATE TEMPORARY TABLE user_role_current_tab
                 AS SELECT * FROM acl.user_role_tab"
            );
        }
    }


    /**
     * Logs changes in roles assigned to a user and cleans up the
     * temporary table.
     */
    protected static function _executeUsersRolesChange()
    {
        if (App::settings('AclChangelog')) {
            self::logUserRolesChange();
            self::$_db->execDML("DROP TABLE user_role_current_tab");
        }
    }


    /**
     * Returns user privs directly assigned
     *
     * @param int $userId
     * @return array
     */
    public static function getUserPrivs($userId)
    {
        if (Priv::hasField('priv_acro')) {
            $query = "SELECT up.user_id, up.direction, up.priv_acro, up.app_module_id
                         FROM acl.user_priv_tab AS up
                         WHERE 1=1
                           AND up.user_id       = :user_id";
        } else {
            $query = "SELECT up.user_id, up.direction, up.priv_id, p.acro AS priv_acro, am.acro AS app_module_acro
                         FROM acl.user_priv_tab AS up, acl.priv_tab AS p, acl.app_module_tab AS am
                         WHERE 1=1
                           AND p.priv_id = up.priv_id
                           AND am.app_module_id = p.app_module_id
                           AND up.user_id       = :user_id";
        }

        return self::$_db->getAll($query, array('user_id' => $userId));
    }


    /**
     * Returns user privs assigned by role
     *
     * @param int $userId
     * @return array
     */
    public static function getUserRolesPrivs($userId)
    {
        if (Priv::hasField('priv_acro')) {
            $query = "SELECT ur.user_id, rp.priv_acro, rp.app_module_id
               FROM acl.role_priv_tab AS rp,
                    acl.user_role_tab AS ur
               WHERE 1=1
                 AND ur.role_id       = rp.role_id
                 AND ur.user_id       = :user_id";
        } else {
            $query = "SELECT ur.user_id, rp.priv_id AS priv_id, p.acro AS priv_acro, am.acro AS app_module_acro
               FROM acl.role_priv_tab AS rp, acl.user_role_tab AS ur, acl.priv_tab AS p, acl.app_module_tab AS am
               WHERE 1=1
                 AND ur.role_id       = rp.role_id
                 AND ur.user_id       = :user_id
                 AND p.priv_id = rp.priv_id
                 AND am.app_module_id = p.app_module_id";
        }

        return self::$_db->getAll($query, array('user_id' => $userId));
    }


    /**
     * Saves user-priv matrix (for selected one user)
     *
     * @param array $matrix
     * @param int $userId
     * @throws \Exception
     */
    public static function saveUserToPrivs(array $matrix, $userId)
    {
        if (!sizeof($matrix)) {
            return;
        }

        $commit = self::$_db->startTrans();

        try {
            self::_prepareUsersPrivsChange();
            if (App::$config->session->adapter == 'Redis') {
                $privsReloader = new PrivsReloader();
                $privsReloader->addUsers($userId);
            }

            $sql = "DELETE FROM acl.user_priv_tab up WHERE up.user_id = :user_id";

            if (!App::isSuper() && Priv::hasField('restricted_granting')) {
                $sql .= " AND NOT EXISTS(SELECT 1 FROM acl.priv_tab p WHERE p.app_module_id = up.app_module_id AND p.priv_acro = up.priv_acro AND p.restricted_granting = 1)";
            }

            self::$_db->execDML($sql, ['user_id' => $userId]);

            foreach ($matrix as $privId => $priv) {
                if (!($priv == "+" || $priv == "-" || is_array($priv))) {
                    continue;
                }

                if (!(Filter::filterInt($privId) > 0 || Filter::filterAlpha($privId))) {
                    continue;
                }

                $params = array(
                    'user_id'   => $userId,
                );

                if (Priv::hasField('priv_acro')) {
                    $sql = 'INSERT INTO acl.user_priv_tab (user_id, app_module_id, priv_acro, direction)
                            SELECT :user_id, :app_module_id::varchar, :priv_acro::varchar, :direction::char FROM acl.priv_tab p
                            WHERE p.app_module_id = :app_module_id AND p.priv_acro = :priv_acro';
                } else {
                    $sql = 'INSERT INTO acl.user_priv_tab (user_id, priv_id, direction)
                            SELECT :user_id, :priv_id, :direction::char FROM acl.priv_tab p
                            WHERE p.priv_id = :priv_id';
                }

                if (!App::isSuper() && Priv::hasField('restricted_granting')) {
                    $sql .= ' AND p.restricted_granting = 0';
                }

                $insertStmt = self::$_db->prepare($sql);

                if (is_array($priv)) {
                    $params['app_module_id'] = $privId;
                    foreach ($priv as $key => $direction) {
                        if ($direction == "+" || $direction == "-") {
                            $params['priv_acro'] = $key;
                            $params['direction'] = $direction;
                            $insertStmt->execute($params);
                        }
                    }
                } else {
                    $params['priv_id'] = $privId;
                    $params['direction'] = $priv;
                    $insertStmt->execute($params);
                }
            }

            if (App::$config->session->adapter == 'Redis') {
                $privsReloader->reload();
            }
            self::_executeUsersPrivsChange();

            if ($commit) {
                self::$_db->commit();
            }

            if (App::hasModule('Mobile')) {
                $mobileNotice = new MobileNotice(MobileNotice::USER_EDIT);
                $mobileNotice
                    ->addUsers($userId)
                    ->send();
            }
        } catch (\Exception $e) {
            if ($commit) {
                self::$_db->rollback();
            }

            throw $e;
        }
    }


    /**
     * Creates temporary table for tracking changes in privileges assigned
     * to a user.
     */
    protected static function _prepareUsersPrivsChange()
    {
        if (App::settings('AclChangelog')) {
            self::$_db->execDML(
                "CREATE TEMPORARY TABLE user_priv_current_tab
                 AS SELECT * FROM acl.user_priv_tab"
            );
        }
    }

    /**
     * Logs changes in privileges assigned to a user and cleans up the
     * temporary table.
     */
    protected static function _executeUsersPrivsChange()
    {
        if (App::settings('AclChangelog')) {
            self::_logUserPrivsChange();
            self::$_db->execDML("DROP TABLE user_priv_current_tab");
        }
    }


    /**
     * Saves user-priv (for selected one user)
     *
     * @param Acl\Priv|int $priv
     * @param string $direction
     * @param int $userId
     * @throws \Exception
     */
    public static function saveUserPriv($priv, $direction, $userId)
    {
        if ($priv instanceof Priv) {
            $privId = $priv->id();
        } else {
            $privId = $priv;
        }

        self::$_db->startTrans();
        self::_prepareUsersPrivsChange();
        if (App::$config->session->adapter == 'Redis') {
            $privsReloader = new PrivsReloader();
            $privsReloader->addUsers($userId);
        }

        $params = array('user_id' => $userId);

        if (Priv::hasField('priv_acro')) {
            $params['priv_acro'] = $privId['priv_acro'];
            $params['app_module_id'] = $privId['app_module_id'];
        } else {
            $params['priv_id'] = $privId;
        }

        self::$_db->execDML(
            "DELETE FROM acl.user_priv_tab WHERE user_id = :user_id " . self::$_db->conditions($params),
            $params
        );

        if ($direction == "+" || $direction == "-") {
            $params['direction'] = $direction;

            self::$_db->insert(
                "acl.user_priv_tab",
                $params
            );
        }
        if (App::$config->session->adapter == 'Redis') {
            $privsReloader->reload();
        }
        self::_executeUsersPrivsChange();
        self::$_db->commit();

        if (App::hasModule('Mobile')) {
            $mobileNotice = new MobileNotice(MobileNotice::USER_EDIT);
            $mobileNotice
                ->addUsers($userId)
                ->send();
        }
    }


    /**
     * Logs user privs change
     */
    protected static function _logUserPrivsChange()
    {
        $fields = [
            'invoked_by_user_id',
            'matrix_acro',
            'changelog_action_id',
            'user_id',
        ];
        if (Priv::hasField('priv_acro')) {
            $fields[] = 'app_module_id';
            $fields[] = 'priv_acro';
        } else {
            $fields[] = 'priv_id';
        }
        $fields[] = 'direction';

        $sql = "INSERT INTO acl.changelog_tab(" . implode(',', $fields) . ")
                SELECT :invoked_by_user_id,
                       :matrix_acro,
                       t.*
                FROM (
                    SELECT 'PrivAssignmentAdd' AS changelog_action_id, t1.* FROM (
                        SELECT * FROM acl.user_priv_tab
                        EXCEPT
                        SELECT * FROM user_priv_current_tab
                    ) t1
                    UNION ALL
                    SELECT 'PrivAssignmentRemove' AS changelog_action_id, t2.* FROM (
                        SELECT * FROM user_priv_current_tab
                        EXCEPT SELECT * FROM acl.user_priv_tab
                    ) t2
                ) t";

        $params = [
            'invoked_by_user_id' => App::$user->id(),
            'matrix_acro' => self::USER_PRIV_MATRIX
        ];

        self::$_db->execDML($sql, $params);
    }


    /**
     * Logs user roles change
     */
    public static function logUserRolesChange()
    {
        $sql = "INSERT INTO acl.changelog_tab(
                    invoked_by_user_id,
                    matrix_acro,
                    changelog_action_id,
                    user_id,
                    role_id
                )
                SELECT :invoked_by_user_id,
                       :matrix_acro,
                       t.*
                FROM (
                    SELECT 'RoleGrant' AS changelog_action_id, t1.* FROM (
                        SELECT * FROM acl.user_role_tab
                        EXCEPT
                        SELECT * FROM user_role_current_tab
                    ) t1
                    UNION ALL
                    SELECT 'RoleRevoke' AS changelog_action_id, t2.* FROM (
                        SELECT * FROM user_role_current_tab
                        EXCEPT
                        SELECT * FROM acl.user_role_tab
                    ) t2
                ) t";

        $params = [
            'invoked_by_user_id' => App::$user->id(),
            'matrix_acro' => self::USER_ROLE_MATRIX
        ];
        self::$_db->execDML($sql, $params);
    }


    /**
     * Returns list of privileged users
     *
     * @param string $module
     * @param string $priv
     * @return \User\User[]
     * @throws NoColumnsException
     */
    public static function getPrivilegedUsers(string $module, string $priv): array
    {
        if (!isset(self::$_privilegedUsers[$module][$priv])) {
            $dataSource = static::getUserPrivsDatasource();
            $moduleField = Module::hasField('acro') ? 'uep.app_module_acro' : 'uep.app_module_id';
            $order = \User\User::ORDER_NAME;

            $query = "
                SELECT u.*
                FROM $dataSource uep JOIN acl.user u USING (user_id)
                WHERE uep.priv_acro = :priv AND $moduleField = :module AND u.active = 1
                ORDER BY $order
            ";

            $params = [
                'priv' => $priv,
                'module' => $module,
            ];

            $users = [];

            foreach (self::$_db->getAll($query, $params) as $row) {
                $users[$row['user_id']] = new \User\User($row);
            }
            self::$_privilegedUsers[$module][$priv] = $users;
        }

        return self::$_privilegedUsers[$module][$priv];
    }

    /**
     * @return \User\User[]
     * @throws NoColumnsException
     * @deprecated use Acl::getPrivilegedUsers() instead
     */
    public static function getPriviledgedUsers(): array
    {
        if (func_num_args() == 1 && is_array(func_get_arg(0))) {
            list($module, $priv) = func_get_arg(0);
        } else {
            list($module, $priv) = func_get_args();
        }

        return static::getPrivilegedUsers($module, $priv);
    }

    /**
     * Generates strong random password
     * @return string
     */
    private static function _tmpPasswordStrong()
    {
        $ucLetters = str_shuffle('ABCDEFGHIJKLMNOPQRSTUVWXYZ');
        $lcLetters = str_shuffle(strtolower($ucLetters));
        $numbers = str_shuffle('0123456789');
        $specialChars = str_shuffle('!$_-/.,');

        $totalCharactersCount = 12;
        $specialCharsCount = rand(1, 3);
        $numbersCount = rand(2, 4);
        $lettersCount = $totalCharactersCount - $specialCharsCount - $numbersCount;
        $ucLettersCount = (int) ($lettersCount / 2) + rand(-1, 1);
        $lcLettersCount = $lettersCount - $ucLettersCount;

        $selectedUcLetters = substr($ucLetters, 0, $ucLettersCount);
        $selectedLcLetters = substr($lcLetters, 0, $lcLettersCount);
        $selectedNumbers = substr($numbers, 0, $numbersCount);
        $selectedSpecialChars = substr($specialChars, 0, $specialCharsCount);

        return str_shuffle($selectedUcLetters . $selectedLcLetters . $selectedNumbers . $selectedSpecialChars);
    }


    /**
     * Generates weak random password
     * @return string
     */
    private static function _tmpPasswordWeak()
    {
        $min = rand(4, 5);
        $max = rand(6, 9);

        //@codingStandardsIgnoreStart
        $inHash = '0123456789abcdefghijklnopqrstuvwxyz';
        //@codingStandardsIgnoreEnd
        $split = str_split($inHash);
        shuffle($split);

        $i = 0;
        $R = rand($min, $max);
        $passArray = [];

        while ($i < $R) {
            $item = array_rand($split);
            if (in_array($item, $passArray)) {
                continue;
            }

            $passArray[] = $split[$item];
            $i += 1;
        }

        return implode('', $passArray);
    }


    /**
     * Generates random password
     * @return string
     * @throws Exception
     */
    public static function tmpPassword()
    {
        if ((int) App::$config->settings->strongPassword == 1) {
            $pass = self::_tmpPasswordStrong();
        } else {
            $pass = self::_tmpPasswordWeak();
        }

        if (empty($pass) || strlen($pass) < 2) {
            throw new Exception(Lang::get('GENERAL_NO_SAFETY_REQUIREMENTS'), 0);
        }

        return $pass;
    }

    /**
     * Generates random token
     */
    public static function generateToken(): string
    {
        if (function_exists("openssl_random_pseudo_bytes")) {
            return bin2hex(openssl_random_pseudo_bytes(32));
        } else {
            $salt = 'velis7hgtyujkirtfgty5678';

            return sha1(microtime() . $salt);
        }
    }


    /**
     * Checks password complexity
     *
     * @param string $password
     * @param string|null $login
     * @return int degree of complexity: 1 - too short, 2 - weak, 3 - medium, 4 - strong
     */
    public static function checkPasswordStrength($password, $login = null)
    {
        if ($login !== null && strstr($password, $login)) {
            return 1;
        }

        if (preg_match("/" . App::$config->password->level4 . "/", $password)) {
            return 4;
        } elseif (preg_match("/" . App::$config->password->level3 . "/", $password)) {
            return 3;
        } elseif (preg_match("/" . App::$config->password->level2 . "/", $password)) {
            return 2;
        } else {
            return 1;
        }
    }

    /**
     * Copies privileges and role from one user to other users
     *
     * @param int $sourceUserId
     * @param int|array $targetUser
     * @throws BusinessLogicException
     * @throws \Exception
     */
    public static function copyPrivs($sourceUserId, $targetUser)
    {
        self::$_db->startTrans();

        try {
            if (!is_array($targetUser)) {
                $targetUser = [$targetUser];
            }

            self::_prepareUsersRolesChange();
            self::_prepareUsersPrivsChange();

            foreach ($targetUser as $targetUserId) {
                if ($sourceUserId == $targetUserId) {
                    throw new BusinessLogicException(Lang::get('USER_TARGET_USER_SAME_AS_SOURCE_USER'));
                }

                self::$_db->execDML(
                    "DELETE FROM acl.user_role_tab WHERE user_id = :user_id",
                    ['user_id' => $targetUserId]
                );

                self::$_db->execDML(
                    "DELETE FROM acl.user_priv_tab WHERE user_id = :user_id",
                    ['user_id' => $targetUserId]
                );

                self::$_db->execDML(
                    "
                        INSERT INTO acl.user_role_tab (user_id, role_id)
                        SELECT :target_user_id, role_id
                        FROM acl.user_role_tab
                        WHERE user_id = :source_user_id
                    ",
                    [
                        'target_user_id' => $targetUserId,
                        'source_user_id' => $sourceUserId,
                    ]
                );

                self::$_db->execDML(
                    "
                        INSERT INTO acl.user_priv_tab (user_id, app_module_id, priv_acro, direction)
                        SELECT :target_user_id, app_module_id, priv_acro, direction
                        FROM acl.user_priv_tab
                        WHERE user_id = :source_user_id
                    ",
                    [
                        'target_user_id' => $targetUserId,
                        'source_user_id' => $sourceUserId,
                    ]
                );
            }

            self::_executeUsersRolesChange();
            self::_executeUsersPrivsChange();
            self::$_db->commit();
        } catch (\Exception $ex) {
            self::$_db->rollback();
            throw $ex;
        }
    }

    /**
     * Get name of a database table or view, containing user privileges
     * @return string
     */
    public static function getUserPrivsDatasource(): string
    {
        if (App::$config->settings->userPrivDatasource) {
            return App::$config->settings->userPrivDatasource;
        }

        return self::$_db->tableExists('acl.user_eff_priv_tab') ? 'acl.user_eff_priv_tab' : 'acl.user_eff_priv';
    }
}
