<?php

declare(strict_types=1);

namespace App\Framework\Database\UnitOfWork;

use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\DatabaseManager;
use App\Framework\Database\Exception\DatabaseException;
use App\Framework\Database\HydratorInterface;
use App\Framework\Database\IdentityMap;
use App\Framework\Database\Metadata\MetadataRegistry;
use App\Framework\Database\TypeConverter;

/**
 * Unit of Work implementation for transactional entity management
 */
final class UnitOfWork
{
    private ?ConnectionInterface $connection = null;

    private bool $inTransaction = false;

    private bool $autoCommit = true;

    public function __construct(
        private readonly DatabaseManager $databaseManager,
        private readonly ChangeTracker $changeTracker,
        private readonly MetadataRegistry $metadataRegistry,
        private readonly IdentityMap $identityMap,
        private readonly HydratorInterface $hydrator,
        private readonly TypeConverter $typeConverter,
        private readonly BulkOperations $bulkOperations,
    ) {
    }

    /**
     * Register a new entity for insertion
     */
    public function persist(object $entity): void
    {
        $this->changeTracker->trackNew($entity);

        // Auto-commit if enabled
        if ($this->autoCommit) {
            $this->commit();
        }
    }

    /**
     * Mark entity for deletion
     */
    public function remove(object $entity): void
    {
        $this->changeTracker->markDeleted($entity);

        // Auto-commit if enabled
        if ($this->autoCommit) {
            $this->commit();
        }
    }

    /**
     * Merge entity (mark as managed and detect changes)
     */
    public function merge(object $entity): object
    {
        $entityClass = $entity::class;
        $metadata = $this->metadataRegistry->getMetadata($entityClass);

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

        // Check if entity already exists in identity map
        if ($this->identityMap->has($entityClass, $id)) {
            $managedEntity = $this->identityMap->get($entityClass, $id);

            // Update managed entity with changes
            $this->mergeEntityChanges($managedEntity, $entity, $metadata);
            $this->changeTracker->markDirty($managedEntity);

            if ($this->autoCommit) {
                $this->commit();
            }

            return $managedEntity;
        }

        // Entity not in identity map - track as clean first, then check for changes
        $this->changeTracker->trackClean($entity);
        $this->identityMap->set($entityClass, $id, $entity);

        // Auto-commit if enabled and there are changes
        if ($this->autoCommit && $this->changeTracker->hasChanges($entity)) {
            $this->changeTracker->markDirty($entity);
            $this->commit();
        }

        return $entity;
    }

    /**
     * Detach entity from unit of work
     */
    public function detach(object $entity): void
    {
        $this->changeTracker->detach($entity);

        // Remove from identity map
        $entityClass = $entity::class;
        $metadata = $this->metadataRegistry->getMetadata($entityClass);
        $idProperty = $metadata->reflection->getProperty($metadata->idProperty);
        $id = $idProperty->getValue($entity);

        $this->identityMap->remove($entityClass, $id);
    }

    /**
     * Check if entity is managed by this unit of work
     */
    public function contains(object $entity): bool
    {
        $state = $this->changeTracker->getEntityState($entity);

        return $state !== EntityState::DETACHED;
    }

    /**
     * Flush changes to database without committing transaction
     */
    public function flush(): void
    {
        if (! $this->changeTracker->hasAnyChanges()) {
            return; // Nothing to flush
        }

        $this->ensureTransaction();

        try {
            // Auto-detect changes first
            $this->changeTracker->detectChanges();

            // Process in correct order: INSERTs, UPDATEs, DELETEs
            $this->processInserts();
            $this->processUpdates();
            $this->processDeletes();

        } catch (\Exception $e) {
            throw new DatabaseException('Failed to flush changes: ' . $e->getMessage(), previous: $e);
        }
    }

    /**
     * Commit transaction and clear change tracker
     */
    public function commit(): void
    {
        if (! $this->inTransaction && ! $this->changeTracker->hasAnyChanges()) {
            return; // Nothing to commit
        }

        $this->ensureTransaction();

        try {
            $this->flush();
            $this->connection->commit();
            $this->inTransaction = false;

            // Mark all entities as clean
            foreach ($this->changeTracker->getEntitiesByState(EntityState::NEW) as $entity) {
                $this->changeTracker->markClean($entity);
            }
            foreach ($this->changeTracker->getEntitiesByState(EntityState::DIRTY) as $entity) {
                $this->changeTracker->markClean($entity);
            }

            // Remove deleted entities from change tracker
            foreach ($this->changeTracker->getEntitiesByState(EntityState::DELETED) as $entity) {
                $this->changeTracker->detach($entity);
            }

        } catch (\Exception $e) {
            $this->rollback();

            throw new DatabaseException('Failed to commit transaction: ' . $e->getMessage(), previous: $e);
        }
    }

    /**
     * Rollback transaction and reset change tracker
     */
    public function rollback(): void
    {
        if ($this->inTransaction && $this->connection) {
            $this->connection->rollback();
            $this->inTransaction = false;
        }

        // Reset change tracker - entities return to their original state
        $this->changeTracker->clear();
    }

    /**
     * Enable/disable auto-commit mode
     */
    public function setAutoCommit(bool $autoCommit): void
    {
        $this->autoCommit = $autoCommit;
    }

    /**
     * Check if auto-commit is enabled
     */
    public function isAutoCommit(): bool
    {
        return $this->autoCommit;
    }

