<?php

declare(strict_types=1);

namespace App\Framework\Console;

use ReflectionMethod;
use ReflectionNamedType;
use ReflectionParameter;
use ReflectionUnionType;

/**
 * Analyzes method signatures to automatically generate argument definitions
 */
final readonly class MethodSignatureAnalyzer
{
    /**
     * Generate ArgumentDefinitions from method parameters
     * Excludes framework-provided parameters (ConsoleInput, ConsoleOutput)
     *
     * @return ArgumentDefinition[]
     */
    public function generateArgumentDefinitions(ReflectionMethod $method): array
    {
        $definitions = [];

        foreach ($method->getParameters() as $param) {
            // Skip framework-provided parameters
            if ($this->isFrameworkProvidedParameter($param)) {
                continue;
            }

            $definition = $this->createDefinitionFromParameter($param);
            $definitions[$param->getName()] = $definition;
        }

        return $definitions;
    }

    /**
     * Check if parameter is framework-provided (ConsoleInput, ConsoleOutput)
     */
    private function isFrameworkProvidedParameter(ReflectionParameter $param): bool
    {
        $type = $param->getType();

        if (! ($type instanceof ReflectionNamedType)) {
            return false;
        }

        $typeName = $type->getName();

        $frameworkTypes = [
            'App\\Framework\\Console\\ConsoleInput',
            'App\\Framework\\Console\\ConsoleInputInterface',
            'App\\Framework\\Console\\ConsoleOutputInterface',
            'App\\Framework\\Console\\ConsoleOutput',
            'ConsoleInput',
            'ConsoleInputInterface',
            'ConsoleOutputInterface',
            'ConsoleOutput',
        ];

        return in_array($typeName, $frameworkTypes, true);
    }

    /**
     * Create ArgumentDefinition from ReflectionParameter
     */
    private function createDefinitionFromParameter(ReflectionParameter $param): ArgumentDefinition
    {
        $type = $this->mapPhpTypeToArgumentType($param->getType());
        $required = ! $param->isDefaultValueAvailable();
        $default = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null;

        // Extract information from Parameter attribute if present
        $parameterAttribute = $this->extractParameterAttribute($param);

        $description = $parameterAttribute?->description ?: $this->extractDescriptionFromDocComment($param);
        $shortName = $parameterAttribute?->shortName ?: (strlen($param->getName()) === 1 ? $param->getName() : null);

        // Handle Enums - extract allowed values from enum cases
        $allowedValues = $this->extractEnumValues($param->getType());

        return new ArgumentDefinition(
            name: $param->getName(),
            type: $type,
            required: $required,
            default: $default,
            description: $description,
            shortName: $shortName,
            allowedValues: $allowedValues
        );
    }

    /**
     * Extract Parameter attribute from parameter
     */
    private function extractParameterAttribute(ReflectionParameter $param): ?Parameter
    {
        $attributes = $param->getAttributes(Parameter::class);

        if (empty($attributes)) {
            return null;
        }

        return $attributes[0]->newInstance();
    }

    /**
     * Extract enum values if parameter type is an enum
     */
    private function extractEnumValues(?\ReflectionType $type): array
    {
        if (! $type instanceof ReflectionNamedType) {
            return [];
        }

        $typeName = $type->getName();

        if (! enum_exists($typeName)) {
            return [];
        }

        // Get all enum cases
        $enumValues = [];
        foreach ($typeName::cases() as $case) {
            if ($case instanceof \BackedEnum) {
                $enumValues[] = $case->value;
            } else {
                $enumValues[] = $case->name;
            }
        }

        return $enumValues;
    }

    /**
     * Map PHP type to ArgumentType
     */
    private function mapPhpTypeToArgumentType(?\ReflectionType $type): ArgumentType
    {
        if (! $type) {
            return ArgumentType::STRING; // Default fallback
        }

        if ($type instanceof ReflectionUnionType) {
            // Special case: ?bool (nullable bool) = flag
            if ($this->isNullableBool($type)) {
                return ArgumentType::BOOLEAN;
            }

            // For union types, try to find the most specific type
            foreach ($type->getTypes() as $unionType) {
                if ($unionType instanceof ReflectionNamedType && ! $unionType->isBuiltin()) {
                    continue;
                }

                return $this->mapNamedTypeToArgumentType($unionType);
            }

            return ArgumentType::STRING;
        }

        if ($type instanceof ReflectionNamedType) {
            return $this->mapNamedTypeToArgumentType($type);
        }

        return ArgumentType::STRING;
    }

    /**
     * Check if type is nullable bool (?bool)
     */
    private function isNullableBool(ReflectionUnionType $type): bool
    {
        $types = $type->getTypes();

        if (count($types) !== 2) {
            return false;
        }

        $hasNull = false;
        $hasBool = false;

        foreach ($types as $unionType) {
            if (!($unionType instanceof ReflectionNamedType)) {
                return false;
            }

            $name = strtolower($unionType->getName());
            if ($name === 'null') {
                $hasNull = true;
            } elseif ($name === 'bool' || $name === 'boolean') {
                $hasBool = true;
            }
        }

        return $hasNull && $hasBool;
    }

    /**
     * Map ReflectionNamedType to ArgumentType
     */
    private function mapNamedTypeToArgumentType(ReflectionNamedType $type): ArgumentType
    {
        $typeName = $type->getName();

        // Handle enums
        if (enum_exists($typeName)) {
            return ArgumentType::STRING; // Enums are represented as strings
        }

        return match (strtolower($typeName)) {
            'string' => ArgumentType::STRING,
            'int', 'integer' => ArgumentType::INTEGER,
            'float', 'double' => ArgumentType::FLOAT,
            'bool', 'boolean' => ArgumentType::BOOLEAN,
            'array' => ArgumentType::ARRAY,
            default => $this->mapClassTypeToArgumentType($typeName)
        };
    }

    /**
     * Map class types to ArgumentType
     */
    private function mapClassTypeToArgumentType(string $className): ArgumentType
    {
        // Check for framework value objects
        return match (true) {
            str_contains(strtolower($className), 'email') => ArgumentType::EMAIL,
            str_contains(strtolower($className), 'url') => ArgumentType::URL,
            default => ArgumentType::STRING
        };
    }

    /**
     * Extract parameter description from method docblock
     */
    private function extractDescriptionFromDocComment(ReflectionParameter $param): string
    {
        $method = $param->getDeclaringFunction();
        $docComment = $method->getDocComment();

        if (! $docComment) {
            return $this->generateDefaultDescription($param);
        }

        $paramName = $param->getName();

        // Look for @param annotations
        if (preg_match('/@param\s+[^\s]+\s+\$' . preg_quote($paramName) . '\s+(.+?)(?=\n|$)/i', $docComment, $matches)) {
            return trim($matches[1]);
        }

        return $this->generateDefaultDescription($param);
    }

    /**
     * Generate default description based on parameter name and type
     */
    private function generateDefaultDescription(ReflectionParameter $param): string
    {
        $name = $param->getName();
        $type = $param->getType();

        // Convert camelCase to readable format
        $readable = preg_replace('/([a-z])([A-Z])/', '$1 $2', $name);
        $readable = ucfirst(strtolower($readable));

        if ($type instanceof ReflectionNamedType) {
            $typeName = $type->getName();

            return match (strtolower($typeName)) {
                'bool', 'boolean' => "Enable {$readable}",
                'int', 'integer' => "{$readable} value",
                'float', 'double' => "{$readable} amount",
                'array' => "{$readable} list",
                default => $readable
            };
        }

        return $readable;
    }

    /**
     * Analyze method to determine argument order and types
     */
    public function analyzeMethodStructure(ReflectionMethod $method): array
    {
        $structure = [
            'positional' => [],
            'options' => [],
            'flags' => [],
        ];

        foreach ($method->getParameters() as $index => $param) {
            $type = $this->mapPhpTypeToArgumentType($param->getType());
            $paramInfo = [
                'name' => $param->getName(),
                'type' => $type,
                'position' => $index,
                'required' => ! $param->isDefaultValueAvailable(),
                'default' => $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null,
            ];

            if ($type === ArgumentType::BOOLEAN) {
                $structure['flags'][] = $paramInfo;
            } elseif ($param->isDefaultValueAvailable()) {
                $structure['options'][] = $paramInfo;
            } else {
                $structure['positional'][] = $paramInfo;
            }
        }

        return $structure;
    }

    /**
     * Check if method signature is compatible with console command
     */
    public function isValidCommandMethod(ReflectionMethod $method): bool
    {
        // Must be public
        if (! $method->isPublic()) {
            return false;
        }

        // Check return type
        $returnType = $method->getReturnType();
        if ($returnType instanceof ReflectionNamedType) {
            $returnTypeName = $returnType->getName();
            if (! in_array($returnTypeName, ['int', ExitCode::class], true)) {
                return false;
            }
        }

        // Check parameters are compatible
        foreach ($method->getParameters() as $param) {
            if (! $this->isValidParameterType($param)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Check if parameter type is supported
     */
    private function isValidParameterType(ReflectionParameter $param): bool
    {
        $type = $param->getType();

        if (! $type) {
            return true; // Mixed/untyped is allowed
        }

        if ($type instanceof ReflectionUnionType) {
            // ?bool (nullable bool) is allowed for flags
            if ($this->isNullableBool($type)) {
                return true;
            }

            // Other union types are complex, be conservative
            return false;
        }

        if ($type instanceof ReflectionNamedType) {
            $typeName = $type->getName();

            $allowedTypes = [
                'string', 'int', 'integer', 'float', 'double', 'bool', 'boolean', 'array',
            ];

            return in_array(strtolower($typeName), $allowedTypes, true) ||
                   $this->isFrameworkValueObject($typeName) ||
                   $this->isFrameworkProvidedParameter($param);
        }

        return false;
    }

    /**
     * Check if type is a framework value object
     */
    private function isFrameworkValueObject(string $className): bool
    {
        // For now, be permissive - can be enhanced later
        return str_contains($className, 'Email') ||
               str_contains($className, 'Url') ||
               str_contains($className, 'ValueObject');
    }
}
