<?php

namespace App\Framework\Database;

use App\Framework\Attributes\Singleton;
use App\Framework\Database\Metadata\EntityMetadata;
use App\Framework\Database\Metadata\MetadataRegistry;

#[Singleton]
final readonly class EntityManager
{
    private Hydrator $hydrator;
    public function __construct(
        private DatabaseManager $databaseManager,
        private MetadataRegistry $metadataRegistry,
        private TypeConverter $typeConverter,
        private IdentityMap $identityMap,
        private LazyLoader $lazyLoader
    ) {

        $this->hydrator = new Hydrator($this->typeConverter, $this);
    }

    /**
     * Findet Entity - standardmäßig lazy loading
     */
    public function find(string $entityClass, mixed $id): ?object
    {
        // Prüfe Identity Map zuerst
        if ($this->identityMap->has($entityClass, $id)) {
            return $this->identityMap->get($entityClass, $id);
        }

        return $this->findWithLazyLoading($entityClass, $id);
    }

    /**
     * Findet Entity und lädt sie sofort (eager loading)
     */
    public function findEager(string $entityClass, mixed $id): ?object
    {
        // Prüfe Identity Map zuerst
        if ($this->identityMap->has($entityClass, $id)) {
            $entity = $this->identityMap->get($entityClass, $id);

            // Falls es ein Ghost ist, initialisiere es
            if ($this->isLazyGhost($entity)) {
                $this->initializeLazyObject($entity);
            }

            return $entity;
        }

        $metadata = $this->metadataRegistry->getMetadata($entityClass);

        $query = "SELECT * FROM {$metadata->tableName} WHERE {$metadata->idColumn} = ?";
        $result = $this->databaseManager->getConnection()->query($query, [$id]);
        $data = $result->fetch();

        if (!$data) {
            return null;
        }

        $entity = $this->hydrator->hydrate($metadata, $data);

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

        return $entity;
    }

    /**
     * Interne Methode für Lazy Loading
     */
    private function findWithLazyLoading(string $entityClass, mixed $id): ?object
    {
        // Prüfe ob Entity existiert (schneller Check)
        $metadata = $this->metadataRegistry->getMetadata($entityClass);

        $query = "SELECT {$metadata->idColumn} FROM {$metadata->tableName} WHERE {$metadata->idColumn} = ?";
        $result = $this->databaseManager->getConnection()->query($query, [$id]);

        if (!$result->fetch()) {
            return null;
        }

        // Erstelle Lazy Ghost
        return $this->lazyLoader->createLazyGhost($metadata, $id);
    }

    /**
     * Referenz auf Entity (ohne Existenz-Check)
     */
    public function getReference(string $entityClass, mixed $id): object
    {
        // Prüfe Identity Map
        if ($this->identityMap->has($entityClass, $id)) {
            return $this->identityMap->get($entityClass, $id);
        }

        $metadata = $this->metadataRegistry->getMetadata($entityClass);
        return $this->lazyLoader->createLazyGhost($metadata, $id);
    }

    /**
     * Findet alle Entities - standardmäßig lazy
     */
    public function findAll(string $entityClass): array
    {
        return $this->findAllLazy($entityClass);
    }

    /**
     * Findet alle Entities und lädt sie sofort
     */
    public function findAllEager(string $entityClass): array
    {
        $metadata = $this->metadataRegistry->getMetadata($entityClass);

        $query = "SELECT * FROM {$metadata->tableName}";
        $result = $this->databaseManager->getConnection()->query($query);

        $entities = [];
        foreach ($result->fetchAll() as $data) {
            // Prüfe Identity Map für jede Entity
            $idValue = $data[$metadata->idColumn];

            if ($this->identityMap->has($entityClass, $idValue)) {
                $entity = $this->identityMap->get($entityClass, $idValue);

                // Falls Ghost, initialisiere es für eager loading
                if ($this->isLazyGhost($entity)) {
                    $this->initializeLazyObject($entity);
                }

                $entities[] = $entity;
            } else {
                $entity = $this->hydrator->hydrate($metadata, $data);
                $this->identityMap->set($entityClass, $idValue, $entity);
                $entities[] = $entity;
            }
        }

        return $entities;
    }

    /**
     * Interne Methode für lazy findAll
     */
    private function findAllLazy(string $entityClass): array
    {
        $metadata = $this->metadataRegistry->getMetadata($entityClass);

        // Nur IDs laden
        $query = "SELECT {$metadata->idColumn} FROM {$metadata->tableName}";
        $result = $this->databaseManager->getConnection()->query($query);

        $entities = [];
        foreach ($result->fetchAll() as $data) {
            $idValue = $data[$metadata->idColumn];

            if ($this->identityMap->has($entityClass, $idValue)) {
                $entities[] = $this->identityMap->get($entityClass, $idValue);
            } else {
                $entities[] = $this->lazyLoader->createLazyGhost($metadata, $idValue);
            }
        }

        return $entities;
    }

    /**
     * Findet Entities nach Kriterien
     */
    public function findBy(string $entityClass, array $criteria, ?array $orderBy = null, ?int $limit = null): array
    {
        $metadata = $this->metadataRegistry->getMetadata($entityClass);

        $query = "SELECT {$metadata->idColumn} FROM {$metadata->tableName}";
        $params = [];

        if (!empty($criteria)) {
            $conditions = [];
            foreach ($criteria as $field => $value) {
                $columnName = $metadata->getColumnName($field);
                $conditions[] = "{$columnName} = ?";
                $params[] = $value;
            }
            $query .= " WHERE " . implode(' AND ', $conditions);
        }

        if ($orderBy) {
            $orderClauses = [];
            foreach ($orderBy as $field => $direction) {
                $columnName = $metadata->getColumnName($field);
                $orderClauses[] = "{$columnName} " . strtoupper($direction);
            }
            $query .= " ORDER BY " . implode(', ', $orderClauses);
        }

        if ($limit) {
            $query .= " LIMIT {$limit}";
        }

        $result = $this->databaseManager->getConnection()->query($query, $params);

        $entities = [];
        foreach ($result->fetchAll() as $data) {
            $idValue = $data[$metadata->idColumn];
            $entities[] = $this->find($entityClass, $idValue); // Nutzt lazy loading
        }

        return $entities;
    }

    /**
     * Findet eine Entity nach Kriterien
     */
    public function findOneBy(string $entityClass, array $criteria): ?object
    {
        $results = $this->findBy($entityClass, $criteria, limit: 1);
        return $results[0] ?? null;
    }

    /**
     * Utility Methods
     */
    public function detach(object $entity): void
    {
        $metadata = $this->metadataRegistry->getMetadata($entity::class);

        // ID der Entity ermitteln
        $constructor = $metadata->reflection->getConstructor();
        if ($constructor) {
            foreach ($constructor->getParameters() as $param) {
                $paramName = $param->getName();
                $propertyMetadata = $metadata->getProperty($paramName);

                if ($propertyMetadata && $propertyMetadata->columnName === $metadata->idColumn) {
                    try {
                        $property = $metadata->reflection->getProperty($paramName);
                        $id = $property->getValue($entity);
                        $this->identityMap->remove($entity::class, $id);
                        break;
                    } catch (\ReflectionException) {
                        // Property nicht gefunden
                    }
                }
            }
        }
    }

    public function clear(): void
    {
        $this->identityMap->clear();
    }

    public function getIdentityMapStats(): array
    {
        return $this->identityMap->getStats();
    }

    public function isLazyGhost(object $entity): bool
    {
        return $this->lazyLoader->isLazyGhost($entity);
    }

    public function initializeLazyObject(object $entity): void
    {
        $this->lazyLoader->initializeLazyObject($entity);
    }

    public function getMetadata(string $entityClass): EntityMetadata
    {
        return $this->metadataRegistry->getMetadata($entityClass);
    }

    /**
     * Generiert eine einzigartige ID für Entities
     */
    public function generateId(): string
    {
        return IdGenerator::generate();
    }

    /**
     * Speichert eine Entity (INSERT oder UPDATE)
     */
    public function save(object $entity): object
    {
        $metadata = $this->metadataRegistry->getMetadata($entity::class);

        $idProperty = $metadata->reflection->getProperty($metadata->idProperty);
        $id = $idProperty->getValue($entity);

        // Prüfe ob Entity bereits existiert
        if ($this->exists($entity::class, $id)) {
            return $this->update($entity);
        } else {
            return $this->insert($entity);
        }
    }

    /**
     * Prüft ob eine Entity mit der angegebenen ID existiert
     */
    public function exists(string $entityClass, mixed $id): bool
    {
        $metadata = $this->metadataRegistry->getMetadata($entityClass);

        $query = "SELECT 1 FROM {$metadata->tableName} WHERE {$metadata->idColumn} = ? LIMIT 1";
        $result = $this->databaseManager->getConnection()->query($query, [$id]);

        return (bool) $result->fetch();
    }

    /**
     * Fügt eine neue Entity ein (INSERT)
     */
    public function insert(object $entity): object
    {


        $metadata = $this->metadataRegistry->getMetadata($entity::class);

        // Columnnamen und Values sammeln
        $columns = [];
        $values = [];
        $params = [];

        foreach ($metadata->properties as $propertyName => $propertyMetadata) {
            // ID Property überspringen wenn auto-increment
            if ($propertyName === $metadata->idProperty && $propertyMetadata->autoIncrement) {
                continue;
            }

            // Property-Wert auslesen
            $property = $metadata->reflection->getProperty($propertyName);

            if(!$property->isInitialized($entity)) {
                continue;
            }


            $columns[] = $propertyMetadata->columnName;
            $values[] = '?';

            $params[] = $property->getValue($entity);
        }

        // INSERT Query bauen
        $query = "INSERT INTO {$metadata->tableName} (" . implode(', ', $columns) . ") "
              . "VALUES (" . implode(', ', $values) . ")";

        // Query ausführen
        $result = $this->databaseManager->getConnection()->execute($query, $params);

        // In Identity Map speichern
        $idProperty = $metadata->reflection->getProperty($metadata->idProperty);
        $id = $idProperty->getValue($entity);
        $this->identityMap->set($entity::class, $id, $entity);

        return $entity;
    }

    /**
     * Aktualisiert eine vorhandene Entity (UPDATE)
     */
    public function update(object $entity): object
    {
        $metadata = $this->metadataRegistry->getMetadata($entity::class);

        // SET-Clause und Params aufbauen
        $setClause = [];
        $params = [];

        foreach ($metadata->properties as $propertyName => $propertyMetadata) {

            // Relations beim Update ignorieren
            if($propertyMetadata->isRelation) {
                continue;
            }

            // ID überspringen (wird nicht aktualisiert)
            if ($propertyName === $metadata->idProperty) {
                continue;
            }

            $setClause[] = "{$propertyMetadata->columnName} = ?";

            // Property-Wert auslesen
            $property = $metadata->reflection->getProperty($propertyName);
            $params[] = $property->getValue($entity);
        }

        // ID für WHERE Clause hinzufügen
        $idProperty = $metadata->reflection->getProperty($metadata->idProperty);
        $id = $idProperty->getValue($entity);
        $params[] = $id;

        // UPDATE Query bauen
        $query = "UPDATE {$metadata->tableName} SET " . implode(', ', $setClause)
              . " WHERE {$metadata->idColumn} = ?";

        // Query ausführen
        $result = $this->databaseManager->getConnection()->query($query, $params);

        // Identity Map aktualisieren
        $this->identityMap->set($entity::class, $id, $entity);

        return $entity;
    }

    /**
     * Löscht eine Entity
     */
    public function delete(object $entity): void
    {
        $metadata = $this->metadataRegistry->getMetadata($entity::class);

        // ID auslesen
        $idProperty = $metadata->reflection->getProperty($metadata->idProperty);
        $id = $idProperty->getValue($entity);

        // DELETE Query ausführen
        $query = "DELETE FROM {$metadata->tableName} WHERE {$metadata->idColumn} = ?";
        $this->databaseManager->getConnection()->query($query, [$id]);

        // Aus Identity Map entfernen
        $this->identityMap->remove($entity::class, $id);
    }

    /**
     * Speichert mehrere Entities auf einmal
     */
    public function saveAll(object ...$entities): array
    {
        $result = [];
        foreach ($entities as $entity) {
            $result[] = $this->save($entity);
        }
        return $result;
    }

    /**
     * Führt eine Funktion in einer Transaktion aus
     */
    public function transaction(callable $callback): mixed
    {
        $connection = $this->databaseManager->getConnection();

        // Wenn bereits in einer Transaktion, führe Callback direkt aus
        if ($connection->inTransaction()) {
            return $callback($this);
        }

        // Neue Transaktion starten
        $connection->beginTransaction();

        try {
            $result = $callback($this);
            $connection->commit();
            return $result;
        } catch (\Throwable $e) {
            $connection->rollback();
            throw $e;
        }
    }
}
