<?php

declare(strict_types=1);

namespace App\Framework\Http\Parser;

use App\Framework\Http\Headers;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\Method;
use App\Framework\Http\Parser\Exception\ParserSecurityException;
use App\Framework\Http\Request;
use App\Framework\Http\RequestBody;
use App\Framework\Http\RequestIdGenerator;
use App\Framework\Http\ServerEnvironment;
use App\Framework\Http\UploadedFile;
use App\Framework\Http\UploadedFiles;
use InvalidArgumentException;

/**
 * Main HTTP request parser that coordinates all sub-parsers
 * Parses raw HTTP requests without relying on superglobals
 */
final readonly class HttpRequestParser
{
    private ParserConfig $config;

    public function __construct(
        ParserCache $cache,
        ?ParserConfig $config = null,
        ?QueryStringParser $queryParser = null,
        ?CookieParser $cookieParser = null,
        ?FormDataParser $formParser = null,
        ?FileUploadParser $fileParser = null,
        ?HeaderParser $headerParser = null,
        ?StreamingParser $streamingParser = null,
        ?RequestIdGenerator $requestIdGenerator = null
    ) {
        // Use web-friendly configuration by default (less strict than default security config)
        $this->config = $config ?? self::createWebConfig();

        $this->queryParser = $queryParser ?? new QueryStringParser($cache, $this->config);
        $this->cookieParser = $cookieParser ?? new CookieParser($cache, $this->config);
        $this->formParser = $formParser ?? new FormDataParser($this->config, $this->queryParser);
        $this->fileParser = $fileParser ?? new FileUploadParser($this->config);
        $this->headerParser = $headerParser ?? new HeaderParser($this->config);
        $this->streamingParser = $streamingParser ?? new StreamingParser($this->config);
        $this->requestIdGenerator = $requestIdGenerator ?? new RequestIdGenerator();
    }

    /**
     * Create a web-friendly parser configuration
     * More permissive than the default security config for normal web traffic
     */
    private static function createWebConfig(): ParserConfig
    {
        return new ParserConfig(
            // Reasonable web limits
            maxFileSize: \App\Framework\Core\ValueObjects\Byte::fromMegabytes(50),
            maxFileCount: 10,
            maxTotalUploadSize: \App\Framework\Core\ValueObjects\Byte::fromMegabytes(100),
            maxFormDataSize: \App\Framework\Core\ValueObjects\Byte::fromMegabytes(10),
            maxQueryStringLength: 8192,
            maxQueryParameters: 100,
            maxHeaderCount: 50,
            maxCookieCount: 50,

            // Relaxed security for normal web use
            validateFileExtensions: false, // Don't block file extensions for normal web use
            blockedFileExtensions: ['php', 'exe', 'bat', 'sh', 'asp', 'jsp'], // Don't scan content for normal web use
            scanForMaliciousContent: false,

            // Still block obviously dangerous extensions
            strictMimeTypeValidation: false,

            // Performance settings
            throwOnLimitExceeded: true,
            logSecurityViolations: false, // Don't log normal violations in web context
        );
    }

    private QueryStringParser $queryParser;

    private CookieParser $cookieParser;

    private FormDataParser $formParser;

    private FileUploadParser $fileParser;

    private HeaderParser $headerParser;

    private StreamingParser $streamingParser;

    private RequestIdGenerator $requestIdGenerator;

    /**
     * Parse request from minimal PHP globals
     * Only uses $_SERVER for basic info that can't be obtained otherwise
     *
     * @param array<string, mixed> $server Server array (usually $_SERVER)
     * @param string $rawBody Raw request body (usually from php://input)
     * @return Request
     */
    public function parseFromGlobals(array $server, string $rawBody): Request
    {
        // Get basic request info (these can't be obtained any other way in PHP)
        $method = $server['REQUEST_METHOD'] ?? 'GET';
        $uri = $server['REQUEST_URI'] ?? '/';
        #$protocol = $server['SERVER_PROTOCOL'] ?? 'HTTP/1.1';

        // Parse with our custom parsers
        return $this->parseRequest(
            method: $method,
            uri: $uri,
            server: $server,
            rawBody: $rawBody
        );
    }

    /**
     * Parse request from raw components
     * This is the main parsing method that doesn't rely on superglobals
     *
     * @param string $method HTTP method
     * @param string $uri Request URI (path + query)
     * @param array<string, mixed> $server Server environment data
     * @param string $rawBody Raw request body
     * @return Request
     */
    public function parseRequest(
        string $method,
        string $uri,
        array $server,
        string $rawBody
    ): Request {
        // Security: Validate request body size
        $this->validateRequestBodySize($rawBody);

        // Security: Validate URI length
        $this->validateUriLength($uri);

        // Parse URI components
        $uriComponents = parse_url($uri);
        if ($uriComponents === false) {
            throw new InvalidArgumentException("Invalid URI: $uri");
        }

        $path = $uriComponents['path'] ?? '/';
        $queryString = $uriComponents['query'] ?? '';

        // Normalize path
        if ($path !== '/') {
            $path = rtrim($path, '/');
        }

        // Create server environment
        $serverEnvironment = new ServerEnvironment($server);

        // Parse headers
        $headers = $this->headerParser->parseFromServerArray($server);

        // Parse cookies from Cookie header
        $cookieHeader = $headers->getFirst('Cookie') ?? '';
        $cookies = $this->cookieParser->parseToCookies($cookieHeader);

        // Parse query parameters
        $queryParams = $this->queryParser->parse($queryString);

        // Determine content type
        $contentType = $headers->getFirst('Content-Type', '');

        // Parse body based on method and content type
        $parsedBody = [];
        $uploadedFiles = new UploadedFiles([]);

        if (in_array($method, ['POST', 'PUT', 'PATCH'])) {
            if (str_contains($contentType, 'multipart/form-data')) {
                // For multipart/form-data, PHP automatically populates $_POST and $_FILES
                // and makes php://input empty. Use $_POST and $_FILES directly in this case.
                if (strlen($rawBody) === 0 && (! empty($_POST) || ! empty($_FILES))) {
                    $parsedBody = $_POST;

                    // Also handle $_FILES if available
                    if (! empty($_FILES)) {
                        $uploadedFiles = UploadedFiles::fromFilesArray($_FILES);
                    }
                } else {
                    // Extract boundary
                    $boundary = $this->extractBoundary($contentType);
                    if ($boundary !== null) {
                        // Use streaming parser for large requests
                        if ($this->shouldUseStreamingParser($rawBody)) {
                            [$parsedBody, $uploadedFiles] = $this->parseWithStreamingParser($rawBody, $boundary);
                        } else {
                            // Parse both form fields and files with regular parsers
                            $parsedBody = $this->formParser->parse($contentType, $rawBody);
                            $uploadedFiles = $this->fileParser->parseMultipart($rawBody, $boundary);
                        }
                    }
                }
            } else {
                // Parse as regular form data
                $parsedBody = $this->formParser->parse($contentType, $rawBody);
            }
        }

        // Create a method enum
        $methodEnum = Method::tryFrom($method) ?? Method::GET;

        // Check for method override in parsed body
        if ($methodEnum === Method::POST && isset($parsedBody['_method'])) {
            $overrideMethod = Method::tryFrom($parsedBody['_method']);
            if ($overrideMethod !== null) {
                $methodEnum = $overrideMethod;
            }
        }

        // Create RequestBody object
        // For GET requests, pass query params as parsed data
        $bodyData = $methodEnum === Method::GET ? $queryParams : $parsedBody;

        $requestBody = new RequestBody(
            method: $methodEnum,
            headers: $headers,
            body: $rawBody,
            parsedData: $bodyData
        );

        // Generate request ID
        $requestId = $this->requestIdGenerator->generate($headers);

        // Create the request
        return new HttpRequest(
            method: $methodEnum,
            headers: $headers,
            body: $rawBody,
            path: $path,
            queryParams: $queryParams,
            files: $uploadedFiles,
            cookies: $cookies,
            server: $serverEnvironment,
            id: $requestId,
            parsedBody: $requestBody
        );
    }

    /**
     * Parse a raw HTTP request string (for testing or special cases)
     *
     * @param string $rawRequest Complete raw HTTP request including headers and body
     * @return Request
     */
    public function parseRawHttpRequest(string $rawRequest): Request
    {
        // Split request into headers and body
        $parts = explode("\r\n\r\n", $rawRequest, 2);
        $headerSection = $parts[0];
        $body = $parts[1] ?? '';

        // Parse request line
        $lines = explode("\r\n", $headerSection);
        $requestLine = array_shift($lines);

        if (! preg_match('/^(\S+)\s+(\S+)\s+(\S+)$/', $requestLine, $matches)) {
            throw new InvalidArgumentException("Invalid request line: $requestLine");
        }

        $method = $matches[1];
        $uri = $matches[2];
        $protocol = $matches[3];

        // Parse headers from remaining lines
        $headerString = implode("\r\n", $lines);
        $headers = $this->headerParser->parseRawHeaders($headerString);

        // Create minimal server array
        $server = [
            'REQUEST_METHOD' => $method,
            'REQUEST_URI' => $uri,
            'SERVER_PROTOCOL' => $protocol,
        ];

        // Add some headers to server array for compatibility
        if ($headers->has('Host')) {
            $server['HTTP_HOST'] = $headers->getFirst('Host');
        }
        if ($headers->has('User-Agent')) {
            $server['HTTP_USER_AGENT'] = $headers->getFirst('User-Agent');
        }

        return $this->parseRequest($method, $uri, $server, $body);
    }

    /**
     * Extract boundary from Content-Type header
     *
     * @param string $contentType Content-Type header value
     * @return string|null Boundary or null if not found
     */
    private function extractBoundary(string $contentType): ?string
    {
        if (preg_match('/boundary=([^;]+)/', $contentType, $matches)) {
            return trim($matches[1], ' "');
        }

        return null;
    }

    /**
     * Validate request body size against security limits
     *
     * @param string $rawBody Raw request body
     * @throws ParserSecurityException If body size exceeds limits
     */
    private function validateRequestBodySize(string $rawBody): void
    {
        $bodySize = strlen($rawBody);

        // Check against maximum total upload size (includes form data + files)
        if ($bodySize > $this->config->maxTotalUploadSize->toBytes()) {
            throw ParserSecurityException::requestBodySizeExceeded(
                $bodySize,
                $this->config->maxTotalUploadSize->toBytes()
            );
        }
    }

    /**
     * Validate URI length against security limits
     *
     * @param string $uri Request URI
     * @throws ParserSecurityException If URI is too long
     */
    private function validateUriLength(string $uri): void
    {
        $uriLength = strlen($uri);

        // Most web servers have a limit around 8KB for the request line
        // We use a conservative 4KB limit for security
        $maxUriLength = 4096;

        if ($uriLength > $maxUriLength) {
            throw ParserSecurityException::simple(
                "URI too long: {$uriLength} characters > {$maxUriLength} maximum"
            );
        }
    }

    /**
     * Determine if streaming parser should be used based on request size
     *
     * @param string $rawBody
     * @return bool
     */
    private function shouldUseStreamingParser(string $rawBody): bool
    {
        // Use streaming parser for requests larger than 10MB
        $streamingThreshold = 10 * 1024 * 1024; // 10MB

        return strlen($rawBody) > $streamingThreshold;
    }

    /**
     * Parse multipart data using the streaming parser
     *
     * @param string $rawBody
     * @param string $boundary
     * @return array{0: array<string, mixed>, 1: UploadedFiles}
     */
    private function parseWithStreamingParser(string $rawBody, string $boundary): array
    {
        // Create stream from raw body
        $stream = fopen('php://memory', 'r+');
        fwrite($stream, $rawBody);
        rewind($stream);

        $formData = [];
        $files = [];

        try {
            // Process all parts using the generator
            foreach ($this->streamingParser->streamMultipart($stream, $boundary) as $part) {
                if ($part['type'] === 'file') {
                    // Handle file upload
                    if (isset($part['stream'])) {
                        // Create temporary file
                        $tempPath = tempnam(sys_get_temp_dir(), 'upload_');
                        if ($tempPath === false) {
                            throw new \RuntimeException('Failed to create temporary file');
                        }

                        $tempHandle = fopen($tempPath, 'w');
                        if ($tempHandle === false) {
                            throw new \RuntimeException('Failed to open temporary file for writing');
                        }

                        stream_copy_to_stream($part['stream'], $tempHandle);
                        fclose($tempHandle);
                        fclose($part['stream']);

                        // Create UploadedFile
                        $uploadedFile = new UploadedFile(
                            name: $part['filename'] ?? '',
                            type: $part['headers']['Content-Type'] ?? 'application/octet-stream',
                            size: filesize($tempPath) ?: 0,
                            tmpName: $tempPath,
                            error: \App\Framework\Http\UploadError::OK,
                            skipValidation: true
                        );

                        // Handle array notation
                        $name = $part['name'];
                        if (str_contains($name, '[')) {
                            $this->addArrayFile($files, $name, $uploadedFile);
                        } else {
                            $files[$name] = $uploadedFile;
                        }
                    }
                } else {
                    // Handle form field
                    $name = $part['name'];
                    $value = $part['data'] ?? '';

                    // Handle array notation in form fields
                    if (str_contains($name, '[')) {
                        $this->addArrayValue($formData, $name, $value);
                    } else {
                        $formData[$name] = $value;
                    }
                }
            }
        } finally {
            fclose($stream);
        }

        return [$formData, new UploadedFiles($files)];
    }

    /**
     * Add file to array structure based on field name
     *
     * @param array<string, mixed> &$files
     * @param string $name
     * @param UploadedFile $file
     */
    private function addArrayFile(array &$files, string $name, UploadedFile $file): void
    {
        // Parse array notation like "files[0]" or "files[avatar]"
        if (preg_match('/^([^\[]+)\[([^]]*)](.*)$/', $name, $matches)) {
            $baseName = $matches[1];
            $key = $matches[2];
            $remainder = $matches[3];

            if (! isset($files[$baseName])) {
                $files[$baseName] = [];
            }

            if ($remainder) {
                // Nested array
                if ($key === '') {
                    $key = count($files[$baseName]);
                }
                $this->addArrayFile($files[$baseName], $key . $remainder, $file);
            } else {
                // Final level
                if ($key === '') {
                    $files[$baseName][] = $file;
                } else {
                    $files[$baseName][$key] = $file;
                }
            }
        }
    }

    /**
     * Add value to array structure based on field name
     *
     * @param array<string, mixed> &$data
     * @param string $name
     * @param string $value
     */
    private function addArrayValue(array &$data, string $name, string $value): void
    {
        // Parse array notation like "fields[0]" or "fields[name]"
        if (preg_match('/^([^\[]+)\[([^]]*)](.*)$/', $name, $matches)) {
            $baseName = $matches[1];
            $key = $matches[2];
            $remainder = $matches[3];

            if (! isset($data[$baseName])) {
                $data[$baseName] = [];
            }

            if ($remainder) {
                // Nested array
                if ($key === '') {
                    $key = count($data[$baseName]);
                }
                $this->addArrayValue($data[$baseName], $key . $remainder, $value);
            } else {
                // Final level
                if ($key === '') {
                    $data[$baseName][] = $value;
                } else {
                    $data[$baseName][$key] = $value;
                }
            }
        }
    }
}
