<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\MainBundle\Security\PasswordRecover;

use Chill\MainBundle\Entity\User;
use Psr\Log\LoggerInterface;

class TokenManager
{
    final public const HASH = 'h';

    final public const TIMESTAMP = 'ts';

    final public const TOKEN = 't';

    final public const TOKEN_LENGTH = 24;

    final public const USERNAME_CANONICAL = 'u';

    /**
     * @var LoggerInterface
     */
    protected $logger;

    /**
     * @param string $secret
     */
    public function __construct(protected $secret, LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function generate(User $user, \DateTimeInterface $expiration)
    {
        $token = \random_bytes(self::TOKEN_LENGTH);
        $username = $user->getUsernameCanonical();

        if (empty($username)) {
            throw new \UnexpectedValueException('username should not be empty to generate a token');
        }

        $timestamp = (string) $expiration->getTimestamp();
        $hash = \hash('sha1', $token.$username.$timestamp.$this->secret);

        return [
            self::HASH => $hash,
            self::TOKEN => \bin2hex($token),
            self::TIMESTAMP => $timestamp,
            self::USERNAME_CANONICAL => $username,
        ];
    }

    public function verify($hash, $token, User $user, string $timestamp)
    {
        $token = \hex2bin(\trim((string) $token));

        if (self::TOKEN_LENGTH !== \strlen($token)) {
            return false;
        }

        $username = $user->getUsernameCanonical();
        $date = \DateTimeImmutable::createFromFormat('U', $timestamp);

        if ($date < new \DateTime('now')) {
            $this->logger->info('receiving a password recover token with expired '
                .'validity');

            return false;
        }

        $expected = \hash('sha1', $token.$username.$timestamp.$this->secret);

        if ($expected !== $hash) {
            return false;
        }

        return true;
    }
}
