<?php

declare(strict_types=1);

namespace App\Framework\Console;

use App\Framework\Console\Performance\ConsolePerformanceCollector;
use App\Framework\Console\Progress\ProgressMiddleware;
use App\Framework\DI\Container;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\Discovery\ValueObjects\DiscoveredAttribute;
use App\Framework\Exception\Core\ConsoleErrorCode;
use App\Framework\Exception\FrameworkException;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
use ReflectionException;
use ReflectionMethod;
use Throwable;

/**
 * Registry für Console Commands mit Discovery Integration
 */
final readonly class CommandRegistry
{
    private CommandList $commandList;

    /** @var array<string, DiscoveredAttribute> */
    private array $discoveredAttributes;

    private CommandParameterResolver $parameterResolver;

    private ?ConsolePerformanceCollector $performanceCollector;

    private ProgressMiddleware $progressMiddleware;

    public function __construct(
        private Container $container,
        DiscoveryRegistry $discoveryRegistry
    ) {
        $this->parameterResolver = new CommandParameterResolver(new MethodSignatureAnalyzer());
        $this->performanceCollector = $this->createPerformanceCollector();
        $this->progressMiddleware = new ProgressMiddleware();
        $this->discoverCommands($discoveryRegistry);
    }

    public function getCommandList(): CommandList
    {
        return $this->commandList;
    }

    public function getDiscoveredAttribute(string $commandName): DiscoveredAttribute
    {
        if (! isset($this->discoveredAttributes[$commandName])) {
            throw FrameworkException::create(
                ConsoleErrorCode::COMMAND_NOT_FOUND,
                "No discovered attribute found for command '{$commandName}'"
            )->withData(['command_name' => $commandName]);
        }

        return $this->discoveredAttributes[$commandName];
    }

    /**
     * @param array<int, string> $arguments
     */
    public function executeCommand(string $commandName, array $arguments, ConsoleOutputInterface $output): ExitCode
    {
        if ($this->performanceCollector) {
            return $this->performanceCollector->measureCommand(
                $commandName,
                fn () => $this->doExecuteCommand($commandName, $arguments, $output),
                $arguments,
                $output
            );
        }

        return $this->doExecuteCommand($commandName, $arguments, $output);
    }

    private function doExecuteCommand(string $commandName, array $arguments, ConsoleOutputInterface $output): ExitCode
    {
        $command = $this->commandList->get($commandName);
        $discoveredAttribute = $this->getDiscoveredAttribute($commandName);

        try {
            // Get execution context from discovered attribute
            $className = $discoveredAttribute->className->getFullyQualified();
            $methodName = $discoveredAttribute->methodName?->toString() ?? '__invoke';

            // Performance tracking: Container resolution
            $containerStart = microtime(true);
            $instance = $this->container->get($className);
            $containerDuration = (microtime(true) - $containerStart) * 1000;

            if ($this->performanceCollector) {
                $this->performanceCollector->recordContainerResolutionTime($commandName, $className, $containerDuration);
            }

            // Validate command structure
            if (! is_object($instance) || ! method_exists($instance, $methodName)) {
                throw FrameworkException::create(
                    ConsoleErrorCode::INVALID_COMMAND_STRUCTURE,
                    "Invalid command configuration for '{$commandName}'"
                )->withData([
                    'command_name' => $commandName,
                    'class_name' => $className,
                    'method_name' => $methodName,
                ]);
            }

            // Get reflection method for parameter resolution
            $reflectionMethod = new ReflectionMethod($className, $methodName);

            // Performance tracking: Validation time
            $validationStart = microtime(true);
            $this->parameterResolver->validateMethodSignature($reflectionMethod);
            $validationDuration = (microtime(true) - $validationStart) * 1000;

            if ($this->performanceCollector) {
                $this->performanceCollector->recordValidationTime($commandName, $validationDuration);
            }

            // Execute command with automatic parameter resolution
            $result = $this->executeCommandWithReflection($instance, $reflectionMethod, $arguments, $output);

            return $this->normalizeCommandResult($result);

        } catch (Throwable $e) {
            throw FrameworkException::create(
                ConsoleErrorCode::EXECUTION_FAILED,
                "Failed to execute command '{$commandName}': {$e->getMessage()}"
            )->withData([
                'command_name' => $commandName,
                'error_message' => $e->getMessage(),
                'error_type' => get_class($e),
            ]);
        }
    }

    private function createPerformanceCollector(): ?ConsolePerformanceCollector
    {
        try {
            if ($this->container->has(PerformanceCollectorInterface::class)) {
                $performanceCollector = $this->container->get(PerformanceCollectorInterface::class);

                return new ConsolePerformanceCollector($performanceCollector);
            }
        } catch (Throwable $e) {
            // Performance monitoring is optional - don't fail if unavailable
        }

        return null;
    }

    private function discoverCommands(DiscoveryRegistry $discoveryRegistry): void
    {
        $commands = [];
        $discoveredAttributes = [];

        /** @var DiscoveredAttribute $discoveredAttribute */
        foreach ($discoveryRegistry->attributes->get(ConsoleCommand::class) as $discoveredAttribute) {
            try {
                $registeredCommand = $this->registerDiscoveredCommand($discoveredAttribute);
                $commands[] = $registeredCommand;
                $discoveredAttributes[$registeredCommand->name] = $discoveredAttribute;

            } catch (Throwable $e) {
                // Log warning but continue with other commands
                if ($this->container->has(Logger::class)) {
                    $logger = $this->container->get(Logger::class);
                    $logger->warning('Failed to register command', LogContext::withData([
                        'class_name' => $discoveredAttribute->className->getFullyQualified(),
                        'error' => $e->getMessage(),
                        'error_class' => get_class($e),
                        'component' => 'CommandRegistry',
                    ]));
                }
            }
        }

        $this->commandList = new CommandList(...$commands);
        $this->discoveredAttributes = $discoveredAttributes;
    }

    private function registerDiscoveredCommand(DiscoveredAttribute $discoveredAttribute): ConsoleCommand
    {
        // Validate discovered attribute
        if (! $discoveredAttribute->className) {
            throw new \InvalidArgumentException('Missing class name in discovered attribute');
        }

        $className = $discoveredAttribute->className->getFullyQualified();

        // Validate class exists
        if (! class_exists($className)) {
            throw new \InvalidArgumentException("Command class {$className} does not exist");
        }

        /** @var ConsoleCommand $command */
        $command = $discoveredAttribute->createAttributeInstance();

        // Validate command name
        if (empty(trim($command->name))) {
            throw new \InvalidArgumentException("Command name cannot be empty for class {$className}");
        }

        $methodName = $discoveredAttribute->methodName?->toString() ?? '__invoke';

        try {
            // Validate method exists and is callable
            $reflection = new ReflectionMethod($className, $methodName);
            if (! $reflection->isPublic()) {
                throw new \InvalidArgumentException("Command method {$className}::{$methodName} must be public");
            }

            // Validate that instance can be created from container
            $this->container->get($className);

            return $command;

        } catch (ReflectionException $e) {
            throw new \InvalidArgumentException("Invalid command method {$className}::{$methodName}: {$e->getMessage()}");
        } catch (Throwable $e) {
            throw new \RuntimeException("Failed to instantiate command class {$className}: {$e->getMessage()}");
        }
    }

    private function normalizeCommandResult($result): ExitCode
    {
        if ($result instanceof ExitCode) {
            return $result;
        }

        if (is_int($result)) {
            try {
                return ExitCode::from($result);
            } catch (\ValueError) {
                return ExitCode::GENERAL_ERROR;
            }
        }

        if (is_bool($result)) {
            return $result ? ExitCode::SUCCESS : ExitCode::GENERAL_ERROR;
        }

        return ExitCode::SUCCESS;
    }

    /**
     * Execute command with automatic parameter resolution
     */
    private function executeCommandWithReflection(
        object $instance,
        ReflectionMethod $method,
        array $arguments,
        ConsoleOutputInterface $output
    ): ExitCode {
        try {
            // Create the actual command execution callback
            $commandExecutor = function (ConsoleInput $input, ConsoleOutputInterface $progressAwareOutput) use ($instance, $method, $arguments) {
                // Performance tracking: Parameter resolution
                $parameterStart = microtime(true);

                // Resolve parameters with framework injection support
                $resolvedParams = $this->parameterResolver->resolveParameters(
                    $method,
                    $arguments,
                    $input,           // Inject ConsoleInput
                    $progressAwareOutput  // Inject ConsoleOutput
                );

                $parameterDuration = (microtime(true) - $parameterStart) * 1000;

                if ($this->performanceCollector) {
                    $this->performanceCollector->recordParameterResolutionTime($method->getDeclaringClass()->getName(), $parameterDuration);
                }

                $result = $method->invokeArgs($instance, $resolvedParams);

                return $this->normalizeCommandResult($result);
            };

            // Wrap execution with ProgressMiddleware
            $input = new ConsoleInput($arguments, $output);

            return $this->progressMiddleware->handle($input, $output, $commandExecutor, $method, $instance);

        } catch (\ArgumentCountError $e) {
            throw new \InvalidArgumentException(
                "Invalid number of arguments for command. " . $e->getMessage()
            );
        } catch (\TypeError $e) {
            throw new \InvalidArgumentException(
                "Type error in command execution: " . $e->getMessage()
            );
        }
    }


    /**
     * Generate help for a specific command
     */
    public function generateCommandHelp(string $commandName): string
    {
        $discoveredAttribute = $this->getDiscoveredAttribute($commandName);
        $className = $discoveredAttribute->className->getFullyQualified();
        $methodName = $discoveredAttribute->methodName?->toString() ?? '__invoke';

        try {
            $reflectionMethod = new ReflectionMethod($className, $methodName);

            return $this->parameterResolver->generateMethodHelp($reflectionMethod, $commandName);

        } catch (ReflectionException $e) {
            return "Command: {$commandName}\nError generating help: {$e->getMessage()}";
        }
    }
}
