<?php

/**
 * Lifestyle Webconsulting GmbH
 *
 * LICENSE: This Software is the property of Lifestyle Webconsulting GmbH (Aschaffenburg, Germany)
 * and is private by copyright law - it is NOT Freeware.
 *
 * Any unauthorized use of this software without a valid license
 * is a violation of the license agreement and will be prosecuted by
 * civil and criminal law.
 *
 * @copyright  2018 Lifestyle Webconsulting GmbH
 * @link       http://www.life-style.de
 */

namespace Sso\WebserviceBundle\Api\PasswordPolicy\Policy;

use Sso\WebserviceBundle\Api\PasswordPolicy\History\Service as HistoryService;
use Sso\WebserviceBundle\Database\Manager as DatabaseManager;
use Sso\WebserviceBundle\Entity\Webservice\Type\User;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Class Validator
 * @package Sso\WebserviceBundle\Api\PasswordPolicy\Policy
 */
class Validator
{
    const REGEX_DELEMITER = '/';

    /**
     * @var Service
     */
    private $policyService;

    /**
     * @var HistoryService
     */
    private $historyService;

    /**
     * @var DatabaseManager
     */
    private $databaseManager;

    /**
     * @var array
     */
    private $errors;

    /**
     * @var string
     */
    private $currentLocale;

    /**
     * Validator constructor.
     * @param Service $policyService
     * @param HistoryService $historyService
     * @param DatabaseManager $dbManager
     * @param RequestStack $requestStack
     */
    public function __construct(
        Service $policyService,
        HistoryService $historyService,
        DatabaseManager $dbManager,
        RequestStack $requestStack
    ) {
        $this->policyService = $policyService;
        $this->historyService = $historyService;
        $this->databaseManager = $dbManager;
        $this->errors = [];
        $this->setCurrentLocale($requestStack);
    }

    /**
     * @param RequestStack $requestStack
     */
    private function setCurrentLocale(RequestStack $requestStack): void
    {
        $request = $requestStack->getCurrentRequest();
        $this->currentLocale = null !== $request ? $request->getLocale() : 'en';
    }

    /**
     * @param User $user
     * @return bool
     */
    public function validateUserPolicy(User $user)
    {
        $this->errors = [];

        // Password has not been changed! No need to validate password.
        $plainPassword = $user->getPlainPassword();
        if (0 === strlen($plainPassword)) {
            return true;
        }

        $passwordPolicy = $this->policyService->getPolicy($user->getPasswordPolicy());
        if (null === $passwordPolicy) {
            $passwordPolicy = $this->policyService->getDefaultPolicy();
        }

        // Looks like no default policy exists - return without validation
        if (null === $passwordPolicy) {
            return true;
        }

        $validations = [
            $this->checkRegEx($passwordPolicy, $plainPassword),
            $this->checkValidations($passwordPolicy, $plainPassword),
            $this->checkDistance($passwordPolicy, $plainPassword, $user->getPreviousPassword()),
            $this->checkChangeDelay($passwordPolicy, $user->getPreviousLastPasswordChange()),
            $this->checkHistory($passwordPolicy, $plainPassword, $user->getGuid()),
        ];

        // Return true, if no validation is has been failed
        return 0 === count(array_filter($validations, function ($isValid) {
                return !$isValid;
            }));
    }

    /**
     * @return array
     */
    public function getErrors()
    {
        return $this->errors;
    }

    /**
     * @param Model\Policy $policy
     * @param string $password
     * @return bool
     */
    private function checkRegEx(Model\Policy $policy, $password)
    {
        // As this is deprecated, an empty regular expression should always return true
        if (0 === strlen($policy->getRegEx())) {
            return true;
        }

        $regex = str_replace(self::REGEX_DELEMITER, '\\' . self::REGEX_DELEMITER, $policy->getRegEx());

        $isValid = preg_match(self::REGEX_DELEMITER . $regex . self::REGEX_DELEMITER, $password) > 0;
        if (!$isValid) {
            $errorMessages = $policy->getRegexErrorMessages();
            $this->errors[] = $this->translateErrorMessage($errorMessages, 'Password does not match regular expression');
        }

        return $isValid;
    }

