<?php

namespace Velis\Mvc\Authentication\Strategy;

use Velis\App;
use Velis\Exception\TokenExpiredException;
use Exception;
use Phalcon\Http\RequestInterface;
use Velis\Debug;
use Velis\Exception\UnauthorizedException;
use Velis\Mvc\Authentication\Strategy\TokenStrategyInterface;

class ApiTokenStrategy implements TokenStrategyInterface
{
    private string $authMethod = 'session';

    public function getAuthMethod(): string
    {
        return $this->authMethod;
    }

    /**
     * @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);
            $this->authMethod = '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)) {
            throw new UnauthorizedException();
        }

        // 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);
        throw new TokenExpiredException();
    }

    /**
     * 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
    {
        $currentSessionId = App::$session->getId();
        if ($currentSessionId !== $newSessionId) {
            if (App::$session->exists()) {
                App::$session->destroy($currentSessionId);
                App::$session->close();
            }
            App::$session->setId($newSessionId);
            App::$session->start();
        }
    }
}
