<?php

declare(strict_types=1);

namespace App\Framework\Database;

use App\Framework\Database\Metadata\EntityMetadata;
use App\Framework\Database\Metadata\MetadataRegistry;
use App\Framework\Database\ValueObjects\SqlQuery;
use ReflectionClass;

final class LazyLoader
{
    /** @var array<string, array<string, array<object>>> Track entities by [entityClass][relationName] => entities[] */
    private array $relationAccessTracker = [];

    /** @var int Threshold for N+1 detection - batch load if more than this many entities access same relation */
    private const BATCH_THRESHOLD = 3;

    public function __construct(
        private readonly DatabaseManager $databaseManager,
        private readonly TypeConverter $typeConverter,
        private readonly IdentityMap $identityMap,
        private readonly MetadataRegistry $metadataRegistry,
        private readonly ?BatchRelationLoader $batchRelationLoader = null
    ) {
    }

    /**
     * Erstellt ein Lazy Ghost Object mit dynamischen Relationen
     */
    public function createLazyGhost(EntityMetadata $metadata, mixed $id): object
    {
        // Prüfe zuerst Identity Map
        if ($this->identityMap->has($metadata->entityClass, $id)) {
            return $this->identityMap->get($metadata->entityClass, $id);
        }

        $reflection = $metadata->reflection;

        // Für readonly Klassen müssen wir newLazyProxy verwenden
        if ($reflection->isReadOnly()) {
            return $this->createLazyProxy($metadata, $id);
        }

        // Erstelle Lazy Ghost mit dynamischen Relationen
        $ghost = $reflection->newLazyGhost(
            initializer: function (object $object) use ($metadata, $id): void {
                $this->initializeGhostWithDynamicRelations($object, $metadata, $id);
            }
        );

        // In Identity Map speichern
        $this->identityMap->set($metadata->entityClass, $id, $ghost);

        return $ghost;
    }

    /**
     * Erstellt einen Lazy Proxy für readonly Klassen
     */
    private function createLazyProxy(EntityMetadata $metadata, mixed $id): object
    {
        $reflection = $metadata->reflection;

        $proxy = $reflection->newLazyProxy(
            factory: function () use ($metadata, $id): object {
                return $this->loadEntityWithDynamicRelations($metadata, $id);
            }
        );

        // In Identity Map speichern
        $this->identityMap->set($metadata->entityClass, $id, $proxy);

        return $proxy;
    }

    /**
     * Lädt eine Entity mit dynamischen Relationen
     */
    private function loadEntityWithDynamicRelations(EntityMetadata $metadata, mixed $id): object
    {
        // Basis-Entity laden
        $entity = $this->loadEntity($metadata, $id);

        // Dynamische Relationen laden
        $this->loadDynamicRelations($entity, $metadata, $id);

        return $entity;
    }

    /**
     * Initialisiert das Ghost Object mit dynamischen Relationen
     */
    private function initializeGhostWithDynamicRelations(object $ghost, EntityMetadata $metadata, mixed $id): void
    {
        // Basis-Daten laden
        $this->initializeGhost($ghost, $metadata, $id);

        // Dynamische Relationen laden
        $this->loadDynamicRelations($ghost, $metadata, $id);
    }

    /**
     * Lädt alle definierten Relationen dynamisch basierend auf Attributen
     */
    private function loadDynamicRelations(object $entity, EntityMetadata $metadata, mixed $id): void
    {
        foreach ($metadata->properties as $propertyName => $propertyMetadata) {
            if (! $propertyMetadata->isRelation) {
                continue;
            }

            $this->loadRelation($entity, $propertyMetadata, $id);
        }
    }

