<?php

declare(strict_types=1);

namespace App\Framework\Database\Services;

use App\Framework\Database\Cache\EntityCacheManager;
use App\Framework\Database\DatabaseManager;
use App\Framework\Database\Events\EntityEventManager;
use App\Framework\Database\IdentityMap;
use App\Framework\Database\Metadata\MetadataRegistry;
use App\Framework\Database\TypeCaster\TypeCasterRegistry;
use App\Framework\Database\ValueObjects\SqlQuery;

final readonly class EntityPersister
{
    public function __construct(
        private DatabaseManager $databaseManager,
        private MetadataRegistry $metadataRegistry,
        private IdentityMap $identityMap,
        private EntityEventManager $entityEventManager,
        private TypeCasterRegistry $typeCasterRegistry,
        private ?EntityCacheManager $cacheManager = null
    ) {
    }

    /**
     * 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)) {
            $result = $this->update($entity);
        } else {
            $result = $this->insert($entity);
        }

        // Cache the entity after successful save
        if ($this->cacheManager !== null) {
            $this->cacheManager->cacheEntity($result);
        }

        return $result;
    }

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

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

        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[] = '?';

            $value = $property->getValue($entity);
            // Convert value objects to their database representations using TypeCasters
            $params[] = $this->convertValueToDatabase($value);
        }

        // INSERT Query bauen
        $data = array_combine($columns, $params);

        // Debug logging for all data being inserted
        error_log("EntityPersister INSERT - Table: " . $metadata->tableName);
        foreach ($data as $column => $value) {
            $length = is_string($value) ? strlen($value) : 'N/A';
            error_log("  Column: $column, Value: " . var_export($value, true) . " (length: $length)");
        }

        $query = SqlQuery::insert($metadata->tableName, $data);

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

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

        // Entity Created Event dispatchen
        $this->entityEventManager->entityCreated($entity, $entity::class, $id, $params);

        return $entity;
    }

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

        // ID für Change Tracking und WHERE Clause
        $idProperty = $metadata->reflection->getProperty($metadata->idProperty);
        $id = $idProperty->getValue($entity);

        // Change Tracking: Original Entity aus IdentityMap laden
        $originalEntity = $this->identityMap->get($entity::class, $id);
        $changes = [];
        $oldValues = [];
        $newValues = [];

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

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

            // Change Tracking: Vergleiche mit Original-Werten
            $oldValue = null;
            if ($originalEntity !== null) {
                $originalProperty = $metadata->reflection->getProperty($propertyName);
                $oldValue = $originalProperty->getValue($originalEntity);
            }

            // Nur bei Änderungen in SET clause aufnehmen und Changes tracken
            if ($originalEntity === null || $oldValue !== $newValue) {
                $setClause[] = "{$propertyMetadata->columnName} = ?";
                $params[] = $this->convertValueToDatabase($newValue);

                // Change Tracking Daten sammeln
                $changes[] = $propertyName;
                $oldValues[$propertyName] = $oldValue;
                $newValues[$propertyName] = $newValue;
            }
        }

        // Wenn keine Änderungen vorliegen, kein UPDATE ausführen
        if (empty($setClause)) {
            return $entity;
        }

        // ID für WHERE Clause hinzufügen
        $params[] = $id;

        // UPDATE Query bauen
        $updateData = [];
        $setColumns = [];
        foreach ($setClause as $i => $clause) {
            // Extract column name from "column = ?" format
            $column = explode(' = ', $clause)[0];
            $setColumns[] = $column;
            $updateData[$column] = $params[$i];
        }

        $query = SqlQuery::update($metadata->tableName, $updateData, [$metadata->idColumn => $this->convertValueToDatabase($id)]);

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

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

        // Entity Updated Event mit Change Tracking dispatchen
        $this->entityEventManager->entityUpdated($entity, $entity::class, $id, $changes, $oldValues, $newValues);

        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 = SqlQuery::delete($metadata->tableName, [$metadata->idColumn => $this->convertValueToDatabase($id)]);
        $this->databaseManager->getConnection()->query($query);

        // Evict from cache
        if ($this->cacheManager !== null) {
            $this->cacheManager->evictEntity($entity);
        }

        // Entity Deleted Event dispatchen (vor Identity Map Remove)
        $this->entityEventManager->entityDeleted($entity, $entity::class, $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;
    }

    /**
     * Convert value objects to their database representations using TypeCasters
     */
    private function convertValueToDatabase(mixed $value): mixed
    {
        if ($value === null) {
            return null;
        }

        // Try to find a specific caster for this value's type
        $valueType = is_object($value) ? get_class($value) : gettype($value);
        $caster = $this->typeCasterRegistry->getCasterForType($valueType);

        if ($caster !== null) {
            $result = $caster->toDatabase($value);
            // Debug logging for ULID issues
            if ($valueType === 'App\Framework\Ulid\Ulid') {
                error_log("ULID converted: " . var_export($result, true) . " (length: " . strlen($result) . ")");
            }

            return $result;
        }

        // Fallback for common cases
        if (is_object($value)) {
            // Check if object has toString method
            if (method_exists($value, '__toString')) {
                return (string) $value;
            }

            // For enums, get the value
            if ($value instanceof \BackedEnum) {
                return $value->value;
            }
        }

        // Return primitive values as-is
        return $value;
    }
}
