<?php

namespace MintyPHP\Http;

/**
 * Stores and retrieves the URL the user intended to visit before
 * being redirected to the login page (session timeout, auth guard).
 *
 * URLs are validated to prevent open-redirect attacks.
 */
class IntendedUrlService
{
    private const SESSION_KEY = 'intended_url';
    private const MAX_LENGTH = 2048;
    private const FALLBACK = 'admin';

    /** Prefixes that must never be restored (would cause redirect loops or security issues). */
    private const BLOCKED_PREFIXES = [
        'auth/',
        'api/',
        'login',
        'logout',
        'flash/',
        'branding/',
    ];

    /**
     * Store the intended URL in the session.
     */
    public function store(string $url, SessionStoreInterface $session): void
    {
        $sanitized = $this->sanitize($url);
        if ($sanitized !== '') {
            $session->set(self::SESSION_KEY, $sanitized);
        }
    }

    /**
     * Retrieve the intended URL without removing it.
     */
    public function retrieve(SessionStoreInterface $session): ?string
    {
        $value = $session->get(self::SESSION_KEY);
        return is_string($value) && $value !== '' ? $value : null;
    }

    /**
     * Retrieve the intended URL and remove it from the session.
     * Returns the fallback ('admin') if no valid URL is stored.
     *
     * The stored URL is a full REQUEST_URI (e.g. /de/admin/users?page=2)
     * but Router::redirect() expects a route path without leading slash
     * and without locale prefix (e.g. admin/users?page=2), because it
     * prepends getBaseUrl() which already includes the base path.
     */
    public function retrieveAndForget(SessionStoreInterface $session): string
    {
        $value = $this->retrieve($session);
        $session->remove(self::SESSION_KEY);

        if ($value === null) {
            return self::FALLBACK;
        }

        return $this->toRoutePath($value);
    }

    /**
     * Build a login redirect URL with the return_to query parameter.
     */
    public function buildLoginRedirect(string $loginPath, string $returnTo): string
    {
        $sanitized = $this->sanitize($returnTo);
        if ($sanitized === '') {
            return $loginPath;
        }
        $separator = str_contains($loginPath, '?') ? '&' : '?';
        return $loginPath . $separator . 'return_to=' . rawurlencode($sanitized);
    }

    /**
     * Sanitize and validate a URL for safe redirect.
     * Returns empty string if the URL is not safe.
     */
    public function sanitize(string $url): string
    {
        $url = trim($url);

        if ($url === '' || mb_strlen($url) > self::MAX_LENGTH) {
            return '';
        }

        // Must be a relative path (starts with /)
        if (!str_starts_with($url, '/')) {
            return '';
        }

        // Must not contain protocol indicators (prevent //evil.com or javascript:)
        if (str_contains($url, '://') || str_starts_with($url, '//')) {
            return '';
        }

        // Strip the path to check against blocked prefixes.
        // URL might be /en/admin/users or /admin/users — strip locale prefix.
        $path = $this->extractPathWithoutLocale($url);

        foreach (self::BLOCKED_PREFIXES as $prefix) {
            if (str_starts_with($path, $prefix) || $path === rtrim($prefix, '/')) {
                return '';
            }
        }

        return $url;
    }

    /**
     * Convert a full REQUEST_URI into a Router::redirect()-compatible route path.
     *
     * /de/admin/users?page=2 → admin/users?page=2
     * /admin/users            → admin/users
     */
    public function toRoutePath(string $url): string
    {
        // Remove leading slash
        $path = ltrim($url, '/');

        // Remove locale prefix (2-letter code followed by /)
        if (preg_match('#^[a-z]{2}/#', $path)) {
            $path = substr($path, 3);
        }

        return $path !== '' ? $path : self::FALLBACK;
    }

    /**
     * Extract the path portion without query string and without locale prefix.
     */
    private function extractPathWithoutLocale(string $url): string
    {
        // Remove query string and fragment
        $path = strtok($url, '?#') ?: $url;

        // Remove leading slash
        $path = ltrim($path, '/');

        // Remove locale prefix (2-letter code followed by /)
        if (preg_match('#^[a-z]{2}/#', $path)) {
            $path = substr($path, 3);
        }

        return $path;
    }
}
