<?php

declare(strict_types=1);

namespace App\Framework\Reflection\Builder;

use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Reflection\Collections\AttributeCollection;
use App\Framework\Reflection\Collections\MethodCollection;
use App\Framework\Reflection\Collections\ParameterCollection;
use App\Framework\Reflection\Collections\PropertyCollection;
use App\Framework\Reflection\ReflectionProvider;
use App\Framework\Reflection\WrappedReflectionClass;
use App\Framework\Reflection\WrappedReflectionMethod;

/**
 * Fluent builder for reflection operations
 *
 * This class provides a fluent API for reflection operations, making it easier
 * to chain operations and improve code readability.
 */
final class ReflectionBuilder
{
    private ClassName $className;

    private ?string $methodName = null;

    private ?string $attributeClass = null;

    private int $attributeFlags = 0;

    private ?int $methodFilter = null;

    /**
     * @param ReflectionProvider $reflectionProvider The reflection provider to use
     */
    public function __construct(
        private readonly ReflectionProvider $reflectionProvider
    ) {
    }

    /**
     * Set the class to reflect
     *
     * @param string|ClassName $className The class name to reflect
     * @return self
     */
    public function forClass(string|ClassName $className): self
    {
        $this->className = $className instanceof ClassName
            ? $className
            : ClassName::create($className);

        return $this;
    }

    /**
     * Set the method to reflect
     *
     * @param string $methodName The method name to reflect
     * @return self
     */
    public function method(string $methodName): self
    {
        $this->methodName = $methodName;

        return $this;
    }

    /**
     * Filter by attribute class
     *
     * @param string $attributeClass The attribute class to filter by
     * @param int $flags Optional flags for attribute lookup (ReflectionAttribute::IS_INSTANCEOF)
     * @return self
     */
    public function withAttribute(string $attributeClass, int $flags = 0): self
    {
        $this->attributeClass = $attributeClass;
        $this->attributeFlags = $flags;

        return $this;
    }

    /**
     * Filter methods by visibility
     *
     * @param int $filter The filter to apply (e.g., \ReflectionMethod::IS_PUBLIC)
     * @return self
     */
    public function filterMethods(int $filter): self
    {
        $this->methodFilter = $filter;

        return $this;
    }

    /**
     * Get the reflected class
     *
     * @return WrappedReflectionClass The reflected class
     * @throws \InvalidArgumentException If no class has been set
     */
    public function getClass(): WrappedReflectionClass
    {
        $this->ensureClassIsSet();

        return $this->reflectionProvider->getClass($this->className);
    }

    /**
     * Get the reflected method
     *
     * @return WrappedReflectionMethod The reflected method
     * @throws \InvalidArgumentException If no class or method has been set
     */
    public function getMethod(): WrappedReflectionMethod
    {
        $this->ensureClassIsSet();
        $this->ensureMethodIsSet();

        return $this->reflectionProvider->getMethod($this->className, $this->methodName);
    }

    /**
     * Get the class attributes
     *
     * @return AttributeCollection The class attributes
     * @throws \InvalidArgumentException If no class has been set
     */
    public function getClassAttributes(): AttributeCollection
    {
        $this->ensureClassIsSet();

        return $this->reflectionProvider->getAttributes(
            $this->className,
            $this->attributeClass
        );
    }

    /**
     * Get the method attributes
     *
     * @return AttributeCollection The method attributes
     * @throws \InvalidArgumentException If no class or method has been set
     */
    public function getMethodAttributes(): AttributeCollection
    {
        $this->ensureClassIsSet();
        $this->ensureMethodIsSet();

        return $this->reflectionProvider->getMethodAttributes(
            $this->className,
            $this->methodName,
            $this->attributeClass
        );
    }

    /**
     * Get the method parameters
     *
     * @return ParameterCollection The method parameters
     * @throws \InvalidArgumentException If no class or method has been set
     */
    public function getMethodParameters(): ParameterCollection
    {
        $this->ensureClassIsSet();
        $this->ensureMethodIsSet();

        return $this->reflectionProvider->getMethodParameters(
            $this->className,
            $this->methodName
        );
    }

    /**
     * Get the class methods
     *
     * @return MethodCollection The class methods
     * @throws \InvalidArgumentException If no class has been set
     */
    public function getMethods(): MethodCollection
    {
        $this->ensureClassIsSet();

        return $this->reflectionProvider->getMethods(
            $this->className,
            $this->methodFilter
        );
    }

    /**
     * Get the class properties
     *
     * @return PropertyCollection The class properties
     * @throws \InvalidArgumentException If no class has been set
     */
    public function getProperties(): PropertyCollection
    {
        $this->ensureClassIsSet();

        return $this->reflectionProvider->getProperties($this->className);
    }

    /**
     * Check if the class has a specific attribute
     *
     * @param string|null $attributeClass The attribute class to check for (overrides any previously set attribute)
     * @return bool True if the class has the attribute, false otherwise
     * @throws \InvalidArgumentException If no class has been set
     */
    public function hasAttribute(?string $attributeClass = null): bool
    {
        $this->ensureClassIsSet();

        $attrClass = $attributeClass ?? $this->attributeClass;

        if ($attrClass === null) {
            throw new \InvalidArgumentException(
                'No attribute class specified. Use withAttribute() or provide an attribute class.'
            );
        }

        return $this->reflectionProvider->hasAttribute($this->className, $attrClass);
    }

    /**
     * Check if the class is instantiable
     *
     * @return bool True if the class is instantiable, false otherwise
     * @throws \InvalidArgumentException If no class has been set
     */
    public function isInstantiable(): bool
    {
        $this->ensureClassIsSet();

        return $this->reflectionProvider->isInstantiable($this->className);
    }

    /**
     * Check if the class implements a specific interface
     *
     * @param string $interfaceName The interface name to check for
     * @return bool True if the class implements the interface, false otherwise
     * @throws \InvalidArgumentException If no class has been set
     */
    public function implementsInterface(string $interfaceName): bool
    {
        $this->ensureClassIsSet();

        return $this->reflectionProvider->implementsInterface($this->className, $interfaceName);
    }

    /**
     * Create a new instance of the class
     *
     * @param mixed ...$args Constructor arguments
     * @return object The new instance
     * @throws \InvalidArgumentException If no class has been set
     * @throws \RuntimeException If the class is not instantiable
     */
    public function newInstance(mixed ...$args): object
    {
        return $this->getClass()->newInstance(...$args);
    }

    /**
     * Ensure that a class has been set
     *
     * @throws \InvalidArgumentException If no class has been set
     */
    private function ensureClassIsSet(): void
    {
        if (! isset($this->className)) {
            throw new \InvalidArgumentException(
                'No class has been set. Use forClass() to set a class.'
            );
        }
    }

    /**
     * Ensure that a method has been set
     *
     * @throws \InvalidArgumentException If no method has been set
     */
    private function ensureMethodIsSet(): void
    {
        if ($this->methodName === null) {
            throw new \InvalidArgumentException(
                'No method has been set. Use method() to set a method.'
            );
        }
    }
}