    /**
     * Lädt eine spezifische Relation basierend auf dem Relation-Typ
     * Includes N+1 detection and auto-batching
     */
    private function loadRelation(object $entity, $propertyMetadata, mixed $localKeyValue): void
    {
        try {
            $reflection = new ReflectionClass($entity);
            $property = $reflection->getProperty($propertyMetadata->name);

            // Relation nur laden wenn noch nicht initialisiert
            if (! $property->isInitialized($entity)) {
                // Track relation access for N+1 detection
                $this->trackRelationAccess($entity, $propertyMetadata->name);

                // Check if we should use batch loading
                if ($this->shouldUseBatchLoading($entity::class, $propertyMetadata->name)) {
                    $this->triggerBatchLoading($entity::class, $propertyMetadata->name);
                } else {
                    // Load individual relation
                    $relationValue = $this->loadRelationByType($propertyMetadata, $entity, $localKeyValue);
                    $property->setValue($entity, $relationValue);
                }
            }
        } catch (\ReflectionException $e) {
            // Property nicht vorhanden oder nicht zugänglich
        }
    }

    /**
     * Lädt Relation basierend auf dem Typ
     */
    private function loadRelationByType($propertyMetadata, object $entity, mixed $localKeyValue): mixed
    {
        switch ($propertyMetadata->relationType) {
            case 'belongsTo':
                return $this->loadBelongsToRelation($propertyMetadata, $entity);

            case 'hasMany':
                return $this->loadHasManyRelation($propertyMetadata, $localKeyValue);

            case 'one-to-one':
                return $this->loadOneToOneRelation($propertyMetadata, $localKeyValue);

            default:
                return null;
        }
    }

    /**
     * Lädt eine belongsTo Relation
     */
    private function loadBelongsToRelation($propertyMetadata, object $entity): ?object
    {
        // Für belongsTo müssen wir den Foreign Key Wert aus der Entity selbst holen
        $entityMetadata = $this->metadataRegistry->getMetadata($entity::class);

        // Lade zuerst die Entity-Daten aus der DB, um den Foreign Key zu bekommen
        $entityId = $this->getEntityId($entity, $entityMetadata);

        $query = ValueObjects\SqlQuery::create(
            "SELECT * FROM {$entityMetadata->tableName} WHERE {$entityMetadata->idColumn} = ?",
            [$entityId]
        );
        $result = $this->databaseManager->getConnection()->query($query);
        $data = $result->fetch();

        if (! $data) {
            return null;
        }

        // Hole den Foreign Key Wert
        $foreignKeyValue = $data[$propertyMetadata->relationForeignKey] ?? null;

        if ($foreignKeyValue === null) {
            return null;
        }

        // Lade die verknüpfte Entity
        $targetMetadata = $this->metadataRegistry->getMetadata($propertyMetadata->relationTargetClass);

        if ($this->identityMap->has($propertyMetadata->relationTargetClass, $foreignKeyValue)) {
            return $this->identityMap->get($propertyMetadata->relationTargetClass, $foreignKeyValue);
        }

        $query = "SELECT * FROM {$targetMetadata->tableName} WHERE {$targetMetadata->idColumn} = ?";

        $result = $this->databaseManager->getConnection()->query(
            SqlQuery::create($query, [$foreignKeyValue])
        );
        $targetData = $result->fetch();

        if (! $targetData) {
            return null;
        }

        $targetEntity = $this->hydrateEntity($targetMetadata, $targetData);
        $this->identityMap->set($propertyMetadata->relationTargetClass, $foreignKeyValue, $targetEntity);

        return $targetEntity;
    }

    /**
     * Lädt eine hasMany Relation
     */
    private function loadHasManyRelation($propertyMetadata, mixed $localKeyValue): array
    {
        $relatedData = $this->queryRelatedData(
            $propertyMetadata->relationTargetClass,
            $propertyMetadata->relationForeignKey,
            $localKeyValue
        );

        return $this->hydrateRelatedEntities(
            $propertyMetadata->relationTargetClass,
            $relatedData
        );
    }

