<?php

declare(strict_types=1);

namespace App\Framework\GraphQL\Execution;

use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;

/**
 * Simple GraphQL query parser
 *
 * Parses GraphQL query strings into executable AST
 * Supports: queries, mutations, arguments, nested selections
 */
final readonly class QueryParser
{
    /**
     * Parse GraphQL query string
     */
    public function parse(string $query): ParsedQuery
    {
        $query = trim($query);

        // Detect operation type
        $operationType = $this->detectOperationType($query);

        // Extract variables definition
        $variables = $this->extractVariables($query);

        // Extract fields from query body
        $fields = $this->extractFields($query);

        return new ParsedQuery(
            operationType: $operationType,
            fields: $fields,
            variables: $variables
        );
    }

    /**
     * Detect operation type from query string
     */
    private function detectOperationType(string $query): OperationType
    {
        $query = strtolower(trim($query));

        if (str_starts_with($query, 'mutation')) {
            return OperationType::MUTATION;
        }

        if (str_starts_with($query, 'subscription')) {
            return OperationType::SUBSCRIPTION;
        }

        // Default to query (can be implicit with just { ... })
        return OperationType::QUERY;
    }

    /**
     * Extract variables from query definition
     *
     * Example: query($id: ID!, $name: String) { ... }
     */
    private function extractVariables(string $query): array
    {
        // Match variable definitions in query/mutation declaration
        if (preg_match('/(?:query|mutation)\s*(?:\w+)?\s*\(([^)]+)\)/', $query, $matches)) {
            return $this->parseVariableDefinitions($matches[1]);
        }

        return [];
    }

    /**
     * Parse variable definitions string
     */
    private function parseVariableDefinitions(string $varDefs): array
    {
        $variables = [];
        $parts = explode(',', $varDefs);

        foreach ($parts as $part) {
            $part = trim($part);
            if (empty($part)) {
                continue;
            }

            // Parse $varName: Type format
            if (preg_match('/\$(\w+):\s*(.+)/', $part, $matches)) {
                $varName = $matches[1];
                $varType = trim($matches[2]);
                $variables[$varName] = ['type' => $varType];
            }
        }

        return $variables;
    }

    /**
     * Extract fields from query body
     *
     * @return array<ParsedField>
     */
    private function extractFields(string $query): array
    {
        // Extract content between first { and last }
        if (! preg_match('/{(.+)}/s', $query, $matches)) {
            throw FrameworkException::create(
                ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
                'Invalid GraphQL query: missing selection set'
            );
        }

        $body = trim($matches[1]);

        return $this->parseSelectionSet($body);
    }

    /**
     * Parse selection set (fields inside {})
     *
     * @return array<ParsedField>
     */
    private function parseSelectionSet(string $selectionSet): array
    {
        $fields = [];
        $currentField = '';
        $braceDepth = 0;
        $parenDepth = 0;

        $chars = str_split($selectionSet);
        $length = count($chars);

        for ($i = 0; $i < $length; $i++) {
            $char = $chars[$i];

            if ($char === '{') {
                $braceDepth++;
                $currentField .= $char;
            } elseif ($char === '}') {
                $braceDepth--;
                $currentField .= $char;
            } elseif ($char === '(') {
                $parenDepth++;
                $currentField .= $char;
            } elseif ($char === ')') {
                $parenDepth--;
                $currentField .= $char;
            } elseif ($char === "\n" && $braceDepth === 0 && $parenDepth === 0) {
                // Field separator at root level
                if (! empty(trim($currentField))) {
                    $fields[] = $this->parseField(trim($currentField));
                }
                $currentField = '';
            } else {
                $currentField .= $char;
            }
        }

        // Add last field
        if (! empty(trim($currentField))) {
            $fields[] = $this->parseField(trim($currentField));
        }

        return $fields;
    }

    /**
     * Parse single field
     */
    private function parseField(string $fieldStr): ParsedField
    {
        // Match: [alias:] fieldName [(args)] [{ selections }]
        $pattern = '/^(?:(\w+)\s*:\s*)?(\w+)(?:\(([^)]*)\))?(?:\s*\{(.+)\})?$/s';

        if (! preg_match($pattern, $fieldStr, $matches)) {
            // Simple field without args or selections
            return new ParsedField(name: trim($fieldStr));
        }

        $alias = ! empty($matches[1]) ? $matches[1] : null;
        $name = $matches[2];
        $argsStr = $matches[3] ?? '';
        $selectionsStr = $matches[4] ?? '';

        $arguments = ! empty($argsStr) ? $this->parseArguments($argsStr) : [];
        $selections = ! empty($selectionsStr) ? $this->parseSelectionSet($selectionsStr) : [];

        return new ParsedField(
            name: $name,
            arguments: $arguments,
            selections: $selections,
            alias: $alias
        );
    }

    /**
     * Parse field arguments
     *
     * @return array<string, mixed>
     */
    private function parseArguments(string $argsStr): array
    {
        $arguments = [];
        $parts = $this->splitArguments($argsStr);

        foreach ($parts as $part) {
            $part = trim($part);
            if (empty($part)) {
                continue;
            }

            // Parse key: value
            if (preg_match('/^(\w+):\s*(.+)$/', $part, $matches)) {
                $key = $matches[1];
                $value = $this->parseValue(trim($matches[2]));
                $arguments[$key] = $value;
            }
        }

        return $arguments;
    }

    /**
     * Split arguments by comma (respecting nested structures)
     *
     * @return array<string>
     */
    private function splitArguments(string $argsStr): array
    {
        $parts = [];
        $current = '';
        $depth = 0;
        $inString = false;
        $stringChar = null;

        foreach (str_split($argsStr) as $char) {
            if ($char === '"' || $char === "'") {
                if (! $inString) {
                    $inString = true;
                    $stringChar = $char;
                } elseif ($char === $stringChar) {
                    $inString = false;
                }
                $current .= $char;
            } elseif ($inString) {
                $current .= $char;
            } elseif ($char === '{' || $char === '[') {
                $depth++;
                $current .= $char;
            } elseif ($char === '}' || $char === ']') {
                $depth--;
                $current .= $char;
            } elseif ($char === ',' && $depth === 0) {
                $parts[] = $current;
                $current = '';
            } else {
                $current .= $char;
            }
        }

        if (! empty($current)) {
            $parts[] = $current;
        }

        return $parts;
    }

    /**
     * Parse argument value
     */
    private function parseValue(string $value): mixed
    {
        $value = trim($value);

        // Variable reference
        if (str_starts_with($value, '$')) {
            return $value; // Keep as variable reference
        }

        // Boolean
        if ($value === 'true') {
            return true;
        }
        if ($value === 'false') {
            return false;
        }

        // Null
        if ($value === 'null') {
            return null;
        }

        // String
        if ((str_starts_with($value, '"') && str_ends_with($value, '"')) ||
            (str_starts_with($value, "'") && str_ends_with($value, "'"))) {
            return substr($value, 1, -1);
        }

        // Number
        if (is_numeric($value)) {
            return str_contains($value, '.') ? (float) $value : (int) $value;
        }

        // Array
        if (str_starts_with($value, '[') && str_ends_with($value, ']')) {
            $inner = substr($value, 1, -1);
            $items = $this->splitArguments($inner);

            return array_map(fn ($item) => $this->parseValue($item), $items);
        }

        // Object (simplified)
        if (str_starts_with($value, '{') && str_ends_with($value, '}')) {
            // For now, return as string - proper object parsing would be more complex
            return $value;
        }

        return $value;
    }
}
