<?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
{
    public function __construct(
        private TypeConverter $typeConverter,
        private EntityManager $entityManager,
    ) {}

    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
    {
        return $this->entityManager->findOneBy(
            $propertyMetadata->relationTargetClass,
            [$this->getForeignKeyPropertyName($propertyMetadata) => $localKeyValue]
        );
    }

    private function loadHasManyRelation(PropertyMetadata $propertyMetadata, mixed $localKeyValue): array
    {
        return $this->entityManager->findBy(
            $propertyMetadata->relationTargetClass,
            [$this->getForeignKeyPropertyName($propertyMetadata) => $localKeyValue]
        );
    }

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

        return $this->entityManager->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
        };
    }


    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);
    }

}