    /**
     * Lädt eine one-to-one Relation
     */
    private function loadOneToOneRelation($propertyMetadata, mixed $localKeyValue): ?object
    {
        $relatedData = $this->queryRelatedData(
            $propertyMetadata->relationTargetClass,
            $propertyMetadata->relationForeignKey,
            $localKeyValue
        );

        if (empty($relatedData)) {
            return null;
        }

        // Für one-to-one nur das erste Ergebnis zurückgeben
        $entities = $this->hydrateRelatedEntities(
            $propertyMetadata->relationTargetClass,
            [$relatedData[0]] // Nur das erste Element
        );

        return $entities[0] ?? null;
    }

    /**
     * Ermittelt die ID einer Entity
     */
    private function getEntityId(object $entity, EntityMetadata $metadata): mixed
    {
        try {
            $idProperty = $metadata->reflection->getProperty($metadata->idProperty);

            return $idProperty->getValue($entity);
        } catch (\ReflectionException) {
            // Fallback: versuche über Constructor-Parameter
            return null;
        }
    }

    /**
     * Lädt verwandte Daten aus der Datenbank
     */
    private function queryRelatedData(string $targetClass, string $foreignKey, mixed $localKeyValue): array
    {
        $targetMetadata = $this->metadataRegistry->getMetadata($targetClass);

        $query = ValueObjects\SqlQuery::create(
            "SELECT * FROM {$targetMetadata->tableName} WHERE {$foreignKey} = ?",
            [$localKeyValue]
        );
        $result = $this->databaseManager->getConnection()->query($query);

        return $result->fetchAll();
    }

    /**
     * Hydratisiert verwandte Entities
     */
    private function hydrateRelatedEntities(string $targetClass, array $dataRows): array
    {
        $entities = [];
        $targetMetadata = $this->metadataRegistry->getMetadata($targetClass);

        foreach ($dataRows as $data) {
            // Prüfe Identity Map
            $idValue = $data[$targetMetadata->idColumn];

            if ($this->identityMap->has($targetClass, $idValue)) {
                $entities[] = $this->identityMap->get($targetClass, $idValue);
            } else {
                $entity = $this->hydrateEntity($targetMetadata, $data);
                $this->identityMap->set($targetClass, $idValue, $entity);
                $entities[] = $entity;
            }
        }

        return $entities;
    }

    /**
     * Hydratisiert eine einzelne Entity
     */
    private function hydrateEntity(EntityMetadata $metadata, array $data): object
    {
        $constructor = $metadata->reflection->getConstructor();
        if (! $constructor) {
            throw new \RuntimeException("Entity {$metadata->entityClass} must have a constructor");
        }

        $constructorParams = [];
        foreach ($constructor->getParameters() as $param) {
            $paramName = $param->getName();
            $propertyMetadata = $metadata->getProperty($paramName);

            if (! $propertyMetadata) {
                throw new \RuntimeException("No metadata found for parameter {$paramName}");
            }

            $columnName = $propertyMetadata->columnName;
            $value = $data[$columnName] ?? null;

            // Typ-Konvertierung
            $convertedValue = $this->typeConverter->convertValue($value, $propertyMetadata);
            $constructorParams[] = $convertedValue;
        }

        return new $metadata->entityClass(...$constructorParams);
    }

    /**
     * Lädt eine Entity aus der Datenbank
     */
    private function loadEntity(EntityMetadata $metadata, mixed $id): object
    {
        $query = ValueObjects\SqlQuery::create(
            "SELECT * FROM {$metadata->tableName} WHERE {$metadata->idColumn} = ?",
            [$id]
        );
        $result = $this->databaseManager->getConnection()->query($query);
        $data = $result->fetch();

        if (! $data) {
            throw Exception\EntityNotFoundException::byId($metadata->entityClass, $id);
        }

        return $this->hydrateEntity($metadata, $data);
    }

