<?php

namespace App\Services\Vpn;

use App\Support\ProvisioningSettings;
use Illuminate\Support\Str;

/**
 * Whether the WireGuard hub endpoint an onboarding host is told to dial is
 * even CAPABLE of being reached from outside this network.
 *
 * Deliberately makes no network call. A UDP port cannot be knocked on
 * cleanly from the outside — there is no handshake to observe without a real
 * peer, and this class runs on every readiness page load, not on demand. What
 * IS decidable without touching the wire: whether the address configured is
 * one a host on the internet could even attempt to reach. `10.10.90.185` and
 * `203.0.113.11` (RFC 5737) both look "set" and are unreachable from any host
 * outside this operator's own network or a documentation example generator —
 * exactly the two shapes seen in this installation's configuration on
 * 2026-07-30.
 */
final class WireguardEndpointCheck
{
    /**
     * IPv4 ranges FILTER_FLAG_NO_PRIV_RANGE|FILTER_FLAG_NO_RES_RANGE does NOT
     * reject, checked one by one against this PHP build (`php -r
     * 'var_dump(filter_var($ip, FILTER_VALIDATE_IP,
     * FILTER_FLAG_NO_PRIV_RANGE|FILTER_FLAG_NO_RES_RANGE));'`) so the next
     * reader does not have to repeat the research:
     *
     * Included below, because each is a shape a real installation can
     * actually end up with:
     * - 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24 (RFC 5737 "TEST-NET"):
     *   documentation/example addresses — seed data and copy-pasted examples
     *   land here. This was the original gap (Task 9 review).
     * - 100.64.0.0/10 (RFC 6598, Carrier-Grade NAT): an ISP hands this out to
     *   a real customer connection; it is exactly as unreachable from the
     *   outside as RFC 1918, just not covered by NO_PRIV_RANGE. Confirmed
     *   passing filter_var's flags before this fix (fix-round finding).
     * - 224.0.0.0/4 (multicast): not a shape a *mistake* produces, but never
     *   a single host's own reachable address either — no WireGuard peer's
     *   endpoint can legitimately be a multicast address, so it belongs in
     *   the same bucket as the others rather than being let through by
     *   omission.
     *
     * Already rejected by the filter_var flags above WITHOUT this list, so
     * deliberately NOT duplicated here (verified empirically, not assumed):
     * 0.0.0.0/8, 127.0.0.0/8 (loopback), 169.254.0.0/16 (link-local),
     * 240.0.0.0/4 (reserved — this range also covers the 255.255.255.255
     * broadcast address, confirmed separately).
     *
     * Deliberately left UNCHECKED, a considered omission and not an
     * oversight: 192.0.0.0/24 (IETF protocol assignments), 192.88.99.0/24
     * (deprecated 6to4 relay anycast), 198.18.0.0/15 (RFC 2544 benchmark
     * testing). All three pass filter_var's flags today, but none of them is
     * a shape an operator's real configuration plausibly lands in by
     * accident or by a real provider's assignment — unlike the TEST-NET
     * ranges (copy-pasted examples) and CGNAT (an ISP genuinely assigns it).
     * Adding checks for ranges nobody will ever hit is effort spent on the
     * task's shape, not its risk (R22).
     *
     * @var array<int, string>
     */
    private const NON_PUBLIC_RANGES = [
        '192.0.2.0/24',    // TEST-NET-1 (RFC 5737)
        '198.51.100.0/24', // TEST-NET-2 (RFC 5737)
        '203.0.113.0/24',  // TEST-NET-3 (RFC 5737)
        '100.64.0.0/10',   // Carrier-Grade NAT (RFC 6598)
        '224.0.0.0/4',     // Multicast
    ];

    /** @return array<string, mixed> */
    public function run(?string $endpoint = null): array
    {
        $endpoint ??= ProvisioningSettings::wgEndpoint();

        if (blank($endpoint)) {
            return ['ok' => false, 'reason' => 'missing'];
        }

        if (! str_contains($endpoint, ':')) {
            return ['ok' => false, 'reason' => 'no_port'];
        }

        $host = Str::beforeLast($endpoint, ':');

        // No valid IP means: a name. It can resolve publicly, and guessing
        // here would be worse than letting it through — the check states what
        // it knows, not what it suspects.
        if (! filter_var($host, FILTER_VALIDATE_IP)) {
            return ['ok' => true, 'reason' => 'hostname', 'address' => $host];
        }

        // NO_PRIV_RANGE covers RFC 1918, NO_RES_RANGE covers loopback and
        // link-local among others — exactly the private address that stood
        // at one host on 2026-07-30.
        $public = filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);

        if ($public === false || $this->isKnownNonPublicRange($host)) {
            return ['ok' => false, 'reason' => 'not_public', 'address' => $host];
        }

        return ['ok' => true, 'reason' => 'public', 'address' => $host];
    }

    private function isKnownNonPublicRange(string $ip): bool
    {
        if (! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            return false;
        }

        $long = ip2long($ip);

        foreach (self::NON_PUBLIC_RANGES as $cidr) {
            [$subnet, $bits] = explode('/', $cidr);
            $mask = -1 << (32 - (int) $bits);

            if (($long & $mask) === (ip2long($subnet) & $mask)) {
                return true;
            }
        }

        return false;
    }
}
