<?php

namespace Formatrica\Submission\Security;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
/**
 * Validates and secures remote HTTP endpoints
 */
final class Endpoint_Validator
{
    private const ALLOWED_SCHEMES = ['http', 'https'];

    /**
     * Validate a remote endpoint URL
     *
     * @param string $endpoint
     * @return string|null Validated URL or null if invalid
     */
    public function validate(string $endpoint): ?string
    {
        $validated = wp_http_validate_url($endpoint);
        if (!$validated) {
            return null;
        }

        $parts = wp_parse_url($validated);
        if (!is_array($parts)) {
            return null;
        }

        $scheme = isset($parts['scheme']) ? strtolower((string) $parts['scheme']) : '';
        if (!in_array($scheme, self::ALLOWED_SCHEMES, true)) {
            return null;
        }

        $host = $parts['host'] ?? '';
        if ($host === '') {
            return null;
        }

        // Block localhost and .local domains
        $lower_host = strtolower($host);
        if ($lower_host === 'localhost' || str_ends_with($lower_host, '.local')) {
            return null;
        }

        // Validate IP addresses
        if (filter_var($host, FILTER_VALIDATE_IP)) {
            if (!$this->is_public_ip($host)) {
                return null;
            }
        } else {
            // Resolve domain and check IPs
            $ips = $this->resolve_host_ips($host);

            if (!empty($ips)) {
                foreach ($ips as $ip) {
                    if (!$this->is_public_ip($ip)) {
                        return null;
                    }
                }
            }
        }

        return $validated;
    }

    /**
     * Check if an IP is public (not private/reserved)
     *
     * @param string $ip
     * @return bool
     */
    private function is_public_ip(string $ip): bool
    {
        if (!filter_var($ip, FILTER_VALIDATE_IP)) {
            return false;
        }

        // Disallow loopback explicitly
        if ($ip === '127.0.0.1' || $ip === '::1') {
            return false;
        }

        // filter_var with NO_PRIV_RANGE | NO_RES_RANGE returns the IP if it's public
        $public = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
        if ($public === false) {
            return false;
        }

        // Block link-local IPv4 169.254.0.0/16
        if (str_starts_with($ip, '169.254.')) {
            return false;
        }

        // Basic IPv6 link-local/unique-local checks
        if (strpos($ip, ':') !== false) {
            $lower = strtolower($ip);
            if (str_starts_with($lower, 'fe80:') || str_starts_with($lower, 'fc') || str_starts_with($lower, 'fd')) {
                return false;
            }
        }

        return true;
    }

    /**
     * Resolve a hostname to IP addresses
     *
     * @param string $host
     * @return array<int, string>
     */
    private function resolve_host_ips(string $host): array
    {
        $ips = [];

        if (function_exists('dns_get_record')) {
            $a = @dns_get_record($host, DNS_A);
            $aaaa = @dns_get_record($host, DNS_AAAA);
            foreach ((array) $a as $rec) {
                if (!empty($rec['ip'])) {
                    $ips[] = $rec['ip'];
                }
            }
            foreach ((array) $aaaa as $rec) {
                if (!empty($rec['ipv6'])) {
                    $ips[] = $rec['ipv6'];
                }
            }
        }

        if (empty($ips)) {
            $a_only = @gethostbynamel($host);
            if (is_array($a_only)) {
                $ips = array_merge($ips, $a_only);
            }
        }

        return $ips;
    }
}
