<?php

declare(strict_types=1);

namespace App\Framework\Http\Parser;

use App\Framework\Http\Headers;
use App\Framework\Http\Parser\Exception\ParserSecurityException;

/**
 * Parser for HTTP headers with security validation
 * Handles parsing from various sources (raw headers, $_SERVER array, etc.)
 */
final class HeaderParser
{
    public function __construct(
        private readonly ParserConfig $config = new ParserConfig()
    ) {
    }

    /**
     * Parse headers from raw HTTP header string
     *
     * @param string $rawHeaders Raw headers separated by \r\n
     * @return Headers
     * @throws ParserSecurityException
     */
    public function parseRawHeaders(string $rawHeaders): Headers
    {
        // Security validation: Check total header size
        $headerSize = strlen($rawHeaders);
        if ($headerSize > $this->config->maxTotalHeaderSize->toBytes()) {
            throw ParserSecurityException::headerSizeExceeded(
                $headerSize,
                $this->config->maxTotalHeaderSize->toBytes()
            );
        }

        $headers = [];
        $lines = explode("\r\n", $rawHeaders);

        // Security validation: Check header count
        $headerLineCount = count(array_filter($lines, fn ($line) => str_contains($line, ':')));
        if ($headerLineCount > $this->config->maxHeaderCount) {
            throw ParserSecurityException::headerCountExceeded(
                $headerLineCount,
                $this->config->maxHeaderCount
            );
        }

        foreach ($lines as $line) {
            if ($line === '') {
                // Empty line marks end of headers
                break;
            }

            // Skip the request/status line
            if (! str_contains($line, ':')) {
                continue;
            }

            [$name, $value] = explode(':', $line, 2);
            $name = trim($name);
            $value = trim($value);

            if ($name !== '') {
                // Security validation: Check header name length
                if (strlen($name) > $this->config->maxHeaderNameLength) {
                    throw ParserSecurityException::headerNameTooLong(
                        $name,
                        strlen($name),
                        $this->config->maxHeaderNameLength
                    );
                }

                // Security validation: Check header value length
                if (strlen($value) > $this->config->maxHeaderValueLength) {
                    throw ParserSecurityException::headerValueTooLong(
                        $name,
                        strlen($value),
                        $this->config->maxHeaderValueLength
                    );
                }

                // Security validation: Check for malicious content
                if ($this->config->scanForMaliciousContent) {
                    $this->validateHeaderContent($name, $value);
                }

                if (isset($headers[$name])) {
                    // Multiple headers with same name
                    if (! is_array($headers[$name])) {
                        $headers[$name] = [$headers[$name]];
                    }
                    $headers[$name][] = $value;
                } else {
                    $headers[$name] = $value;
                }
            }
        }

        return new Headers($headers);
    }

    /**
     * Parse headers from $_SERVER array
     *
     * @param array<string, mixed> $server Server array
     * @return Headers
     * @throws ParserSecurityException
     */
    public function parseFromServerArray(array $server): Headers
    {
        $headers = [];
        $totalHeaderSize = 0;

        foreach ($server as $key => $value) {
            // Convert scalar values to string
            if (! is_string($value)) {
                continue;
            }

            $headerName = null;

            // Standard headers with HTTP_ prefix
            if (str_starts_with($key, 'HTTP_')) {
                $headerName = $this->normalizeHeaderName(substr($key, 5));
                $headerValue = $value;
            }
            // Special headers without HTTP_ prefix
            elseif (in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH', 'CONTENT_MD5'])) {
                $headerName = $this->normalizeHeaderName($key);
                $headerValue = $value;
            } else {
                continue;
            }

            // Security validation: Check header name length
            if (strlen($headerName) > $this->config->maxHeaderNameLength) {
                throw ParserSecurityException::headerNameTooLong(
                    $headerName,
                    strlen($headerName),
                    $this->config->maxHeaderNameLength
                );
            }

            // Security validation: Check header value length
            if (strlen($headerValue) > $this->config->maxHeaderValueLength) {
                throw ParserSecurityException::headerValueTooLong(
                    $headerName,
                    strlen($headerValue),
                    $this->config->maxHeaderValueLength
                );
            }

            // Security validation: Check for malicious content
            if ($this->config->scanForMaliciousContent) {
                $this->validateHeaderContent($headerName, $headerValue);
            }

            $headers[$headerName] = $headerValue;
            $totalHeaderSize += strlen($headerName) + strlen($headerValue) + 4; // name: value\r\n
        }

        // Add Authorization header if present in different formats
        if (isset($server['PHP_AUTH_USER']) && isset($server['PHP_AUTH_PW'])) {
            $authHeader = 'Basic ' . base64_encode($server['PHP_AUTH_USER'] . ':' . $server['PHP_AUTH_PW']);

            // Security validation for generated auth header
            if (strlen($authHeader) > $this->config->maxHeaderValueLength) {
                throw ParserSecurityException::headerValueTooLong(
                    'Authorization',
                    strlen($authHeader),
                    $this->config->maxHeaderValueLength
                );
            }

            $headers['Authorization'] = $authHeader;
            $totalHeaderSize += strlen('Authorization') + strlen($authHeader) + 4;
        } elseif (isset($server['PHP_AUTH_DIGEST'])) {
            $authHeader = 'Digest ' . $server['PHP_AUTH_DIGEST'];

            // Security validation for generated auth header
            if (strlen($authHeader) > $this->config->maxHeaderValueLength) {
                throw ParserSecurityException::headerValueTooLong(
                    'Authorization',
                    strlen($authHeader),
                    $this->config->maxHeaderValueLength
                );
            }

            $headers['Authorization'] = $authHeader;
            $totalHeaderSize += strlen('Authorization') + strlen($authHeader) + 4;
        }

        // Security validation: Check header count
        if (count($headers) > $this->config->maxHeaderCount) {
            throw ParserSecurityException::headerCountExceeded(
                count($headers),
                $this->config->maxHeaderCount
            );
        }

        // Security validation: Check total header size
        if ($totalHeaderSize > $this->config->maxTotalHeaderSize->toBytes()) {
            throw ParserSecurityException::headerSizeExceeded(
                $totalHeaderSize,
                $this->config->maxTotalHeaderSize->toBytes()
            );
        }

        return new Headers($headers);
    }

