<?php

namespace apexl\Io\modules\user\Middleware;

use apexl\Io\exceptions\RecordNotFoundException;
use apexl\Io\includes\HookManager;
use apexl\Io\interfaces\GlobalDataInterface;
use apexl\Io\modules\user\entities\sessionEntity;
use apexl\Io\modules\user\entities\userEntity;
use apexl\Io\modules\user\exceptions\JwtException;
use apexl\Io\modules\user\Factories\JwtFromRequestServiceFactory;
use apexl\Io\modules\user\services\currentUser;
use apexl\Io\services\Output;
use apexl\Utils\Urls\Host;
use Exception;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Exception\HttpBadRequestException;
use Slim\Exception\HttpUnauthorizedException;
use function apexl\Io\config;

final readonly class SessionMiddleware implements MiddlewareInterface
{
    public final const string HOOK__POST_AUTH = 'postAuth';

    public function __construct(
        private JwtFromRequestServiceFactory $jwtFromRequestServiceFactory,
        private HookManager $hookManager,
        private GlobalDataInterface $globalData,
        private Output $output,
    ) {}

    /**
     * @throws Exception
     */
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        // each client should remember their session id for EXACTLY the given time, or 1 hour.
        if (config('app.requires_ssl') && !Host::isSecure()) {
            throw new Exception(
                <<<MESSAGE
                You must either set app.requireSSL to FALSE in config (And use a browser other than Chrome),
                or run this under SSL. NOTE for SSL is required even when tested for chrome as new versions
                of chrome will block non-SSL cookies on cross Site Requests.
                MESSAGE
            );
        }

        $authenticated = false;
        $userId = 0;
        // Check token validity
        $config = config('auth.jwt');
        if (!isset($config)) {
            $this->output->addSystemError("No JWT object defined in config.");

            throw new HttpBadRequestException($request, 'JWT config object required');
        }

        try {
            if ($token = $this->jwtFromRequestServiceFactory->make($request)->token()) {
                $session = new sessionEntity();
                $session->load($token->sessionId, true);
                if (($session->sessionId ?? null) !== null && $session->active && $session->uid == $token->userId) {
                    $authenticated = true;
                    $userId = $session->uid;
                }
            }
        } catch (RecordNotFoundException|JwtException) {
            // Invalid token
        }

        //not logged in? we need to check if we need to be authenticated or not.
        if (config('app.requires_auth') === true && !$authenticated) {
            throw new HttpUnauthorizedException($request, 'Authentication Required',);
        }

        if ($request->getAttribute('user') !== null && config('app.environment') === 'test') {
            $userId = $request->getAttribute('user');
            $authenticated = $request->getAttribute('user') === $userId;
        } else {
            $request = $request->withAttribute('user', $userId);
            $request = $request->withAttribute('authenticated', $authenticated);
        }

        $this->addUserToGlobalData($userId);
        $this->globalData->addData('session', ['authenticated' => $authenticated]);

        $this->hookManager->processHook(self::HOOK__POST_AUTH, $userId);

        return $handler->handle($request);
    }

    /**
     * Add the loaded user data to the global data object.
     * @param $userId
     * @throws Exception
     */
    protected function addUserToGlobalData($userId): void
    {
        $user = new userEntity();
        $user->load($userId);
        //Add the object and id to the array to allow a load to be performed just before the data is returned to ensure its up-to-date.
        $this->globalData->addData('entities', ['user' => ['entity' => $user, 'id' => $user->id]], true, true);
    }
}