    /**
     * Initialisiert das Ghost Object (nur für non-readonly Klassen)
     */
    private function initializeGhost(object $ghost, EntityMetadata $metadata, mixed $id): void
    {
        // Daten aus Datenbank laden
        $query = ValueObjects\SqlQuery::create(
            "SELECT * FROM {$metadata->tableName} WHERE {$metadata->idColumn} = ?",
            [$id]
        );
        $result = $this->databaseManager->getConnection()->query($query);
        $data = $result->fetch();

        if (! $data) {
            throw Exception\EntityNotFoundException::byId($metadata->entityClass, $id);
        }

        // Ghost Object Properties setzen
        foreach ($metadata->properties as $propertyName => $propertyMetadata) {
            $columnName = $propertyMetadata->columnName;
            $value = $data[$columnName] ?? null;

            // Typ-Konvertierung
            $convertedValue = $this->typeConverter->convertValue($value, $propertyMetadata);

            // Property setzen
            try {
                $property = $metadata->reflection->getProperty($propertyName);
                $property->setValue($ghost, $convertedValue);
            } catch (\ReflectionException) {
                // Property nicht zugänglich oder existiert nicht
            }
        }
    }

    /**
     * Prüft ob ein Object ein uninitializiertes Lazy Object ist
     */
    public function isLazyGhost(object $object): bool
    {
        $reflection = new ReflectionClass($object);

        return $reflection->isUninitializedLazyObject($object);
    }

    /**
     * Forciert die Initialisierung eines Lazy Objects
     */
    public function initializeLazyObject(object $object): void
    {
        $reflection = new ReflectionClass($object);
        if ($reflection->isUninitializedLazyObject($object)) {
            $reflection->initializeLazyObject($object);
        }
    }

    /**
     * Track relation access for N+1 detection
     */
    private function trackRelationAccess(object $entity, string $relationName): void
    {
        $entityClass = $entity::class;

        if (! isset($this->relationAccessTracker[$entityClass])) {
            $this->relationAccessTracker[$entityClass] = [];
        }

        if (! isset($this->relationAccessTracker[$entityClass][$relationName])) {
            $this->relationAccessTracker[$entityClass][$relationName] = [];
        }

        // Add entity to tracking if not already present
        $entityId = spl_object_hash($entity);
        $found = false;
        foreach ($this->relationAccessTracker[$entityClass][$relationName] as $trackedEntity) {
            if (spl_object_hash($trackedEntity) === $entityId) {
                $found = true;

                break;
            }
        }

        if (! $found) {
            $this->relationAccessTracker[$entityClass][$relationName][] = $entity;
        }
    }

    /**
     * Check if we should use batch loading based on access patterns
     */
    private function shouldUseBatchLoading(string $entityClass, string $relationName): bool
    {
        if (! $this->batchRelationLoader) {
            return false; // No batch loader available
        }

        return isset($this->relationAccessTracker[$entityClass][$relationName]) &&
               count($this->relationAccessTracker[$entityClass][$relationName]) >= self::BATCH_THRESHOLD;
    }

    /**
     * Trigger batch loading for all entities accessing the same relation
     */
    private function triggerBatchLoading(string $entityClass, string $relationName): void
    {
        if (! $this->batchRelationLoader) {
            return;
        }

        if (! isset($this->relationAccessTracker[$entityClass][$relationName])) {
            return;
        }

        $entities = $this->relationAccessTracker[$entityClass][$relationName];

        // Use BatchRelationLoader to preload relations for all tracked entities
        $this->batchRelationLoader->preloadRelation($entities, $relationName);

        // Clear the tracking for this relation - it's been batch loaded
        unset($this->relationAccessTracker[$entityClass][$relationName]);
    }

    /**
     * Clear relation access tracking - useful for testing or memory management
     */
    public function clearRelationTracking(): void
    {
        $this->relationAccessTracker = [];
    }

    /**
     * Get statistics about relation access patterns - useful for debugging N+1 issues
     */
    public function getRelationAccessStats(): array
    {
        $stats = [];
        foreach ($this->relationAccessTracker as $entityClass => $relations) {
            $stats[$entityClass] = [];
            foreach ($relations as $relationName => $entities) {
                $stats[$entityClass][$relationName] = [
                    'entity_count' => count($entities),
                    'would_trigger_batch' => count($entities) >= self::BATCH_THRESHOLD,
                ];
            }
        }

        return $stats;
    }
}
