<?php

namespace Mnv\Core\Auth;

use Mnv\Core\Config;
use Mnv\Core\Database\Throwable\IntegrityConstraintViolationException;
use Mnv\Core\Uploads\ImageSizes;
use Mnv\Core\UserInfo;
use Mnv\Core\Utilities\Cookie\Session;
use Mnv\Core\Utilities\Base64\Base64;

use Mnv\Models\Users\UserTypes;
use Mnv\Core\Auth\Errors\AuthError;
use Mnv\Core\Auth\Errors\MissingCallbackError;
use Mnv\Core\Auth\Exceptions\UnknownIdException;
use Mnv\Core\Auth\Exceptions\InvalidEmailException;
use Mnv\Core\Auth\Exceptions\UserRoleExistsException;
use Mnv\Core\Auth\Exceptions\InvalidPasswordException;
use Mnv\Core\Auth\Exceptions\UnknownUsernameException;
use Mnv\Core\Auth\Exceptions\ValidatePasswordException;
use Mnv\Core\Auth\Exceptions\DuplicateUsernameException;
use Mnv\Core\Auth\Exceptions\UserAlreadyExistsException;

/**
 * Абстрактный базовый класс для компонентов, реализующих управление пользователями
 *
 * @internal
 */
abstract class AdminManager {

    /** ADMIN */
	/** @var string session field for whether the client is currently signed in */
	const SESSION_FIELD_LOGGED_IN = 'auth_logged_in';
	/** @var string session field for the ID of the user who is currently signed in (if any) */
	const SESSION_FIELD_USER_ID = 'auth_user_id';
	/** @var string session field for the email address of the user who is currently signed in (if any) */
	const SESSION_FIELD_EMAIL = 'auth_email';
	/** @var string session field for the display name (if any) of the user who is currently signed in (if any) */
	const SESSION_FIELD_USERNAME = 'auth_username';
	/** @var string session field for the status of the user who is currently signed in (if any) as one of the constants */
	const SESSION_FIELD_STATUS = 'auth_status';
	/** @var string session field for the roles of the user who is currently signed in (if any) as a bitmask using constants */
	const SESSION_FIELD_ROLES = 'auth_roles';
	/** @var string session field for whether the user who is currently signed in (if any) has been remembered (instead of them having authenticated actively) */
	const SESSION_FIELD_REMEMBERED = 'auth_remembered';
	/** @var string session field for the UNIX timestamp in seconds of the session data's last resynchronization with its authoritative source in the database */
	const SESSION_FIELD_LAST_RESYNC = 'auth_last_resync';
	/** @var string session field for the counter that keeps track of forced logouts that need to be performed in the current session */
	const SESSION_FIELD_FORCE_LOGOUT = 'auth_force_logout';
    /** @var string session field limit content count page */
    const SESSION_FIELD_LIMIT = 'limit';
    /** @var string session field info не удачное регистрирование данных */
    const SESSION_FIELD_ERROR = 'error';

    const SESSION_FIELD_BANNED = 'auth_banned';
    const SESSION_SITE_USER = 'siteUser';

	/**
	 * Создает случайную строку с заданной максимальной длиной
	 *
	 * С параметром по умолчанию вывод должен содержать как минимум столько же случайности, сколько UUID.
	 * With the default parameter, the output should contain at least as much randomness as a UUID
	 *
     * @param int $maxLength the maximum length of the output string (integer multiple of 4)
     * @return string the new random string
     * @throws \Mnv\Core\Utilities\Base64\Throwable\EncodingError
     */
	public static function createRandomString($maxLength = 24)
	{
		// вычислить, сколько байтов случайности нам нужно для указанной длины строки
		$bytes = \floor((int) $maxLength / 4) * 3;

		// получить случайные данные
		$data = \openssl_random_pseudo_bytes($bytes);

		// вернуть результат в кодировке Base64
		return Base64::encodeUrlSafe($data);
	}


