<?php

declare(strict_types=1);

namespace App\Framework\Http\Parser;

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

/**
 * Parser for form data with security validation
 * (application/x-www-form-urlencoded and multipart/form-data)
 */
final readonly class FormDataParser
{
    public function __construct(
        private ParserConfig      $config = new ParserConfig(),
        private QueryStringParser $queryStringParser = new QueryStringParser()
    ) {
    }

    /**
     * Parse form data based on content type
     *
     * @param string $contentType The Content-Type header value
     * @param string $body The raw request body
     * @return array<string, mixed> Parsed form data
     * @throws ParserSecurityException
     */
    public function parse(string $contentType, string $body): array
    {
        if ($body === '') {
            return [];
        }

        // Security validation: Check form data size
        $bodySize = strlen($body);
        if ($bodySize > $this->config->maxFormDataSize->toBytes()) {
            throw ParserSecurityException::formDataSizeExceeded(
                $bodySize,
                $this->config->maxFormDataSize->toBytes()
            );
        }

        // Parse content type to get media type and parameters
        $ctParts = explode(';', $contentType);
        $mediaType = strtolower(trim($ctParts[0]));

        switch ($mediaType) {
            case 'application/x-www-form-urlencoded':
                return $this->parseUrlEncoded($body);

            case 'multipart/form-data':
                $boundary = $this->extractBoundary($contentType);
                if ($boundary === null) {
                    throw new \RuntimeException('Missing boundary in multipart/form-data');
                }

                return $this->parseMultipart($body, $boundary);

            default:
                // Not form data
                return [];
        }
    }

    /**
     * Parse URL-encoded form data
     * Same format as query strings
     *
     * @param string $body The raw body
     * @return array<string, mixed>
     */
    private function parseUrlEncoded(string $body): array
    {
        return $this->queryStringParser->parse($body);
    }

    /**
     * Parse multipart/form-data
     *
     * @param string $body The raw body
     * @param string $boundary The multipart boundary
     * @return array<string, mixed> Form fields (not files)
     * @throws ParserSecurityException
     */
    private function parseMultipart(string $body, string $boundary): array
    {
        $data = [];

        // Security validation: Check boundary length
        if (strlen($boundary) > $this->config->maxBoundaryLength) {
            throw ParserSecurityException::boundaryTooLong(
                strlen($boundary),
                $this->config->maxBoundaryLength
            );
        }

        // Split by boundary
        $parts = explode("--$boundary", $body);

        // Security validation: Check multipart parts count
        $actualParts = array_filter($parts, fn ($part) => trim($part) !== '' && trim($part) !== '--');
        if (count($actualParts) > $this->config->maxMultipartParts) {
            throw ParserSecurityException::multipartPartsExceeded(
                count($actualParts),
                $this->config->maxMultipartParts
            );
        }

        $fieldCount = 0;

        foreach ($parts as $part) {
            // Skip empty parts and closing boundary
            if (trim($part) === '' || trim($part) === '--') {
                continue;
            }

            // Split headers and content
            $sections = explode("\r\n\r\n", $part, 2);
            if (count($sections) !== 2) {
                continue;
            }

            [$headers, $content] = $sections;

            // Remove trailing CRLF from content
            $content = rtrim($content, "\r\n");

            // Parse Content-Disposition header
            $disposition = $this->parseContentDisposition($headers);
            if ($disposition === null || ! isset($disposition['name'])) {
                continue;
            }

            $name = $disposition['name'];

            // Check if this is a file upload (has filename parameter)
            if (isset($disposition['filename'])) {
                // Skip file uploads in form data parser
                // These should be handled by FileUploadParser
                continue;
            }

            // Security validation: Check field count
            $fieldCount++;
            if ($fieldCount > $this->config->maxFieldCount) {
                throw ParserSecurityException::fieldCountExceeded(
                    $fieldCount,
                    $this->config->maxFieldCount
                );
            }

            // Security validation: Check field name length
            if (strlen($name) > $this->config->maxFieldNameLength) {
                throw ParserSecurityException::fieldNameTooLong(
                    $name,
                    strlen($name),
                    $this->config->maxFieldNameLength
                );
            }

            // Security validation: Check field value length
            if (strlen($content) > $this->config->maxFieldValueLength) {
                throw ParserSecurityException::fieldValueTooLong(
                    $name,
                    strlen($content),
                    $this->config->maxFieldValueLength
                );
            }

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

            // Parse field name for array notation
            $data = $this->assignFieldValue($data, $name, $content);
        }

        return $data;
    }

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

            return $boundary;
        }

        return null;
    }

    /**
     * Parse Content-Disposition header
     *
     * @param string $headers The headers section of a multipart part
     * @return array{name?: string, filename?: string}|null
     */
    private function parseContentDisposition(string $headers): ?array
    {
        $lines = explode("\r\n", $headers);

        foreach ($lines as $line) {
            if (stripos($line, 'Content-Disposition:') === 0) {
                $disposition = [];
                $parts = explode(';', $line);

                foreach ($parts as $part) {
                    $part = trim($part);

                    if (strpos($part, '=') !== false) {
                        [$key, $value] = explode('=', $part, 2);
                        $key = trim(strtolower($key));
                        $value = trim($value, ' "');

                        if ($key === 'name' || $key === 'filename') {
                            $disposition[$key] = $value;
                        }
                    }
                }

                return $disposition;
            }
        }

        return null;
    }

    /**
     * Assign field value, handling array notation
     *
     * @param array<string, mixed> $data Data array
     * @param string $name Field name (may include [] notation)
     * @param string $value Field value
     * @return array<string, mixed> Updated data array
     */
    private function assignFieldValue(array $data, string $name, string $value): array
    {
        // Check for array notation
        if (preg_match('/^([^\[]+)(\[.*\])$/', $name, $matches)) {
            $baseKey = $matches[1];
            $arrayPart = $matches[2];

            // Initialize array if needed
            if (! isset($data[$baseKey])) {
                $data[$baseKey] = [];
            }

            // Simple array notation field[]
            if ($arrayPart === '[]') {
                if (! is_array($data[$baseKey])) {
                    $data[$baseKey] = [$data[$baseKey]];
                }
                $data[$baseKey][] = $value;
            } else {
                // Complex array notation field[key] or field[key1][key2]
                $data = $this->assignNestedValue($data, $name, $value);
            }
        } else {
            // Simple field
            $data[$name] = $value;
        }

        return $data;
    }

    /**
     * Handle nested array notation
     * Delegates to QueryStringParser for consistency
     */
    private function assignNestedValue(array $data, string $name, string $value): array
    {
        // Create a temporary query string and parse it
        $encoded = urlencode($name) . '=' . urlencode($value);
        $parsed = $this->queryStringParser->parse($encoded);

        // Merge with existing data
        return array_merge_recursive($data, $parsed);
    }

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

        // Check for script injection patterns
        $maliciousPatterns = [
            '/<script[^>]*>/i',      // Script tags
            '/javascript:/i',         // JavaScript URLs
            '/vbscript:/i',          // VBScript URLs
            '/on\w+\s*=/i',          // Event handlers like onclick, onload
            '/expression\s*\(/i',     // CSS expression
            '/eval\s*\(/i',          // JavaScript eval
            '/alert\s*\(/i',         // JavaScript alert
            '/<iframe[^>]*>/i',      // Iframe injection
            '/<object[^>]*>/i',      // Object injection
            '/<embed[^>]*>/i',       // Embed injection
        ];

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

        // Check for SQL injection patterns
        $sqlPatterns = [
            '/union\s+select/i',
            '/select\s+.*\s+from/i',
            '/insert\s+into/i',
            '/delete\s+from/i',
            '/drop\s+table/i',
            '/truncate\s+table/i',
            '/alter\s+table/i',
            '/create\s+table/i',
        ];

        foreach ($sqlPatterns as $pattern) {
            if (preg_match($pattern, $value)) {
                throw ParserSecurityException::maliciousContentDetected(
                    "SQL injection attempt detected in form field '{$name}'"
                );
            }
        }

        // Check for path traversal attempts
        if (preg_match('/\.\.\/|\.\.\\\\/', $value)) {
            throw ParserSecurityException::maliciousContentDetected(
                "Path traversal attempt detected in form field '{$name}'"
            );
        }

        // Check for excessive repetition (potential DoS)
        if (preg_match('/(.)\1{100,}/', $value)) {
            throw ParserSecurityException::maliciousContentDetected(
                "Excessive character repetition detected in form field '{$name}'"
            );
        }
    }
}
