<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\CmsBundle\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\UriSigner;

/**
 * Resolves the post-login redirect target for the sign-in component.
 *
 * This is the single source of truth shared by the password flow
 * ({@see \Ferienpass\CmsBundle\Components\SignIn\Login::targetPath()}) and the
 * passkey flow (the `success_redirect` fed to the web-auth Stimulus controller,
 * which navigates the browser via window.location after a successful assertion).
 *
 * Precedence:
 *  1. An explicit, server-generated target passed by the includer (e.g. the
 *     application form passing the offer's canonical reader URL). Trusted by
 *     origin — it is built with `path()`, never from user input.
 *  2. A `?redirect=` query parameter, but ONLY when the whole request carries a
 *     valid {@see UriSigner} signature, i.e. we generated that link ourselves.
 *     A raw, unsigned `?redirect=` is deliberately ignored: feeding it to the
 *     passkey window.location would be an open redirect (and a `javascript:`
 *     value a DOM-XSS) gated only on a successful login.
 *  3. The current page URI, so an embedded sign-in returns the user to the page
 *     they were on instead of dumping them on `/`.
 */
final class LoginRedirectResolver
{
    public function __construct(
        private readonly RequestStack $requestStack,
        private readonly UriSigner $uriSigner,
    ) {
    }

    public function resolve(?string $explicit = null): string
    {
        if (null !== $explicit && '' !== $explicit) {
            return $explicit;
        }

        $request = $this->requestStack->getCurrentRequest();
        if (null === $request) {
            return '/';
        }

        if ($request->query->has('redirect') && $this->uriSigner->checkRequest($request)) {
            // The original, un-reordered query string — checkRequest() validated
            // the signature over exactly this URI.
            return (string) $request->query->get('redirect');
        }

        return $this->currentPageUri($request);
    }

    public function currentPageUri(Request $request): string
    {
        $pathInfo = $request->getPathInfo();

        // In a LiveComponent re-render (`/live_component_cms/...`) or an ESI
        // fragment render (`/_fragment?...`) the request URI is an internal one,
        // not the page the user is on — fall back to the Referer (the host page).
        // The Referer is client-supplied, though, so only trust it when it points
        // back to our own host: it ends up in the passkey window.location, where
        // an off-site value would be an open redirect. Otherwise use '/'.
        if ('/_fragment' === $pathInfo || str_contains($pathInfo, '/live_component_cms/')) {
            $referer = (string) $request->headers->get('referer');

            return $this->isSameHost($referer, $request) ? $referer : '/';
        }

        return $request->getSchemeAndHttpHost().$request->getRequestUri();
    }

    private function isSameHost(string $url, Request $request): bool
    {
        $host = parse_url($url, \PHP_URL_HOST);

        return null !== $host && 0 === strcasecmp($host, $request->getHost());
    }
}