	/**
	 * Создает нового пользователя
	 *
	 * If you want the user's account to be activated by default, pass `null` as the callback
	 *
	 * If you want to make the user verify their email address first, pass an anonymous function as the callback
	 *
	 * The callback function must have the following signature:
	 *
	 * `function ($selector, $token)`
	 *
	 * Both pieces of information must be sent to the user, usually embedded in a link
	 *
	 * When the user wants to verify their email address as a next step, both pieces will be required again
	 *
	 * @param bool $requireUniqueUsername необходимо ли гарантировать, что имя пользователя является уникальным
	 * @param string $email the email address to register
	 * @param string $password the password for the new account
	 * @param string|null $loginName (optional) the loginName that will be displayed
	 * @param callable|null $callback (optional) the function that sends the confirmation email to the user
	 * @return int the ID of the user that has been created (if any)
	 * @throws InvalidEmailException if the email address has been invalid
	 * @throws InvalidPasswordException if the password has been invalid
	 * @throws UserAlreadyExistsException if a user with the specified email address already exists
	 * @throws DuplicateUsernameException if it was specified that the loginName must be unique while it was *not*
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 *
	 * @see confirmEmail
	 * @see confirmEmailAndSignIn
	 */
	protected function createUserInternal(bool $requireUniqueUsername, string $email, string $password, ?string $loginName = null, callable $callback = null): int
    {
		\ignore_user_abort(true);

		$email = self::validateEmailAddress($email);
		$password = self::validatePassword($password);

        $loginName = isset($loginName) ? \trim($loginName) : null;

		// если нужно обеспечить уникальность `loginName`
		if ($requireUniqueUsername) {
			if ($loginName !== null) {
				// подсчитайте количество пользователей, у которых уже есть указанное `loginName`
                $occurrencesOfUsername = connect()->table('users')->count('*', 'count')->where('loginName', $loginName)->getValue();

				// если пользователь с таким `loginName` уже существует
				if ($occurrencesOfUsername > 0) {
					// отменить операцию и сообщить о нарушении данного требования
					throw new DuplicateUsernameException();
				}
			}
		}

		$password = \password_hash($password, \PASSWORD_DEFAULT);
		$verified = \is_callable($callback) ? 0 : 1;

		try {
            $newUserId = connect()->table('users')->insert([
                'email'         => $email,
                'password'      => $password,
                'loginName'     => $loginName,
                'verified'      => $verified,
                'registered'    => \time(),
            ]);
		}
		// если у нас есть повторяющаяся запись
		catch (IntegrityConstraintViolationException $e) {
			throw new UserAlreadyExistsException();
		}

		if ($verified === 0) {
			$this->createConfirmationRequest($newUserId, $email, $callback);
		}

		return $newUserId;
	}

    /**
	 * Updates the given user's password by setting it to the new specified password
	 *
	 * @param int $userId the ID of the user whose password should be updated
	 * @param string $newPassword the new password
	 * @throws UnknownIdException if no user with the specified ID has been found
	 */
	protected function updatePasswordInternal(int $userId, string $newPassword)
    {
		$newPassword = \password_hash($newPassword, \PASSWORD_DEFAULT);
		$affected = connect()->table('users')->where('userId',  $userId)->update(['password' => $newPassword]);

		if ($affected === 0) {
		    throw new UnknownIdException();
		}
	}

