<?php

namespace Velis\App\User;

use Exception;
use Velis\App;
use Velis\App\User;
use Velis\Lang;

/**
 * Obfuscate unsuccessful login attempts
 *
 * @author Wojciech Polus <wojciech.polus@velis.pl>
 */
class AuthenticationObfuscator
{
    private const MIN_TIME = 1000000;
    private const MAX_TIME = 2000000;

    /**
     * @var int[]
     */
    private $buffer;

    /**
     * @var float
     */
    private $startTime;

    /**
     * @var bool
     */
    private $obfuscateErrors;

    /**
     * Constructor, initialize logins buffer in session
     * @param float $startTime
     */
    public function __construct(float $startTime)
    {
        $this->startTime = $startTime;
        $this->obfuscateErrors = true;

        if (App::$session->loginBuffer === null) {
            App::$session->set('loginBuffer', []);
        }

        $this->buffer = App::$session->get('loginBuffer');
    }

    /**
     *  Adds random sleep to avoid response time analysis
     * @param string|null $login
     * @throws Exception
     */
    public function obfuscate(string $login = null): void
    {
        try {
            $timeDiff = (int) microtime(true) - $this->startTime;
            $sleepTime = random_int(
                self::MIN_TIME - $timeDiff,
                self::MAX_TIME - $timeDiff
            );

            usleep($sleepTime);
        } catch (Exception $e) {
            // do nothing
        }

        if (!$login || !$this->obfuscateErrors) {
            return;
        }

        if (array_key_exists($login, $this->buffer)) {
            $this->buffer[$login] += 1;
        } else {
            $this->buffer[$login] = 1;
        }

        App::$session->set("loginBuffer", $this->buffer);
        $this->fakeErrorMessages($this->buffer[$login]);
    }


    /**
     * @param bool $enable
     */
    public function setErrorObfuscate(bool $enable): void
    {
        $this->obfuscateErrors = $enable;
    }


    /**
     * Sends fake unsuccessful login message
     * @param int $unsuccessfulCount
     * @throws Exception
     */
    private function fakeErrorMessages(int $unsuccessfulCount): void
    {
        $error = '';
        if (App::$config->settings->failedLoginCount) {
            if ($unsuccessfulCount >= App::$config->settings->failedLoginCount) {
                throw new Exception(Lang::get('USER_ACCOUNT_HAS_BEEN_BLOCKED'), User::ACCOUNT_BLOCKED_CODE);
            }

            $loginAttempts = App::$config->settings->failedLoginCount - $unsuccessfulCount;

            if ($loginAttempts == 1 || $loginAttempts == 3) {
                $error .= sprintf(Lang::get('USER_BEFORE_LOCK'), $loginAttempts);
            } elseif ($loginAttempts == 2) {
                $error .= sprintf(Lang::get('USER_BEFORE_LOCK'), $loginAttempts);
                throw new Exception($error, User::ACCOUNT_LOCK_WARNING);
            } else {
                $error .= Lang::get('USER_INVALID_LOG_IN');
            }
        } else {
            $error = Lang::get('USER_INVALID_LOG_IN');
        }
        throw new Exception($error, User::INVALID_CREDENTIALS_CODE);
    }
}