    /**
     * Get unit of work statistics
     */
    public function getStats(): array
    {
        return [
            'change_tracker' => $this->changeTracker->getStats(),
            'in_transaction' => $this->inTransaction,
            'auto_commit' => $this->autoCommit,
            'identity_map' => $this->identityMap->getStats(),
        ];
    }

    /**
     * Clear all tracked entities
     */
    public function clear(): void
    {
        $this->changeTracker->clear();
        $this->identityMap->clear();

        if ($this->inTransaction) {
            $this->rollback();
        }
    }

    /**
     * Ensure we have an active transaction
     */
    private function ensureTransaction(): void
    {
        if (! $this->inTransaction) {
            $this->connection = $this->databaseManager->getConnection();
            $this->connection->beginTransaction();
            $this->inTransaction = true;
        }
    }

    /**
     * Process all INSERT operations using bulk operations when beneficial
     */
    private function processInserts(): void
    {
        $newEntities = $this->changeTracker->getEntitiesByState(EntityState::NEW);

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

        // Use bulk operations for better performance with multiple entities
        if (count($newEntities) > 1) {
            $this->bulkOperations->bulkInsert($newEntities, $this->connection);
        } else {
            $this->insertEntity($newEntities[0]);
        }
    }

    /**
     * Process all UPDATE operations using bulk operations when beneficial
     */
    private function processUpdates(): void
    {
        $dirtyEntities = $this->changeTracker->getEntitiesByState(EntityState::DIRTY);

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

        // Use bulk operations for better performance with multiple entities
        if (count($dirtyEntities) > 1) {
            $this->bulkOperations->bulkUpdate($dirtyEntities, $this->connection);
        } else {
            $this->updateEntity($dirtyEntities[0]);
        }
    }

    /**
     * Process all DELETE operations using bulk operations when beneficial
     */
    private function processDeletes(): void
    {
        $deletedEntities = $this->changeTracker->getEntitiesByState(EntityState::DELETED);

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

        // Use bulk operations for better performance with multiple entities
        if (count($deletedEntities) > 1) {
            $this->bulkOperations->bulkDelete($deletedEntities, $this->connection);

            // Remove all from identity map
            foreach ($deletedEntities as $entity) {
                $entityClass = $entity::class;
                $metadata = $this->metadataRegistry->getMetadata($entityClass);
                $idProperty = $metadata->reflection->getProperty($metadata->idProperty);
                $id = $idProperty->getValue($entity);
                $this->identityMap->remove($entityClass, $id);
            }
        } else {
            $this->deleteEntity($deletedEntities[0]);
        }
    }

    /**
     * Insert single entity
     */
    private function insertEntity(object $entity): void
    {
        $metadata = $this->metadataRegistry->getMetadata($entity::class);

        $columns = [];
        $values = [];
        $params = [];

        foreach ($metadata->properties as $propertyName => $propertyMetadata) {
            if ($propertyMetadata->isRelation) {
                continue; // Skip relations
            }

            // Skip auto-increment ID
            if ($propertyName === $metadata->idProperty && $propertyMetadata->autoIncrement) {
                continue;
            }

            $property = $metadata->reflection->getProperty($propertyName);

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

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

            $rawValue = $property->getValue($entity);
            $params[] = $this->typeConverter->convertToDatabase($rawValue, $propertyMetadata);
        }

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

        $query = "INSERT INTO {$metadata->tableName} (" . implode(', ', $columns) . ") VALUES (" . implode(', ', $values) . ")";
        $this->connection->execute($query, $params);
    }

    /**
     * Update single entity
     */
    private function updateEntity(object $entity): void
    {
        $metadata = $this->metadataRegistry->getMetadata($entity::class);

        $setClause = [];
        $params = [];

        foreach ($metadata->properties as $propertyName => $propertyMetadata) {
            if ($propertyMetadata->isRelation) {
                continue; // Skip relations
            }

            // Skip ID column
            if ($propertyName === $metadata->idProperty) {
                continue;
            }

            $property = $metadata->reflection->getProperty($propertyName);
            $setClause[] = "{$propertyMetadata->columnName} = ?";

            $rawValue = $property->getValue($entity);
            $params[] = $this->typeConverter->convertToDatabase($rawValue, $propertyMetadata);
        }

        // Add ID for WHERE clause
        $idProperty = $metadata->reflection->getProperty($metadata->idProperty);
        $id = $idProperty->getValue($entity);
        $params[] = $id;

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

        $query = "UPDATE {$metadata->tableName} SET " . implode(', ', $setClause) . " WHERE {$metadata->idColumn} = ?";
        $this->connection->execute($query, $params);
    }

    /**
     * Delete single entity
     */
    private function deleteEntity(object $entity): void
    {
        $metadata = $this->metadataRegistry->getMetadata($entity::class);

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

        $query = "DELETE FROM {$metadata->tableName} WHERE {$metadata->idColumn} = ?";
        $this->connection->execute($query, [$id]);

        // Remove from identity map
        $this->identityMap->remove($entity::class, $id);
    }

    /**
     * Merge changes from one entity to another
     */
    private function mergeEntityChanges(object $target, object $source, $metadata): void
    {
        foreach ($metadata->properties as $propertyName => $propertyMetadata) {
            if ($propertyMetadata->isRelation) {
                continue; // Skip relations for now
            }

            try {
                $sourceProperty = $metadata->reflection->getProperty($propertyName);
                $targetProperty = $metadata->reflection->getProperty($propertyName);

                if ($sourceProperty->isInitialized($source)) {
                    $value = $sourceProperty->getValue($source);
                    $targetProperty->setValue($target, $value);
                }
            } catch (\ReflectionException) {
                // Property not accessible
            }
        }
    }
}