    /**
     * @param Model\Policy $policy
     * @param string $password
     * @return bool
     */
    private function checkValidations(Model\Policy $policy, $password)
    {

        $validationIndex = 0;
        $allValid = true;
        foreach ($policy->getPolicyValidations() as $policyValidation){

            $validationIndex++;
            $regex = str_replace(self::REGEX_DELEMITER, '\\' . self::REGEX_DELEMITER, $policyValidation->getRegEx());
            $isValid = preg_match(self::REGEX_DELEMITER . $regex . self::REGEX_DELEMITER, $password) > 0;
            if (!$isValid) {
                $allValid = false;
                $errorMessages = $policyValidation->getErrorMessages();
                $this->errors[] = $this->translateErrorMessage($errorMessages, 'Password does not match regular expression ('.$validationIndex.')');
            }
        }

        return $allValid;
    }

    /**
     * @param Model\Policy $policy
     * @param string $newPassword
     * @param string $oldPassword
     * @return integer
     */
    private function checkDistance(Model\Policy $policy, $newPassword, $oldPassword)
    {
        // Cannot validate distance to old password if old password is not set
        if (empty($oldPassword)) {
            return true;
        }

        $numDiff = 0;
        $lenNew = strlen($newPassword);
        $lenOld = strlen($oldPassword);
        $lenDiff = abs($lenNew - $lenOld);
        for ($i = 0; $i < $lenNew && $i < $lenOld; $i++) {
            if (substr($newPassword, $i, 1) !== substr($oldPassword, $i, 1)) {
                $numDiff++;
            }
        }
        $numDiff += $lenDiff;

        $isValid = $numDiff >= $policy->getEditDistance();

        if (!$isValid) {
            $errorMessages = $policy->getEditDistanceErrorMessages();
            $this->errors[] = $this->translateErrorMessage($errorMessages,
                'Password does not meet required check distance');
        }

        return $isValid;
    }

    /**
     * @param Model\Policy $policy
     * @param \DateTime|null $lastChange
     * @return bool
     */
    private function checkChangeDelay(Model\Policy $policy, \DateTime $lastChange = null)
    {
        // Password has not been changed yet - no delay check needed
        if (empty($lastChange)) {
            return true;
        }

        // No change delay set in policy
        $changeDelay = $policy->getChangeDelay();
        if ($changeDelay <= 0) {
            return true;
        }

        $now = new \DateTime();
        $delay = new \DateInterval('PT' . $changeDelay . 'M');

        $minAge = clone $lastChange;
        $minAge = $minAge->add($delay);

        $isValid = $now > $minAge;

        if (!$isValid) {
            $errorMessages = $policy->getChangeDelayErrorMessages();
            $this->errors[] = $this->translateErrorMessage($errorMessages, 'Password has been set too recently');
        }

        return $isValid;
    }

    /**
     * @param Model\Policy $policy
     * @param string $password
     * @param string $userGuid
     * @return bool
     */
    private function checkHistory(Model\Policy $policy, $password, $userGuid)
    {
        $isValid = !$this->historyService->checkHistory($userGuid, $password, $policy->getHistorySize());

        if (!$isValid) {
            $errorMessages = $policy->getHistorySizeErrorMessages();
            $this->errors[] = $this->translateErrorMessage($errorMessages, 'Password matches previous password');
        }

        return $isValid;
    }

    /**
     * @param array $errorMessages
     * @param string $fallbackError
     * @return string
     */
    private function translateErrorMessage($errorMessages, $fallbackError)
    {

        if (!empty($errorMessages[$this->currentLocale])) {
            return $errorMessages[$this->currentLocale];
        }

        if (!empty($errorMessages['default'])) {
            return $errorMessages['default'];
        }

        if (!empty($errorMessages['en'])) {
            return $errorMessages['en'];
        }

        return $fallbackError;
    }
}
