<?php

declare(strict_types=1);

namespace Facile\OpenIDClient\Middleware;

use Dflydev\FigCookies\Cookies;
use Dflydev\FigCookies\FigResponseCookies;
use Dflydev\FigCookies\Modifier\SameSite;
use Dflydev\FigCookies\SetCookie;
use Facile\OpenIDClient\Exception\LogicException;
use Facile\OpenIDClient\Exception\RuntimeException;
use Facile\OpenIDClient\Session\AuthSession;
use Facile\OpenIDClient\Session\AuthSessionInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\SimpleCache\CacheInterface;
use Override;

use function bin2hex;
use function class_exists;
use function is_array;
use function json_decode;
use function json_encode;
use function random_bytes;

/**
 * @psalm-api
 *
 * @psalm-import-type AuthSessionType from AuthSessionInterface
 */
final class SessionCookieMiddleware implements MiddlewareInterface
{
    public const SESSION_ATTRIBUTE = AuthSessionInterface::class;

    private string $cookieName;

    private int $ttl;

    private CacheInterface $cache;

    public function __construct(CacheInterface $cache, string $cookieName = 'openid', int $ttl = 300)
    {
        $this->cache = $cache;
        $this->cookieName = $cookieName;
        $this->ttl = $ttl;
    }

    #[Override]
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        if (! class_exists(Cookies::class)) {
            throw new LogicException('To use the SessionCookieMiddleware you should install dflydev/fig-cookies package');
        }

        $cookies = Cookies::fromRequest($request);
        $sessionCookie = $cookies->get($this->cookieName);

        $sessionId = null !== $sessionCookie ? $sessionCookie->getValue() : null;
        /** @var string|null $sessionValue */
        $sessionValue = null !== $sessionId ? $this->cache->get($sessionId) : null;
        /** @var false|AuthSessionType $data */
        $data = null !== $sessionValue ? json_decode($sessionValue, true, 512, JSON_THROW_ON_ERROR) : [];

        if (! is_array($data)) {
            $data = [];
        }

        $authSession = AuthSession::fromArray($data);

        $response = $handler->handle($request->withAttribute(self::SESSION_ATTRIBUTE, $authSession));

        $sessionId ??= bin2hex(random_bytes(32));

        $sessionValue = json_encode($authSession, JSON_THROW_ON_ERROR);

        if (false === $this->cache->set($sessionId, $sessionValue, $this->ttl)) {
            throw new RuntimeException('Unable to save session');
        }

        $sessionCookie = SetCookie::create($this->cookieName)
            ->withValue($sessionId)
            ->withMaxAge($this->ttl)
            ->withHttpOnly()
            ->withPath('/')
            ->withSameSite(SameSite::strict());

        $response = FigResponseCookies::set($response, $sessionCookie);

        return $response;
    }
}
