<?php

declare(strict_types=1);

namespace Plugin\tnt_mcp\src\Mcp\Http;

use Psr\Http\Message\ServerRequestInterface;

/**
 * Resolves the PUBLIC origin (scheme + host [+ non-default port]) a connector actually reached us
 * on, so OAuth discovery / challenge URLs advertise a host the connector can call back.
 *
 * Three resolution paths, in precedence order:
 *
 *  1. Operator override — a server param `TNT_MCP_PUBLIC_BASE` (also accepted under the
 *     `REDIRECT_`-prefixed spelling that Apache mod_rewrite `[E=]` produces mid-request) holding the
 *     full public base URL. This is a non-`HTTP_*` server param, so a remote client cannot forge it
 *     (PHP only populates `HTTP_*` from request headers); it is set by the deployment/front-end
 *     (SetEnv / mod_rewrite [E=]) when the shop sits behind an origin that terminates a different
 *     public hostname than the internal request host.
 *  2. Generic trusted reverse proxy — `X-Forwarded-Host` / `-Proto`, trusted ONLY when the request
 *     reached us from a trusted local address.
 *  3. No trusted forwarding — the request URI as-is.
 *
 * Anti-spoofing gate: `X-Forwarded-*` is trusted ONLY when the request reached us from a trusted
 * local address — loopback or an RFC1918 / link-local / IPv6-ULA private address. A trusted proxy
 * always sits on the same host or private network as the shop, so its connection appears with a
 * private `REMOTE_ADDR`; a request whose `REMOTE_ADDR` is a PUBLIC IP arrived directly (no trusted
 * proxy in front) and therefore MUST NOT be able to dictate the OAuth issuer by forging
 * `X-Forwarded-Host`. NB: when the shop runs in Docker the trusted proxy reaches PHP as the bridge
 * gateway (e.g. 192.168.x.1), never 127.0.0.1 — which is exactly why the gate accepts the private
 * ranges and not solely literal loopback.
 */
final class ForwardedRequest
{
    /**
     * Server params (deliberately NOT `HTTP_*` headers, so a remote client cannot forge them — PHP
     * only populates `HTTP_*` from request headers) that may hold an operator-supplied public base
     * URL. The `REDIRECT_` spelling is what Apache mod_rewrite `[E=]` produces during a rewritten
     * request; the plain spelling is what `SetEnv` produces. First non-empty wins.
     */
    private const OVERRIDE_BASE_PARAMS = [
        'TNT_MCP_PUBLIC_BASE',
        'REDIRECT_TNT_MCP_PUBLIC_BASE',
    ];

    /** Public base URL (no trailing slash) the connector should use for callbacks. */
    public static function baseUrl(ServerRequestInterface $request): string
    {
        $params = $request->getServerParams();

        // (1) Operator override: an unspoofable non-header server param holding the full public base.
        foreach (self::OVERRIDE_BASE_PARAMS as $key) {
            $override = $params[$key] ?? null;
            if (\is_string($override) && trim($override) !== '') {
                return rtrim(trim($override), '/');
            }
        }

        // (2) Generic trusted reverse proxy — only when REMOTE_ADDR is a trusted local address.
        $forwardedHost = trim($request->getHeaderLine('X-Forwarded-Host'));
        if ($forwardedHost !== '' && self::isTrustedProxy(self::remoteAddr($params))) {
            $scheme = strtolower(trim($request->getHeaderLine('X-Forwarded-Proto')));
            $scheme = $scheme !== '' ? $scheme : 'https';
            // A proxy chain may present a comma-separated list; the first value is the
            // client-facing host and already carries any non-default port.
            $host = trim(explode(',', $forwardedHost)[0]);

            return $scheme . '://' . $host;
        }

        // (3) No trusted forwarding — use the request URI as-is.
        $uri    = $request->getUri();
        $scheme = $uri->getScheme() !== '' ? $uri->getScheme() : 'https';
        $host   = $uri->getHost();
        $port   = $uri->getPort();
        $suffix = ($port !== null && !\in_array($port, [80, 443], true)) ? ':' . $port : '';

        return $scheme . '://' . $host . $suffix;
    }

    /**
     * The address a rate limit should count against: `REMOTE_ADDR`, unless the request came through
     * a trusted local proxy, in which case the left-most `X-Forwarded-For` entry is the real client.
     *
     * Same trust rule as {@see baseUrl()}: `X-Forwarded-For` is a header, so a direct client can
     * write anything it likes into it. Honouring it only behind a local proxy is what stops a
     * per-address cap from being bypassed by sending a different fake address every time.
     *
     * Returns '' when nothing usable is present — the caller then falls back to the shop-wide cap
     * rather than lumping every unknown client into one bucket.
     */
    public static function clientIp(ServerRequestInterface $request): string
    {
        $params = $request->getServerParams();
        $remote = self::remoteAddr($params);

        if (self::isTrustedProxy($remote)) {
            $forwardedFor = trim(explode(',', $request->getHeaderLine('X-Forwarded-For'))[0]);
            if ($forwardedFor !== '' && filter_var($forwardedFor, \FILTER_VALIDATE_IP) !== false) {
                return $forwardedFor;
            }
        }

        return filter_var($remote, \FILTER_VALIDATE_IP) !== false ? $remote : '';
    }

    /** @param array<string,mixed> $params */
    private static function remoteAddr(array $params): string
    {
        $addr = $params['REMOTE_ADDR'] ?? null;

        return \is_string($addr) ? $addr : '';
    }

    /**
     * True when $ip is a valid loopback / private / reserved / link-local address, i.e. a trusted
     * local proxy rather than a public client. `FILTER_FLAG_NO_PRIV_RANGE | NO_RES_RANGE` makes
     * filter_var reject exactly the private+reserved space (127.0.0.0/8, ::1, 10/8, 172.16/12,
     * 192.168/16, 169.254/16, fc00::/7, …) — so a `=== false` result there, combined with the IP
     * being otherwise valid, means "not public" == trusted-local.
     */
    private static function isTrustedProxy(string $ip): bool
    {
        if ($ip === '' || filter_var($ip, \FILTER_VALIDATE_IP) === false) {
            return false;
        }

        return filter_var(
            $ip,
            \FILTER_VALIDATE_IP,
            \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE,
        ) === false;
    }
}
