<?php
declare(strict_types=1);

namespace App\Security;

final class RequestSecurity
{
    public static function enforce(string $method, string $path): void
    {
        $method = strtoupper(trim($method));
        $rawUri = (string)($_SERVER['REQUEST_URI'] ?? $path);
        HttpSecurityHeaders::applyCors($method, $path);
        RequestRateLimiter::enforce($method, $path);

        if (strlen($rawUri) > self::intEnv('MAX_REQUEST_URI_BYTES', 4096, 1024, 16384)) {
            self::reject(414, 'Requête trop longue.');
        }
        if (str_contains($rawUri, "\0") || preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', $rawUri)) {
            self::reject(400, 'Requête invalide.');
        }
        if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']) || isset($_POST['_method'])) {
            self::reject(400, 'Substitution de méthode HTTP interdite.');
        }

        $contentLength = (int)($_SERVER['CONTENT_LENGTH'] ?? 0);
        $maxBody = self::intEnv('MAX_REQUEST_BODY_BYTES', 20 * 1024 * 1024, 1024, 100 * 1024 * 1024);
        if ($contentLength < 0 || $contentLength > $maxBody) {
            self::reject(413, 'Corps de requête trop volumineux.');
        }

        $host = trim((string)($_SERVER['HTTP_HOST'] ?? ''));
        if ($host !== '' && !self::isAllowedRequestHost($host)) {
            self::reject(400, 'En-tête Host invalide.');
        }

        if (in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
            $origin = trim((string)($_SERVER['HTTP_ORIGIN'] ?? ''));
            if ($origin !== '' && !self::sameOrigin($origin, $host)) {
                self::reject(403, 'Origine de requête non autorisée.');
            }
        }
    }

    /** @param mixed $value */
    public static function scalar($value, int $maxLength = 10000): string
    {
        if (!is_scalar($value) && $value !== null) self::reject(400, 'Paramètre invalide.');
        $text = trim((string)$value);
        if (strlen($text) > $maxLength || str_contains($text, "\0")) self::reject(400, 'Paramètre invalide.');
        return $text;
    }

    public static function safeRedirectTarget(string $target, string $fallback = '/'): string
    {
        $target = trim($target);
        if ($target === '' || $target[0] !== '/' || str_starts_with($target, '//') || str_contains($target, "\0")) return $fallback;
        $parts = parse_url($target);
        if ($parts === false || isset($parts['scheme']) || isset($parts['host'])) return $fallback;
        return $target;
    }

    public static function assertOutboundUrl(string $url, array $allowedHosts = []): string
    {
        $parts = parse_url(trim($url));
        if (!is_array($parts) || !in_array(strtolower((string)($parts['scheme'] ?? '')), ['https'], true)) {
            throw new \InvalidArgumentException('URL sortante HTTPS obligatoire.');
        }
        $host = strtolower((string)($parts['host'] ?? ''));
        if ($host === '' || filter_var($host, FILTER_VALIDATE_IP)) {
            throw new \InvalidArgumentException('Hôte sortant invalide.');
        }
        if ($allowedHosts !== [] && !in_array($host, array_map('strtolower', $allowedHosts), true)) {
            throw new \InvalidArgumentException('Hôte sortant non autorisé.');
        }
        $records = @dns_get_record($host, DNS_A | DNS_AAAA) ?: [];
        foreach ($records as $record) {
            $ip = (string)($record['ip'] ?? $record['ipv6'] ?? '');
            if ($ip !== '' && !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
                throw new \InvalidArgumentException('Adresse réseau privée ou réservée interdite.');
            }
        }
        return $url;
    }


    private static function isAllowedRequestHost(string $host): bool
    {
        if ($host === '' || preg_match('~[\x00-\x20\x7F\\/@?#]~', $host)) {
            return false;
        }

        $configured = trim((string)($_ENV['AXIOM_ALLOWED_HOSTS'] ?? getenv('AXIOM_ALLOWED_HOSTS') ?: ''));
        if ($configured !== '') {
            $normalizedRequest = strtolower(rtrim(self::hostWithoutPort($host), '.'));
            foreach (array_filter(array_map('trim', explode(',', $configured))) as $allowed) {
                $allowed = strtolower(rtrim($allowed, '.'));
                if ($allowed === $normalizedRequest) {
                    return true;
                }
                if (str_starts_with($allowed, '*.') && str_ends_with($normalizedRequest, substr($allowed, 1))) {
                    return true;
                }
            }
        }

        [$hostname, $port] = self::splitHostAndPort($host);
        if ($hostname === null || ($port !== null && ($port < 1 || $port > 65535))) {
            return false;
        }

        $hostname = strtolower(rtrim($hostname, '.'));
        if ($hostname === 'localhost' || filter_var($hostname, FILTER_VALIDATE_IP)) {
            return true;
        }

        // Les domaines Laragon/Docker locaux peuvent contenir un underscore.
        // Cette tolérance est limitée aux suffixes de développement non publics.
        if (preg_match('/^[a-z0-9_-]+(?:\.[a-z0-9_-]+)*\.(?:test|local|localhost)$/', $hostname)) {
            return true;
        }

        if (strlen($hostname) > 253) {
            return false;
        }
        foreach (explode('.', $hostname) as $label) {
            if ($label === '' || strlen($label) > 63 || !preg_match('/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/', $label)) {
                return false;
            }
        }
        return true;
    }

    /** @return array{0:?string,1:?int} */
    private static function splitHostAndPort(string $host): array
    {
        if (str_starts_with($host, '[')) {
            if (!preg_match('/^\[([^\]]+)\](?::(\d{1,5}))?$/', $host, $matches)) {
                return [null, null];
            }
            return [$matches[1], isset($matches[2]) ? (int)$matches[2] : null];
        }
        if (!preg_match('/^([^:]+)(?::(\d{1,5}))?$/', $host, $matches)) {
            return [null, null];
        }
        return [$matches[1], isset($matches[2]) ? (int)$matches[2] : null];
    }

    private static function hostWithoutPort(string $host): string
    {
        [$hostname] = self::splitHostAndPort($host);
        return $hostname ?? '';
    }

    private static function sameOrigin(string $origin, string $host): bool
    {
        $parts = parse_url($origin);
        if (!is_array($parts) || !isset($parts['host'])) return false;
        $originHost = strtolower(rtrim((string)$parts['host'], '.'));
        $requestHost = strtolower(rtrim(self::hostWithoutPort($host), '.'));
        return $originHost !== '' && hash_equals($requestHost, $originHost);
    }

    private static function intEnv(string $name, int $default, int $min, int $max): int
    {
        $value = filter_var($_ENV[$name] ?? $default, FILTER_VALIDATE_INT);
        return max($min, min($max, $value === false ? $default : $value));
    }

    private static function reject(int $status, string $message): never
    {
        http_response_code($status);
        header('Content-Type: text/plain; charset=utf-8');
        exit($message);
    }
}