    /**
     * Parse headers from getallheaders() output
     *
     * @param array<string, string> $headers Headers from getallheaders()
     * @return Headers
     */
    public function parseFromGetAllHeaders(array $headers): Headers
    {
        // getallheaders() returns headers in their original case
        // No transformation needed, just pass to Headers constructor
        return new Headers($headers);
    }

    /**
     * Get all headers from current request
     * Uses best available method
     *
     * @return Headers
     */
    public function parseCurrentRequestHeaders(): Headers
    {
        // Prefer getallheaders() if available (Apache, FastCGI)
        if (function_exists('getallheaders')) {
            $headers = getallheaders();
            if ($headers !== false) {
                return $this->parseFromGetAllHeaders($headers);
            }
        }

        // Fallback to $_SERVER parsing
        return $this->parseFromServerArray($_SERVER);
    }

    /**
     * Normalize header name from SERVER format
     * HTTP_CONTENT_TYPE -> Content-Type
     *
     * @param string $serverKey Server key without HTTP_ prefix
     * @return string Normalized header name
     */
    private function normalizeHeaderName(string $serverKey): string
    {
        // Replace underscores with hyphens
        $headerName = str_replace('_', '-', $serverKey);

        // Convert to proper case (Content-Type, X-Forwarded-For, etc.)
        return implode('-', array_map('ucfirst', explode('-', strtolower($headerName))));
    }

    /**
     * Parse Content-Type header to extract media type and parameters
     *
     * @param string $contentType Raw Content-Type header value
     * @return array{type: string, charset?: string, boundary?: string}
     */
    public function parseContentType(string $contentType): array
    {
        $parts = explode(';', $contentType);
        $result = ['type' => trim(array_shift($parts))];

        foreach ($parts as $part) {
            $part = trim($part);
            if (str_contains($part, '=')) {
                [$key, $value] = explode('=', $part, 2);
                $key = trim(strtolower($key));
                $value = trim($value, ' "');

                if ($key === 'charset' || $key === 'boundary') {
                    $result[$key] = $value;
                }
            }
        }

        return $result;
    }

    /**
     * Validate header content for malicious patterns
     *
     * @throws ParserSecurityException
     */
    private function validateHeaderContent(string $name, string $value): void
    {
        // Check for control characters (HTTP headers should not contain control chars)
        if (preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', $name . $value)) {
            throw ParserSecurityException::maliciousContentDetected(
                "Control characters detected in header '{$name}'"
            );
        }

        // Check for CRLF injection (header splitting)
        if (preg_match('/\r|\n/', $name . $value)) {
            throw ParserSecurityException::maliciousContentDetected(
                "CRLF injection detected in header '{$name}'"
            );
        }

        // Check for suspicious patterns in header values
        $maliciousPatterns = [
            '/<script[^>]*>/i',      // Script injection
            '/javascript:/i',         // JavaScript URL
            '/vbscript:/i',          // VBScript URL
            '/on\w+\s*=/i',          // Event handlers
            '/expression\s*\(/i',     // CSS expression
            '/eval\s*\(/i',          // JavaScript eval
            '/alert\s*\(/i',         // JavaScript alert
        ];

        foreach ($maliciousPatterns as $pattern) {
            if (preg_match($pattern, $value)) {
                throw ParserSecurityException::maliciousContentDetected(
                    "Suspicious content detected in header '{$name}'"
                );
            }
        }

        // Check for suspicious header names
        $suspiciousHeaderNames = [
            'X-XSS-Protection',      // Might be used to disable XSS protection
            'Content-Security-Policy', // Might be used to weaken CSP
        ];

        foreach ($suspiciousHeaderNames as $suspiciousName) {
            if (stripos($name, $suspiciousName) !== false) {
                // Check for potentially dangerous values
                if (stripos($value, 'none') !== false || stripos($value, 'unsafe-inline') !== false) {
                    throw ParserSecurityException::maliciousContentDetected(
                        "Potentially dangerous security header value in '{$name}': '{$value}'"
                    );
                }
            }
        }

        // Check for excessively long base64 encoded values (potential data exfiltration)
        if (preg_match('/^[A-Za-z0-9+\/]+=*$/', $value) && strlen($value) > 1000) {
            throw ParserSecurityException::maliciousContentDetected(
                "Suspicious base64 encoded value in header '{$name}'"
            );
        }
    }
}
