<?php

declare(strict_types=1);

namespace App\Framework\DI;

use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Reflection\ReflectionProvider;
use App\Framework\Reflection\WrappedReflectionMethod;

/**
 * Führt Methoden auf beliebigen Klassen mit automatischer Dependency Injection aus
 */
final readonly class MethodInvoker
{
    private ParameterResolver $parameterResolver;

    public function __construct(
        private Container $container,
        private ReflectionProvider $reflectionProvider
    ) {
        $this->parameterResolver = new ParameterResolver($container, $reflectionProvider);
    }

    /**
     * Führt eine Methode auf einer Klasse aus und löst alle Parameter automatisch auf
     */
    public function invoke(ClassName $className, string $methodName, array $overrides = []): mixed
    {
        $instance = $this->container->get($className->getFullyQualified());

        return $this->invokeOn($instance, $methodName, $overrides);
    }

    /**
     * Führt eine Methode auf einer bereits existierenden Instanz aus
     */
    public function invokeOn(object $instance, string $methodName, array $overrides = []): mixed
    {
        $className = ClassName::fromObject($instance);
        $reflection = $this->reflectionProvider->getClass($className);

        if (! $reflection->hasMethod($methodName)) {
            throw new \InvalidArgumentException(
                "Method $methodName does not exist on class " . $instance::class
            );
        }

        // Framework WrappedReflectionMethod verwenden
        $method = $this->reflectionProvider->getMethod($className, $methodName);
        $nativeMethod = $this->reflectionProvider->getNativeMethod($className, $methodName);

        if (! $nativeMethod->isPublic()) {
            throw new \InvalidArgumentException(
                "Method $methodName on class " . $instance::class . " is not public"
            );
        }

        $parameters = $this->resolveMethodParameters($className, $methodName, $overrides);

        return $nativeMethod->invokeArgs($instance, $parameters);
    }

    /**
     * Führt eine statische Methode aus
     */
    public function invokeStatic(string $className, string $methodName, array $overrides = []): mixed
    {
        $classNameObj = ClassName::create($className);
        $reflection = $this->reflectionProvider->getClass($classNameObj);

        if (! $reflection->hasMethod($methodName)) {
            throw new \InvalidArgumentException(
                "Method '{$methodName}' does not exist on class '{$className}'"
            );
        }

        // Framework WrappedReflectionMethod verwenden
        $method = $this->reflectionProvider->getMethod($classNameObj, $methodName);
        $nativeMethod = $this->reflectionProvider->getNativeMethod($classNameObj, $methodName);

        if (! $nativeMethod->isStatic()) {
            throw new \InvalidArgumentException(
                "Method '{$methodName}' on class '{$className}' is not static"
            );
        }

        $parameters = $this->resolveMethodParameters($classNameObj, $methodName, $overrides);

        return $nativeMethod->invokeArgs(null, $parameters);
    }

    /**
     * Erstellt eine neue Instanz mit automatischer Dependency Injection
     * und optionalen Parameter-Overrides für den Constructor
     *
     * @template T of object
     * @param class-string<T> $className
     * @param array<string, mixed> $overrides Named constructor parameters
     * @return T
     */
    public function make(string $className, array $overrides = []): object
    {
        $classNameObj = ClassName::create($className);
        $reflection = $this->reflectionProvider->getClass($classNameObj);

        if (! $reflection->isInstantiable()) {
            throw new \InvalidArgumentException(
                "Class '{$className}' is not instantiable (interface, abstract class, or trait)"
            );
        }

        // Check if class has constructor
        if (! $reflection->hasMethod('__construct')) {
            return $reflection->newInstance();
        }

        // Resolve constructor parameters with overrides
        $parameters = $this->resolveMethodParameters($classNameObj, '__construct', $overrides);

        return $reflection->newInstance(...$parameters);
    }

    /**
     * Löst alle Parameter einer Methode auf
     */
    private function resolveMethodParameters(ClassName $className, string $methodName, array $overrides): array
    {
        return $this->parameterResolver->resolveMethodParameters($className, $methodName, $overrides);
    }
}