	/**
	 * Called when a user has successfully logged in
	 *
	 * This may happen via the standard login, via the "remember me" feature, or due to impersonation by administrators
	 *
	 * @param int $userId the ID of the user
	 * @param string|null $email the email address of the user
	 * @param string|null $loginName the display name (if any) of the user
	 * @param int $status the status of the user as one of the constants from the {@see Status} class
	 * @param int $role the roles of the user as a bitmask using constants
	 * @param int $forceLogout the counter that keeps track of forced logouts that need to be performed in the current session
	 * @param bool $remembered whether the user has been remembered (instead of them having authenticated actively)
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 */
	protected function onLoginSuccessful(int $userId, ?string $email, ?string $loginName, int $status, int $role, int $forceLogout, bool $remembered = false)
    {
        // повторно сгенерировать идентификатор session, чтобы предотвратить атаки фиксации session (запрашивает запись cookie на клиенте)
		Session::regenerate(true);

		// save the user data in the session variables maintained by this library
        $_SESSION['admin'][self::SESSION_FIELD_BANNED]       = 0;
		$_SESSION['admin'][self::SESSION_FIELD_LOGGED_IN]    = true;
		$_SESSION['admin'][self::SESSION_FIELD_USER_ID]      = $userId;
		$_SESSION['admin'][self::SESSION_FIELD_EMAIL]        = $email;
		$_SESSION['admin'][self::SESSION_FIELD_USERNAME]     = $loginName;
		$_SESSION['admin'][self::SESSION_FIELD_STATUS]       = $status;
		$_SESSION['admin'][self::SESSION_FIELD_ROLES]        = $role;
		$_SESSION['admin'][self::SESSION_FIELD_FORCE_LOGOUT] = $forceLogout;
		$_SESSION['admin'][self::SESSION_FIELD_REMEMBERED]   = $remembered;
		$_SESSION['admin'][self::SESSION_FIELD_LAST_RESYNC]  = \time();
	}


	/**
     * Возвращает запрошенные данные пользователя для учетной записи с указанным именем входа (если есть)
	 * Вы никогда не должны передавать ненадежный ввод в параметр, который принимает список столбцов.
	 *
	 * @param string $loginName the loginName to look for
	 * @param array $requestedColumns the columns to request from the user's record
	 * @return array the user data (if an account was found unambiguously)
	 * @throws UnknownUsernameException если не найден пользователь с указанным логином
	 */
	protected function getUserDataByUsername(string $loginName, array $requestedColumns): array
    {
        $projection = \implode(', ', $requestedColumns);
        $users = connect()->table('users')->select($projection)->where('loginName', $loginName)->in('userType', [UserTypes::DEVELOPER, UserTypes::ADMIN])->get('array');

		if (empty($users)) {
			throw new UnknownUsernameException();
		}
		else {
		    return $users;
		}
	}

    /**
     * Возвращает запрошенные данные user для учетной записи с указанным именем входа (если есть)
     * Вы никогда не должны передавать ненадежный ввод в параметр, который принимает список столбцов.
     *
     * @param string $loginName the loginName to look for
     * @param array $requestedColumns the columns to request from the user's record
     * @return array the user data (if an account was found unambiguously)
     * @throws UserRoleExistsException
     */
    protected function getAdminUserDataByRole(string $loginName, array $requestedColumns): array
    {

        $projection = \implode(', ', $requestedColumns);
        $users = connect()->table('users')->select($projection)->where('loginName', $loginName)->where('status',Status::NORMAL)->in('userType', [UserTypes::DEVELOPER, UserTypes::ADMIN])->get('array');

        if (empty($users)) {
            throw new UserRoleExistsException();
        } else {
            return $users;
        }
    }



	/**
	 * Validates an email address
	 *
	 * @param string $email the email address to validate
	 * @return string the sanitized email address
	 * @throws InvalidEmailException if the email address has been invalid
	 */
	protected static function validateEmailAddress(string $email): string
    {
		if (empty($email)) {
			throw new InvalidEmailException();
		}

		$email = \trim($email);

		if (!\filter_var($email, \FILTER_VALIDATE_EMAIL)) {
			throw new InvalidEmailException();
		}

		return $email;
	}

	/**
	 * Validates a password
	 *
	 * @param string $password the password to validate
	 * @return string the sanitized password
	 * @throws ValidatePasswordException if the password has been invalid
	 */
	protected static function validatePassword(string $password): string
    {
		if (empty($password)) {
			throw new ValidatePasswordException();
		}

		$password = \trim($password);

		if (\strlen($password) < 1) {
			throw new ValidatePasswordException();
		}

		return $password;
	}

