<?php

namespace QUI\MasterLogin;

use PDO;
use QUI;
use QUI\Exception;
use QUI\Users\User;
use QUI\Utils\Security\Orthos;

use function array_filter;
use function array_map;
use function array_merge;
use function array_search;
use function array_unique;
use function array_values;
use function date;
use function explode;
use function implode;
use function in_array;
use function mb_strpos;
use function mb_substr;

/**
 * Class Manager
 *
 * Main manager class for quiqqer/masterlogin
 */
class Manager
{
    /**
     * Tables
     */
    const TBL_LOG = 'masterlogin_log';

    /**
     * Permissions
     */
    const PERMISSION_CAN_USE = 'quiqqer.masterlogin.can_use';
    const PERMISSION_SLAVES = 'quiqqer.masterlogin.slaves';

    /**
     * Session variables
     */
    const SESSION_MASTER_USER_ID = 'session_master_user_id';
    const SESSION_LOG_ID = 'session_log_id';

    /**
     * Get IDs of all users that $User is allowed to log in as
     *
     * @param User|null $user $User (optional) - [default: current session user]
     * @return string[]|null - List of User UUIDs; returns null if there is no restriction to specific users (SU)
     * @throws Exception
     */
    public static function getLoginUserIds(?User $user = null): ?array
    {
        if (empty($user)) {
            // Check if the logged-in user is actually a master user logged in as another user.
            $user = QUI::getUserBySession();
            $masterUserIdFromSession = QUI::getSession()->get(self::SESSION_MASTER_USER_ID);

            if (!empty($masterUserIdFromSession)) {
                try {
                    $user = QUI::getUsers()->get($masterUserIdFromSession);
                } catch (\Exception $Exception) {
                    QUI\System\Log::writeException($Exception);
                }
            }
        }

        if ($user->isSU()) {
            return null;
        }

        // Contains list of user and/or groups the current user is allowed to log in as.
        // If this list is empty, every user is allowed.
        $listOfAllowedLoginEntities = $user->getPermission(self::PERMISSION_SLAVES);

        if (empty($listOfAllowedLoginEntities)) {
            return null;
        }

        $slaves = explode(',', $listOfAllowedLoginEntities);

        $userUuids = array_filter($slaves, function ($entry) {
            return mb_strpos($entry, 'u') === 0;
        });

        $groupUuids = array_filter($slaves, function ($entry) {
            return mb_strpos($entry, 'g') === 0;
        });

        // Remove entity identification letter
        $removeIdLetter = function ($value) {
            return mb_substr($value, 1);
        };

        $userUuids = array_map($removeIdLetter, $userUuids);
        $groupUuids = array_map($removeIdLetter, $groupUuids);

        // Parse user IDs from group(s)
        $Groups = QUI::getGroups();

        foreach ($groupUuids as $groupId) {
            try {
                $Group = $Groups->get($groupId);
                $userUuids = array_merge(
                    $userUuids,
                    $Group->getUserIds()
                );
            } catch (\Exception $Exception) {
                QUI\System\Log::writeException($Exception);
            }
        }

        $k = array_search($user->getUUID(), $userUuids);

        if ($k !== false) {
            unset($userUuids[$k]);
        }

        return array_values(array_unique($userUuids));
    }

