<?php
declare(strict_types=1);

namespace App\Framework\Discovery\Visitors;

use AllowDynamicProperties;
use App\Framework\Cache\Cache;
use App\Framework\Core\AttributeMapper;
use App\Framework\Discovery\FileVisitor;
use Attribute;
use Override;
use ReflectionClass;
use ReflectionMethod;
use ReturnTypeWillChange;
use SensitiveParameter;
use Throwable;

final class AttributeDiscoveryVisitor implements FileVisitor
{
    private array $attributeResults = [];

    /** @var array<string, AttributeMapper> */
    private array $mapperMap;

    /**
     * @param array<AttributeMapper|string> $attributeMappers Array von Mapper-Instanzen oder Klassennamen
     */
    /** @var array<string> PHP-interne Attribute die ignoriert werden sollen */
    private array $ignoredAttributes = [
        'Attribute',
        Attribute::class,
        'Override',
        Override::class,
        'AllowDynamicProperties',
        AllowDynamicProperties::class,
        'ReturnTypeWillChange',
        ReturnTypeWillChange::class,
        'SensitiveParameter',
        SensitiveParameter::class,
    ];

    public function __construct(array $attributeMappers = [])
    {
        $this->mapperMap = [];
        foreach ($attributeMappers as $mapper) {
            if (is_string($mapper)) {
                $mapper = new $mapper();
            }
            $this->mapperMap[$mapper->getAttributeClass()] = $mapper;
        }
    }

    public function onScanStart(): void
    {
        $this->attributeResults = [];
    }

    public function onIncrementalScanStart(): void
    {
        // Bei inkrementellem Scan behalten wir bestehende Attribute
        // Einzelne Klassen werden bei visitClass() aktualisiert
    }

    public function onIncrementalScanComplete(): void
    {
        // Eventuell nachsortieren oder optimieren
        $this->optimizeResults();
    }

