<?php

declare(strict_types=1);

namespace App\Framework\Http\Parser;

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

/**
 * Parser for URL query strings with security validation
 * Handles complex cases like arrays, nested values, and special characters
 */
final readonly class QueryStringParser
{
    public function __construct(
        private ParserCache  $cache,
        private ParserConfig $config,
    ) {
    }

    /**
     * Parse a query string into an associative array
     *
     * @param string $queryString The raw query string (without leading ?)
     * @return array<string, mixed> Parsed parameters
     * @throws ParserSecurityException
     */
    public function parse(string $queryString): array
    {
        if ($queryString === '') {
            return [];
        }

        // Try cache first
        $cached = $this->cache->getQueryString($queryString);
        if ($cached !== null) {
            return $cached;
        }

        // Security validation
        if (strlen($queryString) > $this->config->maxQueryStringLength) {
            throw ParserSecurityException::queryStringSizeExceeded(
                strlen($queryString),
                $this->config->maxQueryStringLength
            );
        }

        $params = [];
        $pairs = explode('&', $queryString);

        // Check parameter count
        if (count($pairs) > $this->config->maxQueryParameters) {
            throw ParserSecurityException::queryParameterCountExceeded(
                count($pairs),
                $this->config->maxQueryParameters
            );
        }

        foreach ($pairs as $pair) {
            if ($pair === '') {
                continue;
            }

            $parts = explode('=', $pair, 2);
            $key = $this->decodeComponent($parts[0]);
            $value = isset($parts[1]) ? $this->decodeComponent($parts[1]) : '';

            $params = $this->assignValue($params, $key, $value);
        }

        // Cache the result before returning
        $this->cache->setQueryString($queryString, $params);

        return $params;
    }

    /**
     * Decode URL component with proper error handling
     */
    private function decodeComponent(string $component): string
    {
        // Replace + with space before URL decoding (form encoding standard)
        $component = str_replace('+', ' ', $component);

        // Decode percent-encoded characters
        return rawurldecode($component);
    }

    /**
     * Assign value to params array, handling array notation
     *
     * @param array<string, mixed> $params Params array
     * @param string $key The parameter key (may include [] notation)
     * @param string $value The parameter value
     * @return array<string, mixed> Updated params array
     */
    private function assignValue(array $params, string $key, string $value): array
    {
        // Handle array notation like key[] or key[index]
        if (preg_match('/^([^\[]+)(\[.*])$/', $key, $matches)) {
            $baseKey = $matches[1];
            $arrayPart = $matches[2];

            return $this->assignArrayValue($params, $baseKey, $arrayPart, $value);
        } else {
            // Simple key=value
            $params[$key] = $value;

            return $params;
        }
    }

    /**
     * Handle array notation in query parameters
     */
    private function assignArrayValue(array $params, string $baseKey, string $arrayPart, string $value): array
    {
        // Initialize array if not exists
        if (! isset($params[$baseKey])) {
            $params[$baseKey] = [];
        }

        // Parse array notation: [], [key], [key1][key2], etc.
        if ($arrayPart === '[]') {
            // Simple array push
            if (! is_array($params[$baseKey])) {
                $params[$baseKey] = [$params[$baseKey]];
            }
            $params[$baseKey][] = $value;
        } else {
            // Parse nested keys
            $keys = $this->parseArrayKeys($arrayPart);
            $params[$baseKey] = $this->assignNestedValue($params[$baseKey], $keys, $value);
        }

        return $params;
    }

    /**
     * Parse array keys from notation like [key1][key2][key3]
     *
     * @return string[] Array of keys
     */
    private function parseArrayKeys(string $arrayPart): array
    {
        preg_match_all('/\[([^]]*)]/', $arrayPart, $matches);

        return $matches[1];
    }

    /**
     * Assign value to nested array structure (immutable)
     *
     * @param mixed $target Target array/value
     * @param string[] $keys Array of keys to traverse
     * @param string $value Value to assign
     * @return mixed Updated structure
     */
    private function assignNestedValue(mixed $target, array $keys, string $value): mixed
    {
        if (empty($keys)) {
            return $value;
        }

        $key = array_shift($keys);

        if (! is_array($target)) {
            $target = [];
        }

        if ($key === '') {
            // Empty key means array push
            $index = count($target);
            $target[$index] = $this->assignNestedValue($target[$index] ?? null, $keys, $value);
        } else {
            $target[$key] = $this->assignNestedValue($target[$key] ?? null, $keys, $value);
        }

        return $target;
    }
}