	/**
	 * Создает запрос на подтверждение по email
	 *
	 * The callback function must have the following signature:
	 *
	 * `function ($selector, $token)`
	 *
	 * Both pieces of information must be sent to the user, usually embedded in a link
	 *
	 * When the user wants to verify their email address as a next step, both pieces will be required again
	 *
	 * @param int $userId the user's ID
	 * @param string $email the email address to verify
	 * @param callable $callback the function that sends the confirmation email to the user
	 * @throws AuthError if an internal problem occurred (do *not* catch)
	 */
	protected function createConfirmationRequest(int $userId, string $email, callable $callback)
	{
		$selector = self::createRandomString(16);
		$token = self::createRandomString(16);
		$tokenHashed = \password_hash($token, \PASSWORD_DEFAULT);
		$expires = \time() + 60 * 60 * 24;

		connect()->table('users_confirmations')->insert([
		    'user_id'   => $userId,
            'email'     => $email,
            'selector'  => $selector,
            'token'     => $tokenHashed,
            'expires'   => $expires
        ]);

		if (\is_callable($callback)) {
			$callback($selector, $token);
		}
		else {
			throw new MissingCallbackError();
		}
	}

	/**
	 * Удаляет существующую директиву, которая удерживает пользователя в системе («запомни меня»).
	 *
	 * @param int $userId the ID of the user who shouldn't be kept signed in anymore
	 * @param string|null $selector (optional) the selector which the deletion should be restricted to
	 */
	protected function deleteRememberDirectiveForUserById(int $userId, string $selector = null)
    {
		if (isset($selector)) connect()->where('selector', $selector);

		connect()->table('users_remembered')->where('user', $userId)->delete();

	}

	/**
	 * Запускает принудительный выход из системы во всех сеансах, принадлежащих указанному пользователю.
	 *
	 * @param int $userId ID пользователя для выхода
	 */
	protected function forceLogoutForUserById(int $userId)
    {
		$this->deleteRememberDirectiveForUserById($userId);
        $user = connect()->table('users')->select('force_logout')->where('userId',  $userId)->get();
        $user->force_logout = $user->force_logout + 1;

        connect()->table('users')->where('userId', $userId)->update(['force_logout' => $user->force_logout]);
	}


    /**
     * Получение менеджера
     */
    /**
     * @param int|null $managerId
     * @return array|false|mixed|string|null
     */
    public function adminManager(?int $managerId)
    {
        if (empty($managerId)) return null;

        $fields = 'userId, fullName, firstName, middleName, lastName, loginName, accessLevel, fileId, userType, groupName AS accessLevelName';
        if ($manager = connect()->table('users')->join('user_group', 'groupId', '=', 'accessLevel')
            ->select($fields)->where('userId', $managerId)->in('userType', [UserTypes::DEVELOPER, UserTypes::ADMIN])->get('array')) {

            if ($fileId = connect()->table('user_images')->select('fileId')->where('userId', $manager['userId'])->getValue()) {
                $file = connect()->table('files')->select('fileId, directory, path, fileName, size')->where('fileId', $fileId)->get('array');
                $manager['image'] = ImageSizes::init()->get(null, $file);

            }

            if (!is_null($manager['accessLevel'])) {
                $manager['permissions'] = connect()->table('user_group_privileges')->select('privilege')->where('groupId', $manager['accessLevel'])->indexKey('privilege')->valueKey('privilege')->getAllIndexes('array');
            }
//        print_r($manager);
            return $manager;
        }

        return null;

    }


    /** Блокировка пользователя или злоумышленника на 10 часов */
    public function setUserBanned(): void
    {
        if (Config::getValue('check_banned_ips') && (Config::getValue('check_banned_ips') == 1)) {
            $ban['visitorIp']   = UserInfo::get_ip();
            $ban['bannedOn']    = date('Y-m-d H:i:s');
            $ban['expiresOn']   = date('Y-m-d H:i:s', strtotime('+10 hours'));

            connect()->table('users_banned_ips')->insert($ban);

            $this->unsetBanned();
        }
    }

    public function getUserBanned(): bool
    {
        if (!connect()->table('users_banned_ips')->where('visitorIp', UserInfo::get_ip())->get()) {
            return true;
        }

        return false;
    }

    /**
     * @return int
     */
    public function unsetBanned(): int
    {
        return $_SESSION['admin'][AdminManager::SESSION_FIELD_BANNED] = 0;
    }


}