    public function visitClass(string $className, string $filePath): void
    {
        if (!class_exists($className)) {
            return;
        }

        try {
            $reflection = new ReflectionClass($className);

            // Entferne alte Einträge für diese Klasse (wichtig für inkrementelle Scans)
            $this->removeAttributesForClass($className);

            // Klassen-Attribute verarbeiten
            $this->processElementAttributes($reflection);

            // Methoden-Attribute verarbeiten
            foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
                $this->processElementAttributes($method);
            }
        } catch (Throwable $e) {
            error_log("Fehler beim Verarbeiten der Attribute für {$className}: " . $e->getMessage());
        }
    }

    /**
     * Verarbeitet die Attribute eines Elements (Klasse oder Methode)
     */
    private function processElementAttributes(ReflectionClass|ReflectionMethod $element): void
    {
        foreach ($element->getAttributes() as $attribute) {

            $attrName = $attribute->getName();

            // Ignoriere PHP-interne Attribute
            if ($this->shouldIgnoreAttribute($attrName)) {
                continue;
            }

            $attributeInstance = $attribute->newInstance();

            // Standard-Daten für alle Attribute sammeln
            $baseData = [
                'attribute_class' => $attrName,
                'target_type' => $element instanceof ReflectionClass ? 'class' : 'method',
                'class' => $element instanceof ReflectionClass ? $element->getName() : $element->getDeclaringClass()->getName(),
                'file' => $element instanceof ReflectionClass ? $element->getFileName() : $element->getDeclaringClass()->getFileName(),
            ];

            if ($element instanceof ReflectionMethod) {
                $baseData['method'] = $element->getName();
                $baseData['parameters'] = $this->extractMethodParameters($element);
            }

            // Attribute-spezifische Daten hinzufügen
            $baseData['attribute_data'] = $this->extractAttributeData($attributeInstance);

            // Wenn ein Mapper existiert, verwende ihn
            if (isset($this->mapperMap[$attrName])) {
                $mapped = $this->mapperMap[$attrName]->map($element, $attributeInstance);

                if ($mapped !== null) {
                    // Füge Parameter-Informationen hinzu bei Methoden (falls noch nicht vom Mapper gemacht)
                    if ($element instanceof ReflectionMethod && !isset($mapped['parameters'])) {
                        $mapped['parameters'] = $this->extractMethodParameters($element);
                    }

                    $this->attributeResults[$attrName][] = $mapped;
                }
            } else {
                // Keine Mapper vorhanden - verwende Standard-Daten
                #debug("Attribute ohne Mapper gefunden: $attrName");
                $this->attributeResults[$attrName][] = $baseData;
            }
        }
    }

    /**
     * Extrahiert Parameter-Informationen aus einer Methode
     */
    private function extractMethodParameters(ReflectionMethod $method): array
    {
        $params = [];
        foreach ($method->getParameters() as $param) {
            $paramData = [
                'name' => $param->getName(),
                'type' => $param->getType()?->getName(),
                'isBuiltin' => $param->getType()?->isBuiltin() ?? false,
                'hasDefault' => $param->isDefaultValueAvailable(),
                'default' => $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null,
                'attributes' => array_map(
                    fn ($a) => $a->getName(),
                    $param->getAttributes()
                ),
            ];
            $params[] = $paramData;
        }

        return $params;
    }

    /**
     * Prüft, ob ein Attribut ignoriert werden soll
     */
    private function shouldIgnoreAttribute(string $attributeName): bool
    {
        // Direkte Prüfung
        if (in_array($attributeName, $this->ignoredAttributes, true)) {
            return true;
        }

        // Prüfung ohne Namespace (falls nur Klassenname verwendet wird)
        $shortName = substr($attributeName, strrpos($attributeName, '\\') + 1);
        if (in_array($shortName, $this->ignoredAttributes, true)) {
            return true;
        }

        return false;
    }

    /**
     * Extrahiert verfügbare Daten aus einem Attribute-Objekt
     */
    private function extractAttributeData(object $attributeInstance): array
    {
        $data = [];

        try {
            $reflection = new ReflectionClass($attributeInstance);

            // Öffentliche Eigenschaften
            foreach ($reflection->getProperties() as $property) {
                if ($property->isPublic()) {
                    $data[$property->getName()] = $property->getValue($attributeInstance);
                }
            }

            // Constructor-Parameter (für readonly properties)
            $constructor = $reflection->getConstructor();
            if ($constructor) {
                foreach ($constructor->getParameters() as $param) {
                    $name = $param->getName();
                    if (!isset($data[$name])) {
                        // Versuche über magische Methoden oder Getter
                        $getter = 'get' . ucfirst($name);
                        if (method_exists($attributeInstance, $getter)) {
                            $data[$name] = $attributeInstance->$getter();
                        } elseif (property_exists($attributeInstance, $name)) {
                            $property = $reflection->getProperty($name);
                            if ($property->isPublic() ||
                                ($property->isProtected() || $property->isPrivate()) &&
                                version_compare(PHP_VERSION, '8.1.0') >= 0) {
                                try {
                                    $data[$name] = $property->getValue($attributeInstance);
                                } catch (Throwable) {
                                    // Ignore protected/private properties we can't access
                                }
                            }
                        }
                    }
                }
            }
        } catch (Throwable $e) {
            error_log("Fehler beim Extrahieren der Attribute-Daten: " . $e->getMessage());
        }

        return $data;
    }

    /**
     * Entfernt alle Attribute für eine bestimmte Klasse (für inkrementelle Scans)
     */
    private function removeAttributesForClass(string $className): void
    {
        foreach ($this->attributeResults as $attributeClass => &$results) {
            $results = array_filter($results, function($result) use ($className) {
                return ($result['class'] ?? '') !== $className;
            });
            // Array-Indizes neu numerieren
            $results = array_values($results);
        }
    }

    /**
     * Optimiert die Ergebnisse für bessere Performance
     */
    private function optimizeResults(): void
    {
        foreach ($this->attributeResults as &$results) {
            // Sortiere nach Klasse und Methode für konsistente Reihenfolge
            usort($results, function($a, $b) {
                $classCompare = ($a['class'] ?? '') <=> ($b['class'] ?? '');
                if ($classCompare !== 0) {
                    return $classCompare;
                }
                return ($a['method'] ?? '') <=> ($b['method'] ?? '');
            });
        }
    }

    public function onScanComplete(): void
    {
        $this->optimizeResults();
    }

    public function loadFromCache(Cache $cache): void
    {
        $cacheItem = $cache->get($this->getCacheKey());
        if ($cacheItem->isHit) {
            $this->attributeResults = $cacheItem->value ?? [];
        }
    }

    public function getCacheKey(): string
    {
        return 'attribute_discovery';
    }

    public function getCacheableData(): mixed
    {
        return $this->attributeResults;
    }

    /**
     * Gibt alle Attribute-Ergebnisse für eine bestimmte Attributklasse zurück
     */
    public function getAttributeResults(string $attributeClass): array
    {
        return $this->attributeResults[$attributeClass] ?? [];
    }

    /**
     * Gibt alle gefundenen Attribute-Ergebnisse zurück
     */
    public function getAllResults(): array
    {
        return $this->attributeResults;
    }

    /**
     * Prüft, ob Attribute einer bestimmten Klasse gefunden wurden
     */
    public function hasAttributeResults(string $attributeClass): bool
    {
        return !empty($this->attributeResults[$attributeClass]);
    }

    /**
     * Gibt die Anzahl gefundener Attribute zurück
     */
    public function getAttributeCount(string $attributeClass): int
    {
        return count($this->attributeResults[$attributeClass] ?? []);
    }
}
