<?php

namespace App\Framework\DI;

use App\Framework\Reflection\ReflectionProvider;
use ReflectionMethod;
use ReflectionNamedType;

/**
 * Führt Methoden auf beliebigen Klassen mit automatischer Dependency Injection aus
 */
final readonly class MethodInvoker
{
    public function __construct(
        private Container       $container,
        private ReflectionProvider $reflectionCache
    ) {}

    /**
     * Führt eine Methode auf einer Klasse aus und löst alle Parameter automatisch auf
     */
    public function invoke(string $className, string $methodName, array $overrides = []): mixed
    {
        $instance = $this->container->get($className);
        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
    {
        $reflection = $this->reflectionCache->getClass($instance::class);

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

        $method = $this->reflectionCache->getMethod($instance::class, $methodName);

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

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

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

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

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

        $method = $this->reflectionCache->getMethod($className, $methodName);

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

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

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

    /**
     * Löst alle Parameter einer Methode auf
     */
    private function resolveMethodParameters(ReflectionMethod $method, array $overrides): array
    {
        $parameters = [];

        foreach ($method->getParameters() as $param) {
            $paramName = $param->getName();

            // Override-Werte haben Priorität
            if (array_key_exists($paramName, $overrides)) {
                $parameters[] = $overrides[$paramName];
                continue;
            }

            // Type-based injection
            $type = $param->getType();

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

                if ($this->container->has($typeName)) {
                    $parameters[] = $this->container->get($typeName);
                    continue;
                }
            }

            // Default-Werte verwenden
            if ($param->isDefaultValueAvailable()) {
                $parameters[] = $param->getDefaultValue();
                continue;
            }

            // Nullable Parameter
            if ($type && $type->allowsNull()) {
                $parameters[] = null;
                continue;
            }

            throw new \InvalidArgumentException(
                "Cannot resolve parameter '{$paramName}' for method '{$method->getName()}' on class '{$method->getDeclaringClass()->getName()}'"
            );
        }

        return $parameters;
    }
}
