<?php

declare(strict_types=1);

namespace App\Framework\Console;

use App\Framework\Core\ValueObjects\MethodName;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use ReflectionNamedType;
use ReflectionParameter;

/**
 * Validates console command signatures and implementations
 */
final readonly class CommandValidator
{
    /**
     * Validate a command object and its method signature
     */
    public function validateCommand(object $command, ?MethodName $methodName = null): ValidationResult
    {
        $errors = [];
        $warnings = [];

        try {
            // If no method name provided, find the method with ConsoleCommand attribute
            if ($methodName === null) {
                $methodName = $this->findConsoleCommandMethod($command);
                if ($methodName === null) {
                    $errors[] = [
                        'type' => 'no_command_method',
                        'message' => 'No method with #[ConsoleCommand] attribute found',
                        'severity' => 'critical',
                        'suggestion' => 'Add #[ConsoleCommand] attribute to a public method',
                    ];

                    return new ValidationResult(
                        valid: false,
                        errors: $errors,
                        warnings: $warnings,
                        commandName: $this->extractCommandName($command)
                    );
                }
            }

            $reflection = new ReflectionMethod($command, $methodName->toString());

            // Validate method exists and is callable
            $classValidation = $this->validateClassStructure($command);
            $errors = array_merge($errors, $classValidation['errors']);
            $warnings = array_merge($warnings, $classValidation['warnings']);

            // Validate method signature
            $signatureValidation = $this->validateMethodSignature($reflection);
            $errors = array_merge($errors, $signatureValidation['errors']);
            $warnings = array_merge($warnings, $signatureValidation['warnings']);

            // Validate parameters
            $parameterValidation = $this->validateParameters($reflection);
            $errors = array_merge($errors, $parameterValidation['errors']);
            $warnings = array_merge($warnings, $parameterValidation['warnings']);

            // Validate return type
            $returnValidation = $this->validateReturnType($reflection);
            $errors = array_merge($errors, $returnValidation['errors']);
            $warnings = array_merge($warnings, $returnValidation['warnings']);

            // Validate ConsoleCommand attribute
            $attributeValidation = $this->validateCommandAttribute($command, $methodName);
            $errors = array_merge($errors, $attributeValidation['errors']);
            $warnings = array_merge($warnings, $attributeValidation['warnings']);

        } catch (ReflectionException $e) {
            $methodNameStr = $methodName ? $methodName->toString() : 'unknown';
            $errors[] = [
                'type' => 'reflection_error',
                'message' => "Could not reflect method '{$methodNameStr}': " . $e->getMessage(),
                'severity' => 'critical',
                'suggestion' => "Ensure the method '{$methodNameStr}' exists and is accessible",
            ];
        }

        return new ValidationResult(
            valid: empty($errors),
            errors: $errors,
            warnings: $warnings,
            commandName: $this->extractCommandName($command, $methodName)
        );
    }

    /**
     * Find the method with ConsoleCommand attribute
     */
    private function findConsoleCommandMethod(object $command): ?MethodName
    {
        try {
            $reflection = new ReflectionClass($command);
            $methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC);

            foreach ($methods as $method) {
                $attributes = $method->getAttributes(ConsoleCommand::class);
                if (! empty($attributes)) {
                    return MethodName::create($method->getName());
                }
            }
        } catch (ReflectionException) {
            // If reflection fails, return null
        }

        return null;
    }

    /**
     * Validate class structure
     */
    private function validateClassStructure(object $command): array
    {
        $errors = [];
        $warnings = [];

        $reflection = new ReflectionClass($command);

        // Check if class is final
        if (! $reflection->isFinal()) {
            $warnings[] = [
                'type' => 'class_structure',
                'message' => 'Command class should be final for better performance and type safety',
                'severity' => 'medium',
                'suggestion' => 'Add "final" keyword to the class declaration',
            ];
        }

        // Check if class is readonly
        if (PHP_VERSION_ID >= 80200 && ! $reflection->isReadOnly()) {
            $warnings[] = [
                'type' => 'class_structure',
                'message' => 'Command class should be readonly for immutability',
                'severity' => 'low',
                'suggestion' => 'Add "readonly" keyword to the class declaration',
            ];
        }

        // Check for proper constructor dependency injection
        $constructor = $reflection->getConstructor();
        if ($constructor && ! $constructor->isPublic()) {
            $errors[] = [
                'type' => 'constructor',
                'message' => 'Constructor must be public for dependency injection',
                'severity' => 'critical',
                'suggestion' => 'Change constructor visibility to public',
            ];
        }

        return ['errors' => $errors, 'warnings' => $warnings];
    }

    /**
     * Validate method signature structure
     */
    private function validateMethodSignature(ReflectionMethod $method): array
    {
        $errors = [];
        $warnings = [];

        // Check method visibility
        if (! $method->isPublic()) {
            $errors[] = [
                'type' => 'method_visibility',
                'message' => 'Command method must be public',
                'severity' => 'critical',
                'suggestion' => 'Change method visibility to public',
            ];
        }

        // Check if method is static
        if ($method->isStatic()) {
            $errors[] = [
                'type' => 'method_static',
                'message' => 'Command method should not be static',
                'severity' => 'critical',
                'suggestion' => 'Remove static keyword from method declaration',
            ];
        }

        // Validate parameter count
        $parameters = $method->getParameters();
        $requiredParams = array_filter($parameters, fn ($p) => ! $p->isOptional());

        if (count($requiredParams) < 2) {
            $errors[] = [
                'type' => 'parameter_count',
                'message' => 'Command method must have at least ConsoleInput and ConsoleOutput parameters',
                'severity' => 'critical',
                'suggestion' => 'Add ConsoleInput and ConsoleOutput parameters to method signature',
            ];
        }

        return ['errors' => $errors, 'warnings' => $warnings];
    }

    /**
     * Validate method parameters
     */
    private function validateParameters(ReflectionMethod $method): array
    {
        $errors = [];
        $warnings = [];
        $parameters = $method->getParameters();

        if (count($parameters) < 2) {
            return ['errors' => $errors, 'warnings' => $warnings];
        }

        // Validate first parameter (should be ConsoleInput)
        $firstParam = $parameters[0];
        $inputValidation = $this->validateConsoleInputParameter($firstParam);
        $errors = array_merge($errors, $inputValidation['errors']);
        $warnings = array_merge($warnings, $inputValidation['warnings']);

        // Validate second parameter (should be ConsoleOutput)
        $secondParam = $parameters[1];
        $outputValidation = $this->validateConsoleOutputParameter($secondParam);
        $errors = array_merge($errors, $outputValidation['errors']);
        $warnings = array_merge($warnings, $outputValidation['warnings']);

        // Validate additional parameters (business logic parameters)
        if (count($parameters) > 2) {
            for ($i = 2; $i < count($parameters); $i++) {
                $param = $parameters[$i];
                $businessValidation = $this->validateBusinessParameter($param, $i);
                $errors = array_merge($errors, $businessValidation['errors']);
                $warnings = array_merge($warnings, $businessValidation['warnings']);
            }
        }

        return ['errors' => $errors, 'warnings' => $warnings];
    }

    /**
     * Validate ConsoleInput parameter
     */
    private function validateConsoleInputParameter(ReflectionParameter $parameter): array
    {
        $errors = [];
        $warnings = [];

        $type = $parameter->getType();

        if (! $type instanceof ReflectionNamedType) {
            $errors[] = [
                'type' => 'parameter_type',
                'message' => 'First parameter must be typed as ConsoleInput',
                'severity' => 'critical',
                'suggestion' => 'Add type hint: ConsoleInput $input',
            ];

            return ['errors' => $errors, 'warnings' => $warnings];
        }

        $expectedTypes = [
            'App\Framework\Console\ConsoleInput',
            'App\Framework\Console\Input\ConsoleInput',
            ConsoleInput::class,
        ];

        if (! in_array($type->getName(), $expectedTypes)) {
            $errors[] = [
                'type' => 'parameter_type',
                'message' => "First parameter must be ConsoleInput, got {$type->getName()}",
                'severity' => 'critical',
                'suggestion' => 'Change first parameter type to ConsoleInput',
            ];
        }

        // Check parameter name convention
        $expectedNames = ['input', 'consoleInput'];
        if (! in_array($parameter->getName(), $expectedNames)) {
            $warnings[] = [
                'type' => 'parameter_naming',
                'message' => "ConsoleInput parameter should be named 'input' by convention",
                'severity' => 'low',
                'suggestion' => "Rename parameter to '\$input'",
            ];
        }

        return ['errors' => $errors, 'warnings' => $warnings];
    }

    /**
     * Validate ConsoleOutput parameter
     */
    private function validateConsoleOutputParameter(ReflectionParameter $parameter): array
    {
        $errors = [];
        $warnings = [];

        $type = $parameter->getType();

        if (! $type instanceof ReflectionNamedType) {
            $errors[] = [
                'type' => 'parameter_type',
                'message' => 'Second parameter must be typed as ConsoleOutput',
                'severity' => 'critical',
                'suggestion' => 'Add type hint: ConsoleOutput $output',
            ];

            return ['errors' => $errors, 'warnings' => $warnings];
        }

        $expectedTypes = [
            'App\Framework\Console\ConsoleOutput',
            'App\Framework\Console\Output\ConsoleOutput',
            'App\Framework\Console\ConsoleOutputInterface',
            ConsoleOutput::class,
        ];

        if (! in_array($type->getName(), $expectedTypes)) {
            $errors[] = [
                'type' => 'parameter_type',
                'message' => "Second parameter must be ConsoleOutput, got {$type->getName()}",
                'severity' => 'critical',
                'suggestion' => 'Change second parameter type to ConsoleOutput',
            ];
        }

        // Check parameter name convention
        $expectedNames = ['output', 'consoleOutput'];
        if (! in_array($parameter->getName(), $expectedNames)) {
            $warnings[] = [
                'type' => 'parameter_naming',
                'message' => "ConsoleOutput parameter should be named 'output' by convention",
                'severity' => 'low',
                'suggestion' => "Rename parameter to '\$output'",
            ];
        }

        return ['errors' => $errors, 'warnings' => $warnings];
    }

    /**
     * Validate business logic parameters
     */
    private function validateBusinessParameter(ReflectionParameter $parameter, int $position): array
    {
        $errors = [];
        $warnings = [];

        $type = $parameter->getType();

        // Check if parameter has type hint
        if (! $type) {
            $warnings[] = [
                'type' => 'parameter_type',
                'message' => "Parameter '{$parameter->getName()}' should have a type hint",
                'severity' => 'medium',
                'suggestion' => 'Add appropriate type hint for better type safety',
            ];
        }

        // Check for complex types that might be hard to handle via CLI
        if ($type instanceof ReflectionNamedType) {
            $typeName = $type->getName();

            // Check for problematic types
            $problematicTypes = ['resource', 'object', 'callable'];
            if (in_array($typeName, $problematicTypes)) {
                $errors[] = [
                    'type' => 'parameter_type',
                    'message' => "Parameter type '{$typeName}' cannot be provided via command line",
                    'severity' => 'critical',
                    'suggestion' => 'Use primitive types (string, int, float, bool) or simple value objects',
                ];
            }

            // Check for array without documentation
            if ($typeName === 'array') {
                $warnings[] = [
                    'type' => 'parameter_type',
                    'message' => "Array parameter '{$parameter->getName()}' should have documentation about expected format",
                    'severity' => 'medium',
                    'suggestion' => 'Add PHPDoc comment describing the array structure',
                ];
            }
        }

        // Check if parameter has default value for non-required parameters
        if (! $parameter->isOptional() && $position > 2) {
            $warnings[] = [
                'type' => 'parameter_optional',
                'message' => "Consider making parameter '{$parameter->getName()}' optional with a default value",
                'severity' => 'low',
                'suggestion' => 'Add default value or make parameter optional for better UX',
            ];
        }

        return ['errors' => $errors, 'warnings' => $warnings];
    }

    /**
     * Validate return type
     */
    private function validateReturnType(ReflectionMethod $method): array
    {
        $errors = [];
        $warnings = [];

        $returnType = $method->getReturnType();

        if (! $returnType) {
            $errors[] = [
                'type' => 'return_type',
                'message' => 'Command method must have ExitCode return type',
                'severity' => 'critical',
                'suggestion' => 'Add ": ExitCode" return type to method signature',
            ];

            return ['errors' => $errors, 'warnings' => $warnings];
        }

        if ($returnType instanceof ReflectionNamedType) {
            $expectedTypes = [
                'App\Framework\Console\ExitCode',
                ExitCode::class,
                'int', // for backward compatibility
            ];

            if (! in_array($returnType->getName(), $expectedTypes)) {
                $errors[] = [
                    'type' => 'return_type',
                    'message' => "Return type must be ExitCode, got {$returnType->getName()}",
                    'severity' => 'critical',
                    'suggestion' => 'Change return type to ExitCode',
                ];
            }

            // Warn about using int instead of ExitCode
            if ($returnType->getName() === 'int') {
                $warnings[] = [
                    'type' => 'return_type',
                    'message' => 'Consider using ExitCode enum instead of int for better type safety',
                    'severity' => 'medium',
                    'suggestion' => 'Change return type from int to ExitCode',
                ];
            }
        }

        return ['errors' => $errors, 'warnings' => $warnings];
    }

    /**
     * Validate ConsoleCommand attribute
     */
    private function validateCommandAttribute(object $command, MethodName $methodName): array
    {
        $errors = [];
        $warnings = [];

        try {
            $method = new ReflectionMethod($command, $methodName->toString());
            $attributes = $method->getAttributes(ConsoleCommand::class);

            if (empty($attributes)) {
                $errors[] = [
                    'type' => 'missing_attribute',
                    'message' => 'Command method must have #[ConsoleCommand] attribute',
                    'severity' => 'critical',
                    'suggestion' => 'Add #[ConsoleCommand(name: "command-name", description: "Command description")] attribute',
                ];

                return ['errors' => $errors, 'warnings' => $warnings];
            }

            $consoleCommandAttribute = $attributes[0];

            // Validate attribute arguments
            $args = $consoleCommandAttribute->getArguments();

            if (empty($args)) {
                $errors[] = [
                    'type' => 'attribute_arguments',
                    'message' => 'ConsoleCommand attribute must have name and description',
                    'severity' => 'critical',
                    'suggestion' => 'Add name and description: #[ConsoleCommand("command-name", "Description")]',
                ];
            } else {
                // Check command name
                $commandName = $args[0] ?? $args['name'] ?? null;
                if (! $commandName || ! is_string($commandName)) {
                    $errors[] = [
                        'type' => 'command_name',
                        'message' => 'Command name must be a non-empty string',
                        'severity' => 'critical',
                        'suggestion' => 'Provide valid command name as first argument',
                    ];
                } else {
                    // Validate command name format
                    if (! preg_match('/^[a-z0-9]+(?:[:-][a-z0-9]+)*$/', $commandName)) {
                        $warnings[] = [
                            'type' => 'command_naming',
                            'message' => 'Command name should follow kebab-case convention (e.g., "user:create")',
                            'severity' => 'medium',
                            'suggestion' => 'Use lowercase letters, numbers, and hyphens/colons only',
                        ];
                    }
                }

                // Check description
                $description = $args[1] ?? $args['description'] ?? null;
                if (! $description || ! is_string($description)) {
                    $warnings[] = [
                        'type' => 'command_description',
                        'message' => 'Command should have a descriptive description',
                        'severity' => 'medium',
                        'suggestion' => 'Add meaningful description as second argument',
                    ];
                } elseif (strlen($description) < 10) {
                    $warnings[] = [
                        'type' => 'command_description',
                        'message' => 'Command description should be more descriptive',
                        'severity' => 'low',
                        'suggestion' => 'Provide more detailed description (at least 10 characters)',
                    ];
                }
            }

        } catch (ReflectionException $e) {
            $errors[] = [
                'type' => 'reflection_error',
                'message' => 'Could not validate ConsoleCommand attribute: ' . $e->getMessage(),
                'severity' => 'critical',
                'suggestion' => 'Check if method exists and is properly defined',
            ];
        }

        return ['errors' => $errors, 'warnings' => $warnings];
    }

    /**
     * Extract command name from object
     */
    private function extractCommandName(object $command, ?MethodName $methodName = null): ?string
    {
        try {
            // If no method name provided, find the method with ConsoleCommand attribute
            if ($methodName === null) {
                $methodName = $this->findConsoleCommandMethod($command);
            }

            if ($methodName !== null) {
                $method = new ReflectionMethod($command, $methodName->toString());
                $attributes = $method->getAttributes(ConsoleCommand::class);

                if (! empty($attributes)) {
                    $args = $attributes[0]->getArguments();

                    return $args[0] ?? $args['name'] ?? null;
                }
            }
        } catch (ReflectionException) {
            // Ignore
        }

        return get_class($command);
    }

    /**
     * Validate multiple commands at once
     */
    public function validateCommands(array $commands): array
    {
        $results = [];

        foreach ($commands as $command) {
            $result = $this->validateCommand($command);
            $results[] = $result;
        }

        return $results;
    }

    /**
     * Quick validation check (only critical errors)
     */
    public function isValidCommand(object $command, ?MethodName $methodName = null): bool
    {
        $result = $this->validateCommand($command, $methodName);

        return $result->isValid();
    }
}
