<?php

namespace Velis\Mvc\Controller\RestRequestHandler;

use Exception;
use Phalcon\Http\RequestInterface;
use Velis\Api\HttpExceptionHandler\HttpException;
use Velis\App;
use Velis\Debug;
use Velis\Exception\TokenExpiredException;
use Velis\Exception\UnauthorizedException;
use Velis\Lang;

/**
 * This middleware is responsible for authenticating the user using a cookie.
 * It would be useful, when the user is already logged via cookie&session, and we want to authenticate him
 * in the API that uses Authentication Token.
 * In simple words, it would be useful when we want to authenticate the user in the API using the cookie.
 * @author Szymon Janaczek <szymon.janaczek@velistech.com>
 */
class AuthenticationMiddleware extends AbstractRestRequestHandler
{
    /**
     * @throws TokenExpiredException
     * @throws UnauthorizedException
     */
    public function handle(RequestInterface $request): void
    {
        if ($this->canPass($request)) {
            $this->handleNext($request);
            return;
        }

        $this->unauthorized();
    }

    /**
     * @throws TokenExpiredException
     * @throws UnauthorizedException
     */
    public function canPass(RequestInterface $request): bool
    {
        $isSessionValid = $this->checkSessionValidity();

        // Try to get the token from the request.
        $token = $request->authorization();

        // If a session was obtained through the token, check whether the token is still valid.
        // If not, return HTTP 406 - because we use this type of HTTP error in other places.
        if (is_string($token) && App::$session->getId() === $token) {
            $this->checkTokenValidity($token);
        }

        // Session exists and is valid, so just proceed with the request.
        if ($isSessionValid) {
            return true;
        }

        // no session and no token? Unauthorized.
        if (!is_string($token)) {
            $this->unauthorized();
        }

        // There was no session assigned to this token, so check whether the token is valid.
        // Token is invalid?
        // - Unauthorized.
        $this->checkTokenValidity($token);

        // The token is valid, so create a new session with the same ID as the token and authenticate user.
        $this->signInViaToken($token);

        // One more check of the newly started session.
        return $this->checkSessionValidity();
    }

    /**
     * Check session validity.
     * Check whether the session exists and the user is logged in properly.
     */
    protected function checkSessionValidity(): bool
    {
        return !!App::$user?->isLogged();
    }

    /**
     * Check token validity - whether it really exists and is not expired.
     * If token is not valid, automatically destroy session with ID equal to the token and throw an exception.
     * @throws TokenExpiredException
     */
    protected function checkTokenValidity(?string $token): bool
    {
        if (!is_string($token)) {
            return false;
        }

        try {
            if (App::$user->checkToken($token)) {
                return true;
            }
        } catch (Exception $e) {
            Debug::reportException($e);
        }

        // The token expired or there was a problem while checking.
        App::$session->destroy($token);
        $this->tokenExpired();
    }

    /**
     * Set the current token by session id and perform token login to authenticate the user.
     */
    protected function signInViaToken(string $token): void
    {
        $this->setSessionByToken($token);

        App::$user->tokenLogin($token);
    }

    /**
     * Set the current session to the one with the given ID.
     */
    protected function setSessionByToken(string $newSessionId): void
    {
        App::$session->destroy(App::$session->getId());
        App::$session->setId($newSessionId);
        App::$session->start();
    }

    /**
     * @throws UnauthorizedException
     */
    protected function unauthorized(): void
    {
        throw new UnauthorizedException();
    }

    /**
     * @throws TokenExpiredException
     */
    protected function tokenExpired(): void
    {
        throw new TokenExpiredException();
    }
}