    /**
     * Search list of users that $MasterUser is allowed to log in as
     *
     * @throws Exception
     */
    public static function searchLoginUsers(array $searchParams, null | User $MasterUser = null): array | int
    {
        $Grid = new QUI\Utils\Grid($searchParams);
        $gridParams = $Grid->parseDBParams($searchParams);
        $usersTbl = QUI::getDBTableName('users');
        $addressTbl = QUI::getDBTableName('users_address');
        $binds = [];
        $where = [];
        $count = !empty($searchParams['count']);
        $allowedUserIds = self::getLoginUserIds($MasterUser);

        if ($count) {
            $sql = "SELECT COUNT(*)";
        } else {
            $sql = "SELECT u.`id`, u.`uuid`, u.`username`, u.`firstname` as user_firstname, u.`lastname` as user_lastname, "
                . "a.`firstname` as address_firstname, a.`lastname` as address_lastname, u.`email`, a.`company`";
        }

        $sql .= " FROM `" . $usersTbl . "` as u LEFT JOIN `" . $addressTbl . "` as a ON u.`id` = a.`uid`";
        $where[] = 'u.`active` = 1';

        if ($allowedUserIds !== null) {
            $where[] = 'u.`uuid` IN (\'' . implode('\',\'', $allowedUserIds) . '\')';
        }

        if (!empty($searchParams['search'])) {
            $whereOR = [];

            $searchColumns = [
                'u.`id`',
                'u.`uuid`',
                'u.`username`',
                'u.`firstname`',
                'u.`lastname`',
                'a.`firstname`',
                'a.`lastname`',
                'u.`email`',
                'a.`company`',
                'a.`city`'
            ];

            foreach ($searchColumns as $column) {
                $whereOR[] = $column . ' LIKE :search';
                $binds['search'] = [
                    'value' => '%' . $searchParams['search'] . '%',
                    'type' => PDO::PARAM_STR
                ];
            }

            $where[] = '(' . implode(' OR ', $whereOR) . ')';
        }

        // build WHERE query string
        $sql .= " WHERE " . implode(" AND ", $where);

        // ORDER
        if (!empty($searchParams['sortOn'])) {
            if (!empty($searchParams['sortBy'])) {
                $sortDirection = Orthos::clear($searchParams['sortBy']);
            } else {
                $sortDirection = "ASC";
            }

            $sortColumns = match (Orthos::clear($searchParams['sortOn'])) {
                'name' => [
                    'u.`username`',
                    'u.`firstname`',
                    'u.`lastname`',
                    'a.`firstname`',
                    'a.`lastname`'
                ],
                'email' => [
                    'u.`email`'
                ],
                'company' => [
                    'a.`company`'
                ],
                default => [
                    'u.`id`',
                ],
            };

            $order = [];

            foreach ($sortColumns as $column) {
                $order[] = "$column $sortDirection";
            }

            $sql .= " ORDER BY " . implode(',', $order);
        }

        // LIMIT
        if (!empty($gridParams['limit']) && !$count) {
            $sql .= " LIMIT " . $gridParams['limit'];
        } else {
            if (!$count) {
                $sql .= " LIMIT " . 20;
            }
        }

        $Stmt = QUI::getPDO()->prepare($sql);

        // bind search values
        foreach ($binds as $var => $bind) {
            $Stmt->bindValue(':' . $var, $bind['value'], $bind['type']);
        }

        try {
            $Stmt->execute();
            $result = $Stmt->fetchAll(PDO::FETCH_ASSOC);
        } catch (\Exception $Exception) {
            QUI\System\Log::addError(
                self::class . ' :: searchUsers() -> ' . $Exception->getMessage()
            );

            return [];
        }

        if ($count) {
            return (int)current(current($result));
        }

        $users = [];

        foreach ($result as $row) {
            $userId = (int)$row['id'];

            if (isset($users[$userId])) {
                continue;
            }

            // Name
            $name = $row['user_firstname'] . ' ' . $row['user_lastname'];

            if (!empty($row['address_firstname']) && !empty($row['address_lastname'])) {
                $name = $row['address_firstname'] . ' ' . $row['address_lastname'];
            }

            if (empty(trim($name))) {
                $name = $row['username'];
            }

            $users[$userId] = [
                'id' => $userId,
                'uuid' => $row['uuid'],
                'name' => $name,
                'email' => $row['email'],
                'company' => $row['company']
            ];
        }

        return array_values($users);
    }

