<?php

declare(strict_types=1);

namespace App\Framework\Database;

use App\Framework\Database\Exception\DatabaseException;
use App\Framework\Database\Metadata\EntityMetadata;
use App\Framework\Database\Metadata\PropertyMetadata;

final readonly class Hydrator implements HydratorInterface
{
    public function __construct(
        private TypeConverter $typeConverter,
        private ?EntityLoaderInterface $entityLoader = null,
    ) {
    }

    public function hydrate(EntityMetadata $metadata, array $data): object
    {
        try {
            $constructor = $metadata->reflection->getConstructor();

            if (! $constructor) {
                throw new DatabaseException("Entity {$metadata->entityClass} must have a constructor");
            }

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

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

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

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

                $constructorParams[] = $value;
            }

            $entity = new $metadata->entityClass(...$constructorParams);

            $this->hydrateRelations($entity, $metadata, $data);

            return $entity;


        } catch (\ReflectionException $e) {
            throw new DatabaseException("Failed to create entity {$metadata->entityClass}: {$e->getMessage()}", 0, $e);
        }
    }

    private function hydrateRelations(object $entity, EntityMetadata $metadata, array $data): void
    {

        foreach ($metadata->properties as $propertyMetadata) {
            if (! $propertyMetadata->isRelation) {
                continue;
            }

            $relationValue = $this->loadRelation($propertyMetadata, $data);

            if ($relationValue !== null) {
                $this->setProperty($entity, $propertyMetadata->name, $relationValue);
            }
        }
    }

    private function loadRelation(PropertyMetadata $propertyMetadata, array $data): mixed
    {
        $keyValue = $this->getRelationKeyValue($propertyMetadata, $data);

        if ($keyValue === null) {
            return $this->getDefaultRelationValue($propertyMetadata);
        }

        switch ($propertyMetadata->relationType) {
            case 'one-to-one':
                return $this->loadOneToOneRelation($propertyMetadata, $keyValue);
            case 'hasMany':
                return $this->loadHasManyRelation($propertyMetadata, $keyValue);
            case 'belongsTo':
                return $this->loadBelongsToRelation($propertyMetadata, $keyValue);
            default:
                throw new DatabaseException("Unknown relation type {$propertyMetadata->relationType}");
        }
    }

    private function getRelationKeyValue(PropertyMetadata $propertyMetadata, array $data): mixed
    {
        return match($propertyMetadata->relationType) {
            'one-to-one' => $data[$propertyMetadata->relationLocalKey] ?? null,
            'hasMany' => $data[$propertyMetadata->relationLocalKey] ?? null,
            'belongsTo' => $data[$propertyMetadata->relationForeignKey] ?? null,
            default => null
        };
    }

    private function loadOneToOneRelation(PropertyMetadata $propertyMetadata, mixed $localKeyValue): ?object
    {
        if ($this->entityLoader === null) {
            return null; // No loader available, skip relation loading
        }

        return $this->entityLoader->findOneBy(
            $propertyMetadata->relationTargetClass,
            [$this->getForeignKeyPropertyName($propertyMetadata) => $localKeyValue]
        );
    }

    private function loadHasManyRelation(PropertyMetadata $propertyMetadata, mixed $localKeyValue): array
    {
        if ($this->entityLoader === null) {
            return []; // No loader available, return empty array
        }

        return $this->entityLoader->findBy(
            $propertyMetadata->relationTargetClass,
            [$this->getForeignKeyPropertyName($propertyMetadata) => $localKeyValue]
        );
    }

    private function loadBelongsToRelation(PropertyMetadata $propertyMetadata, mixed $foreignKeyValue): ?object
    {
        if ($foreignKeyValue === null || $this->entityLoader === null) {
            return null;
        }

        return $this->entityLoader->find(
            $propertyMetadata->relationTargetClass,
            $foreignKeyValue
        );
    }

    private function getForeignKeyPropertyName(PropertyMetadata $propertyMetadata): string
    {
        return $propertyMetadata->relationForeignKey;
    }

    private function getDefaultRelationValue(PropertyMetadata $propertyMetadata): mixed
    {
        return match($propertyMetadata->relationType) {
            'one-to-one' => null,
            'hasMany' => [],
            'belongsTo' => null,
            default => null
        };
    }

    public function hydrateWithRelations(EntityMetadata $metadata, array $data, array $relations = []): object
    {
        // Wenn keine Relations spezifiziert sind, normale Hydration verwenden
        if (empty($relations)) {
            return $this->hydrate($metadata, $data);
        }

        // Entity ohne Relations erstellen
        $entity = $this->hydrateWithoutRelations($metadata, $data);

        // Eager Loading für spezifizierte Relations
        $this->eagerLoadRelations($entity, $metadata, $data, $relations);

        return $entity;
    }

    /**
     * Hydrate entity without relations (for eager loading)
     */
    private function hydrateWithoutRelations(EntityMetadata $metadata, array $data): object
    {
        try {
            $constructor = $metadata->reflection->getConstructor();

            if (! $constructor) {
                throw new DatabaseException("Entity {$metadata->entityClass} must have a constructor");
            }

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

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

                // Skip relations in constructor
                if ($propertyMetadata->isRelation) {
                    $constructorParams[] = $this->getDefaultRelationValue($propertyMetadata);

                    continue;
                }

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

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

                $constructorParams[] = $value;
            }

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

        } catch (\ReflectionException $e) {
            throw new DatabaseException("Failed to create entity {$metadata->entityClass}: {$e->getMessage()}", 0, $e);
        }
    }

    /**
     * Eager load specified relations
     */
    private function eagerLoadRelations(object $entity, EntityMetadata $metadata, array $data, array $relations): void
    {
        foreach ($relations as $relationName) {
            $propertyMetadata = $metadata->getProperty($relationName);

            if (! $propertyMetadata || ! $propertyMetadata->isRelation) {
                continue; // Skip non-existent or non-relation properties
            }

            $relationValue = $this->loadRelation($propertyMetadata, $data);

            if ($relationValue !== null) {
                $this->setProperty($entity, $propertyMetadata->name, $relationValue);
            }
        }
    }

    /**
     * Hydrate multiple entities with eager loaded relations (solves N+1 problem)
     */
    public function hydrateManyWithRelations(EntityMetadata $metadata, array $dataRows, array $relations = []): array
    {
        if (empty($dataRows)) {
            return [];
        }

        // Wenn keine Relations spezifiziert sind, normale Hydration verwenden
        if (empty($relations)) {
            return array_map(fn ($data) => $this->hydrate($metadata, $data), $dataRows);
        }

        // Schritt 1: Alle Entities ohne Relations erstellen
        $entities = [];
        $entityKeys = []; // Sammle alle IDs/Keys für Batch-Loading

        foreach ($dataRows as $index => $data) {
            $entity = $this->hydrateWithoutRelations($metadata, $data);
            $entities[$index] = $entity;

            // Entity ID für Batch-Loading sammeln
            $idProperty = $metadata->getProperty($metadata->idProperty);
            if ($idProperty) {
                $entityId = $data[$idProperty->columnName] ?? null;
                if ($entityId !== null) {
                    $entityKeys[$index] = $entityId;
                }
            }
        }

        // Schritt 2: Relations batch-weise laden
        $this->batchLoadRelations($entities, $entityKeys, $metadata, $dataRows, $relations);

        return array_values($entities);
    }

    /**
     * Batch load relations for multiple entities to avoid N+1 problem
     */
    private function batchLoadRelations(array $entities, array $entityKeys, EntityMetadata $metadata, array $dataRows, array $relations): void
    {
        if ($this->entityLoader === null || empty($entityKeys)) {
            return;
        }

        foreach ($relations as $relationName) {
            $propertyMetadata = $metadata->getProperty($relationName);

            if (! $propertyMetadata || ! $propertyMetadata->isRelation) {
                continue;
            }

            $this->batchLoadSingleRelation($entities, $entityKeys, $propertyMetadata, $dataRows);
        }
    }

    /**
     * Batch load a single relation type for all entities
     */
    private function batchLoadSingleRelation(array $entities, array $entityKeys, PropertyMetadata $propertyMetadata, array $dataRows): void
    {
        switch ($propertyMetadata->relationType) {
            case 'belongsTo':
                $this->batchLoadBelongsToRelation($entities, $propertyMetadata, $dataRows);

                break;

            case 'hasMany':
                $this->batchLoadHasManyRelation($entities, $entityKeys, $propertyMetadata);

                break;

            case 'one-to-one':
                $this->batchLoadOneToOneRelation($entities, $entityKeys, $propertyMetadata);

                break;
        }
    }

    /**
     * Batch load belongsTo relations (foreign key lookup)
     */
    private function batchLoadBelongsToRelation(array $entities, PropertyMetadata $propertyMetadata, array $dataRows): void
    {
        // Sammle alle Foreign Keys
        $foreignKeys = [];
        foreach ($dataRows as $index => $data) {
            $foreignKey = $data[$propertyMetadata->relationLocalKey] ?? null;
            if ($foreignKey !== null) {
                $foreignKeys[$index] = $foreignKey;
            }
        }

        if (empty($foreignKeys)) {
            return;
        }

        // Batch-Load aller referenzierten Entities
        $relatedEntities = $this->entityLoader->findBy(
            $propertyMetadata->relationTargetClass,
            ['id' => array_values(array_unique($foreignKeys))] // IN-Query
        );

        // Index by ID für schnelle Lookups
        $relatedByKey = [];
        foreach ($relatedEntities as $related) {
            $idProperty = (new \ReflectionClass($related))->getProperty('id');
            $idProperty->setAccessible(true);
            $relatedByKey[$idProperty->getValue($related)] = $related;
        }

        // Relations zu Entities zuweisen
        foreach ($foreignKeys as $index => $foreignKey) {
            if (isset($relatedByKey[$foreignKey]) && isset($entities[$index])) {
                $this->setProperty($entities[$index], $propertyMetadata->name, $relatedByKey[$foreignKey]);
            }
        }
    }

    /**
     * Batch load hasMany relations (reverse foreign key lookup)
     */
    private function batchLoadHasManyRelation(array $entities, array $entityKeys, PropertyMetadata $propertyMetadata): void
    {
        if (empty($entityKeys)) {
            return;
        }

        // Batch-Load aller related Entities mit IN-Query
        $relatedEntities = $this->entityLoader->findBy(
            $propertyMetadata->relationTargetClass,
            [$propertyMetadata->relationForeignKey => array_values($entityKeys)]
        );

        // Gruppiere related Entities nach Foreign Key
        $relatedByForeignKey = [];
        foreach ($relatedEntities as $related) {
            $foreignKeyProperty = (new \ReflectionClass($related))->getProperty($propertyMetadata->relationForeignKey);
            $foreignKeyProperty->setAccessible(true);
            $foreignKeyValue = $foreignKeyProperty->getValue($related);

            if (! isset($relatedByForeignKey[$foreignKeyValue])) {
                $relatedByForeignKey[$foreignKeyValue] = [];
            }
            $relatedByForeignKey[$foreignKeyValue][] = $related;
        }

        // Relations zu Entities zuweisen
        foreach ($entityKeys as $index => $entityKey) {
            if (isset($entities[$index])) {
                $relatedCollection = $relatedByForeignKey[$entityKey] ?? [];
                $this->setProperty($entities[$index], $propertyMetadata->name, $relatedCollection);
            }
        }
    }

    /**
     * Batch load one-to-one relations
     */
    private function batchLoadOneToOneRelation(array $entities, array $entityKeys, PropertyMetadata $propertyMetadata): void
    {
        if (empty($entityKeys)) {
            return;
        }

        // Batch-Load aller related Entities
        $relatedEntities = $this->entityLoader->findBy(
            $propertyMetadata->relationTargetClass,
            [$propertyMetadata->relationForeignKey => array_values($entityKeys)]
        );

        // Index by foreign key
        $relatedByForeignKey = [];
        foreach ($relatedEntities as $related) {
            $foreignKeyProperty = (new \ReflectionClass($related))->getProperty($propertyMetadata->relationForeignKey);
            $foreignKeyProperty->setAccessible(true);
            $foreignKeyValue = $foreignKeyProperty->getValue($related);
            $relatedByForeignKey[$foreignKeyValue] = $related; // Only one relation per key
        }

        // Relations zu Entities zuweisen
        foreach ($entityKeys as $index => $entityKey) {
            if (isset($entities[$index]) && isset($relatedByForeignKey[$entityKey])) {
                $this->setProperty($entities[$index], $propertyMetadata->name, $relatedByForeignKey[$entityKey]);
            }
        }
    }

    private function setProperty(object $entity, string $propertyName, mixed $value): void
    {
        $reflection = new \ReflectionClass($entity);
        $property = $reflection->getProperty($propertyName);

        // Property zugänglich machen (falls private/protected)
        $property->setValue($entity, $value);
    }
}
