<?php

declare(strict_types=1);

namespace App\Framework\Console;

/**
 * Advanced argument parser for console commands
 *
 * Supports:
 * - Long options: --option=value, --option value, --flag
 * - Short options: -o value, -f, -abc (combined flags)
 * - Positional arguments
 * - Type validation and casting
 * - Required/optional arguments
 */
final readonly class ArgumentParser
{
    /** @param array<string, ArgumentDefinition> $definitions */
    public function __construct(
        private array $definitions = []
    ) {
    }

    /**
     * Parse command line arguments
     *
     * @param string[] $arguments Raw command line arguments
     */
    public function parse(array $arguments): ParsedArguments
    {
        $parsed = [
            'arguments' => [],
            'options' => [],
            'flags' => [],
            'positional' => [],
        ];

        $i = 0;
        while ($i < count($arguments)) {
            $arg = $arguments[$i];

            if (str_starts_with($arg, '--')) {
                $i = $this->parseLongOption($arguments, $i, $parsed);
            } elseif (str_starts_with($arg, '-') && strlen($arg) > 1) {
                $i = $this->parseShortOption($arguments, $i, $parsed);
            } else {
                $this->parsePositionalArgument($arg, $parsed);
                $i++;
            }
        }

        // Merge all parsed values
        $allValues = array_merge($parsed['arguments'], $parsed['options'], $parsed['flags']);

        // Apply defaults for missing values
        foreach ($this->definitions as $name => $definition) {
            if (! array_key_exists($name, $allValues) && $definition->default !== null) {
                $allValues[$name] = $definition->default;
            }
        }

        $result = new ParsedArguments(
            $parsed['arguments'],
            array_merge($parsed['options'], $parsed['flags']),
            $this->definitions
        );

        // Validate all requirements
        $result->validate();

        return $result;
    }

    /**
     * Parse long option (--option or --option=value)
     */
    private function parseLongOption(array $arguments, int $index, array &$parsed): int
    {
        $arg = $arguments[$index];
        $optionPart = substr($arg, 2); // Remove '--'

        // Handle --option=value
        if (str_contains($optionPart, '=')) {
            [$name, $value] = explode('=', $optionPart, 2);
            $camelCaseName = $this->kebabToCamelCase($name);
            $parsed['options'][$camelCaseName] = $this->parseValue($camelCaseName, $value);

            return $index + 1;
        }

        // Convert kebab-case to camelCase for definition lookup
        $camelCaseName = $this->kebabToCamelCase($optionPart);

        // Handle --option value or --flag
        $definition = $this->findDefinitionByName($camelCaseName);

        if ($definition && $definition->type === ArgumentType::BOOLEAN) {
            // Boolean flag - use camelCase name
            $parsed['flags'][$camelCaseName] = true;

            return $index + 1;
        }

        // Option that expects a value
        if ($index + 1 >= count($arguments) || str_starts_with($arguments[$index + 1], '-')) {
            if ($definition && $definition->required) {
                throw new \InvalidArgumentException("Option '--{$optionPart}' requires a value");
            }
            // Optional option without value - treat as flag
            $parsed['flags'][$camelCaseName] = true;

            return $index + 1;
        }

        $parsed['options'][$camelCaseName] = $this->parseValue($camelCaseName, $arguments[$index + 1]);

        return $index + 2;
    }

    /**
     * Convert kebab-case to camelCase
     * Example: dry-run -> dryRun
     */
    private function kebabToCamelCase(string $kebab): string
    {
        return lcfirst(str_replace('-', '', ucwords($kebab, '-')));
    }

    /**
     * Parse short option (-o or -o value or -abc)
     */
    private function parseShortOption(array $arguments, int $index, array &$parsed): int
    {
        $arg = $arguments[$index];
        $options = substr($arg, 1); // Remove '-'

        // Handle combined short options: -abc = -a -b -c
        if (strlen($options) > 1) {
            foreach (str_split($options) as $shortOption) {
                $definition = $this->findDefinitionByShortName($shortOption);
                $longName = $definition?->name ?? $shortOption;

                if ($definition && $definition->type !== ArgumentType::BOOLEAN) {
                    throw new \InvalidArgumentException(
                        "Short option '-{$shortOption}' requires a value and cannot be combined with other options"
                    );
                }

                $parsed['flags'][$longName] = true;
            }

            return $index + 1;
        }

        // Handle single short option: -f or -f value
        $shortName = $options;
        $definition = $this->findDefinitionByShortName($shortName);
        $longName = $definition?->name ?? $shortName;

        if ($definition && $definition->type === ArgumentType::BOOLEAN) {
            $parsed['flags'][$longName] = true;

            return $index + 1;
        }

        // Option that expects a value
        if ($index + 1 >= count($arguments) || str_starts_with($arguments[$index + 1], '-')) {
            if ($definition && $definition->required) {
                throw new \InvalidArgumentException("Option '-{$shortName}' requires a value");
            }
            // Optional option without value - treat as flag
            $parsed['flags'][$longName] = true;

            return $index + 1;
        }

        $parsed['options'][$longName] = $this->parseValue($longName, $arguments[$index + 1]);

        return $index + 2;
    }

    /**
     * Parse positional argument
     */
    private function parsePositionalArgument(string $value, array &$parsed): void
    {
        $parsed['positional'][] = $value;

        // Try to match with positional argument definitions
        $positionalIndex = count($parsed['positional']) - 1;
        $positionalDefs = array_values(array_filter(
            $this->definitions,
            fn ($def) => $def->type !== ArgumentType::BOOLEAN
        ));

        if (isset($positionalDefs[$positionalIndex])) {
            $definition = $positionalDefs[$positionalIndex];
            $parsed['arguments'][$definition->name] = $this->parseValue($definition->name, $value);
        }
    }

    /**
     * Parse and validate a value according to its definition
     */
    private function parseValue(string $name, string $value): mixed
    {
        $definition = $this->findDefinitionByName($name);

        if (! $definition) {
            return $value; // Unknown option, keep as string
        }

        // Validate against allowed values
        if (! empty($definition->allowedValues) && ! in_array($value, $definition->allowedValues, true)) {
            throw new \InvalidArgumentException(
                "Invalid value '{$value}' for '{$name}'. Allowed values: " .
                implode(', ', $definition->allowedValues)
            );
        }

        return $this->castToType($value, $definition->type);
    }

    /**
     * Cast value to appropriate type
     */
    private function castToType(mixed $value, ArgumentType $type): mixed
    {
        return match($type) {
            ArgumentType::STRING => (string) $value,
            ArgumentType::INTEGER => $this->parseInteger($value),
            ArgumentType::FLOAT => $this->parseFloat($value),
            ArgumentType::BOOLEAN => $this->parseBoolean($value),
            ArgumentType::ARRAY => $this->parseArray($value),
            ArgumentType::EMAIL => $this->validateEmail($value),
            ArgumentType::URL => $this->validateUrl($value),
        };
    }

    private function parseInteger(mixed $value): int
    {
        if (! is_numeric($value)) {
            throw new \InvalidArgumentException("Value '{$value}' is not a valid integer");
        }

        return (int) $value;
    }

    private function parseFloat(mixed $value): float
    {
        if (! is_numeric($value)) {
            throw new \InvalidArgumentException("Value '{$value}' is not a valid number");
        }

        return (float) $value;
    }

    private function parseBoolean(mixed $value): bool
    {
        if (is_bool($value)) {
            return $value;
        }

        $lowered = strtolower((string) $value);

        return in_array($lowered, ['true', '1', 'yes', 'on'], true);
    }

    private function parseArray(mixed $value): array
    {
        if (is_array($value)) {
            return $value;
        }

        $items = array_map('trim', explode(',', (string) $value));

        return array_filter($items, fn ($item) => $item !== '');
    }

    private function validateEmail(mixed $value): string
    {
        $email = (string) $value;
        if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException("'{$email}' is not a valid email address");
        }

        return $email;
    }

    private function validateUrl(mixed $value): string
    {
        $url = (string) $value;
        if (! filter_var($url, FILTER_VALIDATE_URL)) {
            throw new \InvalidArgumentException("'{$url}' is not a valid URL");
        }

        return $url;
    }

    /**
     * Find argument definition by long name
     */
    private function findDefinitionByName(string $name): ?ArgumentDefinition
    {
        return $this->definitions[$name] ?? null;
    }

    /**
     * Find argument definition by short name
     */
    private function findDefinitionByShortName(string $shortName): ?ArgumentDefinition
    {
        foreach ($this->definitions as $definition) {
            if ($definition->shortName === $shortName) {
                return $definition;
            }
        }

        return null;
    }

    /**
     * Get all argument definitions
     *
     * @return array<string, ArgumentDefinition>
     */
    public function getDefinitions(): array
    {
        return $this->definitions;
    }

    /**
     * Create parser with fluent interface
     */
    public static function create(): ArgumentParserBuilder
    {
        return new ArgumentParserBuilder();
    }
}
