<?php

declare(strict_types=1);

namespace App\Framework\Reflection\BatchOperations;

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

/**
 * Batch processor for efficient reflection operations
 *
 * This class provides methods for performing reflection operations on multiple
 * classes or methods in batch, which can be more efficient than performing
 * individual operations.
 */
final readonly class ReflectionBatchProcessor
{
    /**
     * @param ReflectionProvider $reflectionProvider The reflection provider to use
     */
    public function __construct(
        private ReflectionProvider $reflectionProvider
    ) {
    }

    /**
     * Get attributes for multiple classes in batch
     * @param array<string> $classNames
     * @return array<string, \App\Framework\Reflection\Collections\AttributeCollection>
     */
    public function getAttributesForClasses(array $classNames, ?string $attributeClass = null): array
    {
        $results = [];

        foreach ($classNames as $className) {
            $classNameObj = ClassName::create($className);
            $results[$className] = $this->reflectionProvider
                ->getAttributes($classNameObj, $attributeClass);
        }

        return $results;
    }

    /**
     * Get methods with specific attribute across multiple classes
     * @param array<string> $classNames
     * @return array<string, \App\Framework\Reflection\Collections\MethodCollection>
     */
    public function getMethodsWithAttribute(array $classNames, string $attributeClass): array
    {
        $results = [];

        foreach ($classNames as $className) {
            $classNameObj = ClassName::create($className);
            $methods = $this->reflectionProvider->getMethods($classNameObj);

            $methodsWithAttribute = $methods->getWithAttribute($attributeClass);
            if (! $methodsWithAttribute->isEmpty()) {
                $results[$className] = $methodsWithAttribute;
            }
        }

        return $results;
    }

    /**
     * Check multiple classes for instantiability
     * @param array<string> $classNames
     * @return array<string, bool>
     */
    public function checkInstantiability(array $classNames): array
    {
        $results = [];

        foreach ($classNames as $className) {
            $classNameObj = ClassName::create($className);
            $results[$className] = $this->reflectionProvider->isInstantiable($classNameObj);
        }

        return $results;
    }

    /**
     * Bulk check interface implementation
     * @param array<string> $classNames
     * @return array<string, bool>
     */
    public function checkInterfaceImplementation(array $classNames, string $interfaceName): array
    {
        $results = [];

        foreach ($classNames as $className) {
            $classNameObj = ClassName::create($className);
            $results[$className] = $this->reflectionProvider
                ->implementsInterface($classNameObj, $interfaceName);
        }

        return $results;
    }

    /**
     * Get properties for multiple classes in batch
     *
     * @param array<string> $classNames The class names to get properties for
     * @return array<string, \App\Framework\Reflection\Collections\PropertyCollection>
     */
    public function getPropertiesForClasses(array $classNames): array
    {
        $results = [];

        foreach ($classNames as $className) {
            $classNameObj = ClassName::create($className);
            $results[$className] = $this->reflectionProvider->getProperties($classNameObj);
        }

        return $results;
    }

    /**
     * Get methods for multiple classes in batch
     *
     * @param array<string> $classNames The class names to get methods for
     * @param int|null $filter Optional filter for methods (e.g., \ReflectionMethod::IS_PUBLIC)
     * @return array<string, \App\Framework\Reflection\Collections\MethodCollection>
     */
    public function getMethodsForClasses(array $classNames, ?int $filter = null): array
    {
        $results = [];

        foreach ($classNames as $className) {
            $classNameObj = ClassName::create($className);
            $results[$className] = $this->reflectionProvider->getMethods($classNameObj, $filter);
        }

        return $results;
    }

    /**
     * Get parameters for multiple methods across classes
     *
     * @param array<string, string|array<string>> $methodMap Map of class names to method names
     *                                                      (string for single method or array for multiple methods)
     * @return array<string, array<string, \App\Framework\Reflection\Collections\ParameterCollection>>
     */
    public function getMethodParametersForMethods(array $methodMap): array
    {
        $results = [];

        foreach ($methodMap as $className => $methodNames) {
            $classNameObj = ClassName::create($className);
            $results[$className] = [];

            // Handle both single method name and array of method names
            $methods = is_array($methodNames) ? $methodNames : [$methodNames];

            foreach ($methods as $methodName) {
                $results[$className][$methodName] = $this->reflectionProvider
                    ->getMethodParameters($classNameObj, $methodName);
            }
        }

        return $results;
    }

    /**
     * Get attributes for multiple methods across classes
     *
     * @param array<string, string|array<string>> $methodMap Map of class names to method names
     *                                                      (string for single method or array for multiple methods)
     * @param string|null $attributeClass Optional attribute class to filter by
     * @return array<string, array<string, \App\Framework\Reflection\Collections\AttributeCollection>>
     */
    public function getMethodAttributesForMethods(array $methodMap, ?string $attributeClass = null): array
    {
        $results = [];

        foreach ($methodMap as $className => $methodNames) {
            $classNameObj = ClassName::create($className);
            $results[$className] = [];

            // Handle both single method name and array of method names
            $methods = is_array($methodNames) ? $methodNames : [$methodNames];

            foreach ($methods as $methodName) {
                $results[$className][$methodName] = $this->reflectionProvider
                    ->getMethodAttributes($classNameObj, $methodName, $attributeClass);
            }
        }

        return $results;
    }

    /**
     * Create instances of multiple classes in batch
     *
     * @param array<string, array<mixed>> $constructorArgs Map of class names to constructor arguments
     * @return array<string, object|null> Map of class names to instances (null for classes that couldn't be instantiated)
     */
    public function createInstances(array $constructorArgs): array
    {
        $results = [];

        foreach ($constructorArgs as $className => $args) {
            $classNameObj = ClassName::create($className);

            try {
                // Check if class is instantiable first
                if (! $this->reflectionProvider->isInstantiable($classNameObj)) {
                    $results[$className] = null;

                    continue;
                }

                // Get the class and create a new instance
                $class = $this->reflectionProvider->getClass($classNameObj);
                $results[$className] = $class->newInstance(...$args);
            } catch (\Throwable $e) {
                // If instantiation fails, store null
                $results[$className] = null;
            }
        }

        return $results;
    }

    /**
     * Filter classes by the presence of a specific attribute
     *
     * @param array<string> $classNames The class names to filter
     * @param string $attributeClass The attribute class to filter by
     * @param bool $hasAttribute Whether to include classes that have the attribute (true) or don't have it (false)
     * @return array<string> Filtered class names
     */
    public function filterClassesByAttribute(array $classNames, string $attributeClass, bool $hasAttribute = true): array
    {
        $results = [];

        foreach ($classNames as $className) {
            $classNameObj = ClassName::create($className);
            $hasAttr = $this->reflectionProvider->hasAttribute($classNameObj, $attributeClass);

            if ($hasAttr === $hasAttribute) {
                $results[] = $className;
            }
        }

        return $results;
    }

    /**
     * Filter classes by the implementation of a specific interface
     *
     * @param array<string> $classNames The class names to filter
     * @param string $interfaceName The interface name to filter by
     * @param bool $implementsInterface Whether to include classes that implement the interface (true) or don't implement it (false)
     * @return array<string> Filtered class names
     */
    public function filterClassesByInterface(array $classNames, string $interfaceName, bool $implementsInterface = true): array
    {
        $results = [];

        foreach ($classNames as $className) {
            $classNameObj = ClassName::create($className);
            $implements = $this->reflectionProvider->implementsInterface($classNameObj, $interfaceName);

            if ($implements === $implementsInterface) {
                $results[] = $className;
            }
        }

        return $results;
    }

    /**
     * Get enum cases for multiple enum classes (PHP 8.1+)
     *
     * @param array<string> $classNames The enum class names to get cases for
     * @return array<string, array<string, object>> Map of enum class names to their cases (case name => case value)
     */
    public function getEnumCasesForClasses(array $classNames): array
    {
        // Only available in PHP 8.1+
        if (PHP_VERSION_ID < 80100) {
            return array_fill_keys($classNames, []);
        }

        $results = [];

        foreach ($classNames as $className) {
            $classNameObj = ClassName::create($className);
            $class = $this->reflectionProvider->getClass($classNameObj);

            // Skip non-enum classes
            if (! method_exists($class->getNativeClass(), 'isEnum') || ! $class->getNativeClass()->isEnum()) {
                $results[$className] = [];

                continue;
            }

            // Get enum cases
            $cases = [];
            $reflectionCases = $class->getNativeClass()->getCases();

            foreach ($reflectionCases as $case) {
                $caseName = $case->getName();
                /** @var class-string $classNameString */
                $classNameString = $className;
                $cases[$caseName] = $classNameString::$caseName;
            }

            $results[$className] = $cases;
        }

        return $results;
    }

    /**
     * Filter classes by whether they are enums (PHP 8.1+)
     *
     * @param array<string> $classNames The class names to filter
     * @param bool $isEnum Whether to include classes that are enums (true) or aren't enums (false)
     * @return array<string> Filtered class names
     */
    public function filterEnumClasses(array $classNames, bool $isEnum = true): array
    {
        // Only available in PHP 8.1+
        if (PHP_VERSION_ID < 80100) {
            return $isEnum ? [] : $classNames;
        }

        $results = [];

        foreach ($classNames as $className) {
            $classNameObj = ClassName::create($className);
            $class = $this->reflectionProvider->getClass($classNameObj);

            $classIsEnum = method_exists($class->getNativeClass(), 'isEnum') &&
                           $class->getNativeClass()->isEnum();

            if ($classIsEnum === $isEnum) {
                $results[] = $className;
            }
        }

        return $results;
    }
}