    /**
     * Switches the current session user to $slaveUserId (requires permission!)
     *
     * @param string $slaveUserId
     * @return void
     *
     * @throws PermissionDeniedException
     * @throws Exception
     */
    public static function logInAs(string $slaveUserId): void
    {
        $allowedLoginUserIds = self::getLoginUserIds();

        if ($allowedLoginUserIds !== null && !in_array($slaveUserId, $allowedLoginUserIds)) {
            throw new PermissionDeniedException();
        }

        $User = QUI::getUsers();
        $Session = QUI::getSession();
        $masterUserId = $Session->get(self::SESSION_MASTER_USER_ID);

        $Slave = QUI::getUsers()->get($slaveUserId);
        $slaveUserId = $Slave->getUUID();

        if (empty($masterUserId)) {
            $SessionUser = QUI::getUserBySession();
            $masterUserId = $SessionUser->getUUID();
            $Session->set(self::SESSION_MASTER_USER_ID, $masterUserId);
        }

        $Session->set('uid', $slaveUserId);

        // Security hash must be updated for slave user
        try {
            QUI::getDataBase()->update(
                $User::table(),
                ['secHash' => $User->getSecHash()],
                ['uuid' => $slaveUserId]
            );
        } catch (\Exception $Exception) {
            // @todo falls nicht mehr existiert
        }

        // Create log entry
        try {
            QUI::getDataBase()->insert(
                self::getLogTable(),
                [
                    'id' => QUI\Utils\Uuid::get(),
                    'master_id' => $masterUserId,
                    'slave_id' => $slaveUserId,
                    'start' => date('Y-m-d H:i:s'),
                    'sec_hash' => QUI::getUsers()->getSecHash()
                ]
            );

            $Session->set(self::SESSION_LOG_ID, QUI::getDataBase()->getPDO()->lastInsertId('id'));
        } catch (\Exception $Exception) {
            QUI\System\Log::writeException($Exception);
        }
    }

    /**
     * Switches the current session user to the original user
     *
     * @return void
     */
    public static function logBack(): void
    {
        if (!self::isLoggedInAsSlave()) {
            return;
        }

        $Session = QUI::getSession();
        $masterUserId = $Session->get(self::SESSION_MASTER_USER_ID);

        $Session->set('uid', $masterUserId);
        $Session->set(self::SESSION_MASTER_USER_ID, false);

        // Update log entry
        $logId = $Session->get(self::SESSION_LOG_ID);

        if (empty($logId)) {
            return;
        }

        try {
            QUI::getDataBase()->update(
                self::getLogTable(),
                ['end' => date('Y-m-d H:i:s')],
                ['id' => $logId]
            );

            $Session->set(self::SESSION_LOG_ID, false);
        } catch (\Exception $Exception) {
            QUI\System\Log::writeException($Exception);
        }
    }

    /**
     * Checks if the current Session user is logged in as a slave user
     *
     * @return bool
     */
    public static function isLoggedInAsSlave(): bool
    {
        $masterUserId = QUI::getSession()->get(self::SESSION_MASTER_USER_ID);
        return !empty($masterUserId);
    }

    /**
     * Check if $User is allowed to use the Master Login feature and throws and exception if not
     *
     * @param User|null $User $User (optional) - [default: session user]
     * @return void
     *
     * @throws PermissionDeniedException
     */
    public static function checkUsePermission(null | User $User = null): void
    {
        if (!self::canUse($User)) {
            throw new PermissionDeniedException();
        }
    }

    /**
     * Check if $User is allowed to use the Master Login feature
     *
     * @param User|null $User $User (optional) - [default: session user]
     * @return bool
     */
    public static function canUse(null | User $User = null): bool
    {
        if (self::isLoggedInAsSlave()) {
            return true;
        }

        if (empty($User)) {
            $User = QUI::getUserBySession();
        }

        if ($User->isSU()) {
            return true;
        }

        if (!method_exists($User, 'hasPermission')) {
            return false;
        }

        return $User->hasPermission(self::PERMISSION_CAN_USE);
    }

    /**
     * Get log table name
     *
     * @return string
     */
    public static function getLogTable(): string
    {
        return QUI::getDBTableName(self::TBL_LOG);
    }
}
