<?php

declare(strict_types=1);

namespace App\Framework\Database\UnitOfWork;

use App\Framework\Database\Metadata\MetadataRegistry;

/**
 * Tracks entity changes for Unit of Work pattern using WeakMaps for optimal memory management
 */
final class ChangeTracker
{
    /** @var \WeakMap<object, EntityState> Track entity states */
    private \WeakMap $entityStates;

    /** @var \WeakMap<object, array> Original entity data for dirty checking */
    private \WeakMap $originalData;

    /** @var array<string, array<object>> Entities grouped by state for bulk operations */
    private array $entitiesByState = [
        'new' => [],
        'dirty' => [],
        'deleted' => [],
    ];

    public function __construct(
        private readonly MetadataRegistry $metadataRegistry
    ) {
        $this->entityStates = new \WeakMap();
        $this->originalData = new \WeakMap();
    }

    /**
     * Track a new entity
     */
    public function trackNew(object $entity): void
    {
        $this->entityStates[$entity] = EntityState::NEW;
        $this->entitiesByState['new'][] = $entity;

        // Store original data for potential rollback
        $this->originalData[$entity] = $this->extractEntityData($entity);
    }

    /**
     * Track a clean entity (just loaded from database)
     */
    public function trackClean(object $entity): void
    {
        $this->entityStates[$entity] = EntityState::CLEAN;
        $this->originalData[$entity] = $this->extractEntityData($entity);
    }

    /**
     * Mark entity as modified
     */
    public function markDirty(object $entity): void
    {
        $currentState = $this->entityStates[$entity] ?? EntityState::DETACHED;

        // Only clean entities can become dirty
        // New entities stay new, deleted entities stay deleted
        if ($currentState === EntityState::CLEAN) {
            $this->entityStates[$entity] = EntityState::DIRTY;
            $this->entitiesByState['dirty'][] = $entity;
        }
    }

    /**
     * Mark entity for deletion
     */
    public function markDeleted(object $entity): void
    {
        $currentState = $this->entityStates[$entity] ?? EntityState::DETACHED;

        // Remove from other states if present
        if ($currentState === EntityState::NEW) {
            $this->removeFromStateArray('new', $entity);
        } elseif ($currentState === EntityState::DIRTY) {
            $this->removeFromStateArray('dirty', $entity);
        }

        // Only track deletion if entity was previously managed
        if ($currentState !== EntityState::DETACHED) {
            $this->entityStates[$entity] = EntityState::DELETED;
            $this->entitiesByState['deleted'][] = $entity;
        }
    }

    /**
     * Detach entity from tracking
     */
    public function detach(object $entity): void
    {
        $currentState = $this->entityStates[$entity] ?? EntityState::DETACHED;

        // Remove from state arrays
        $this->removeFromStateArray(match($currentState) {
            EntityState::NEW => 'new',
            EntityState::DIRTY => 'dirty',
            EntityState::DELETED => 'deleted',
            default => null
        }, $entity);

        // WeakMaps will automatically clean up when entity is garbage collected
        unset($this->entityStates[$entity]);
        unset($this->originalData[$entity]);
    }

    /**
     * Get current entity state
     */
    public function getEntityState(object $entity): EntityState
    {
        return $this->entityStates[$entity] ?? EntityState::DETACHED;
    }

    /**
     * Check if entity has changes compared to original data
     */
    public function hasChanges(object $entity): bool
    {
        $currentState = $this->getEntityState($entity);

        if ($currentState === EntityState::NEW || $currentState === EntityState::DELETED) {
            return true;
        }

        if ($currentState === EntityState::DETACHED) {
            return false;
        }

        $originalData = $this->originalData[$entity] ?? null;
        if ($originalData === null) {
            return false;
        }

        $currentData = $this->extractEntityData($entity);

        return $originalData !== $currentData;
    }

    /**
     * Auto-detect changes by comparing current vs original data
     */
    public function detectChanges(): void
    {
        foreach ($this->entityStates as $entity => $state) {
            if ($state === EntityState::CLEAN && $this->hasChanges($entity)) {
                $this->markDirty($entity);
            }
        }
    }

    /**
     * Get entities by state for bulk operations
     */
    public function getEntitiesByState(EntityState $state): array
    {
        return match($state) {
            EntityState::NEW => $this->entitiesByState['new'],
            EntityState::DIRTY => $this->entitiesByState['dirty'],
            EntityState::DELETED => $this->entitiesByState['deleted'],
            default => []
        };
    }

    /**
     * Get all tracked entities with their states
     */
    public function getAllTrackedEntities(): array
    {
        $result = [];
        foreach ($this->entityStates as $entity => $state) {
            $result[] = ['entity' => $entity, 'state' => $state];
        }

        return $result;
    }

    /**
     * Clear all tracked entities (after successful commit)
     */
    public function clear(): void
    {
        $this->entitiesByState = [
            'new' => [],
            'dirty' => [],
            'deleted' => [],
        ];
        // WeakMaps will be automatically cleared when entities are garbage collected
    }

    /**
     * Reset entity to clean state (after successful save)
     */
    public function markClean(object $entity): void
    {
        $currentState = $this->entityStates[$entity] ?? EntityState::DETACHED;

        // Remove from state arrays
        $this->removeFromStateArray(match($currentState) {
            EntityState::NEW => 'new',
            EntityState::DIRTY => 'dirty',
            EntityState::DELETED => 'deleted',
            default => null
        }, $entity);

        // Mark as clean and update original data
        $this->entityStates[$entity] = EntityState::CLEAN;
        $this->originalData[$entity] = $this->extractEntityData($entity);
    }

    /**
     * Get tracking statistics for debugging
     */
    public function getStats(): array
    {
        return [
            'total_tracked' => count($this->entityStates),
            'new_entities' => count($this->entitiesByState['new']),
            'dirty_entities' => count($this->entitiesByState['dirty']),
            'deleted_entities' => count($this->entitiesByState['deleted']),
            'has_changes' => $this->hasAnyChanges(),
        ];
    }

    /**
     * Check if there are any changes to commit
     */
    public function hasAnyChanges(): bool
    {
        return ! empty($this->entitiesByState['new']) ||
               ! empty($this->entitiesByState['dirty']) ||
               ! empty($this->entitiesByState['deleted']);
    }

    /**
     * Extract entity data for comparison
     */
    private function extractEntityData(object $entity): array
    {
        $metadata = $this->metadataRegistry->getMetadata($entity::class);
        $data = [];

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

            try {
                $property = $metadata->reflection->getProperty($propertyName);
                if ($property->isInitialized($entity)) {
                    $data[$propertyName] = $property->getValue($entity);
                }
            } catch (\ReflectionException) {
                // Property not accessible
            }
        }

        return $data;
    }

    /**
     * Remove entity from specific state array
     */
    private function removeFromStateArray(?string $stateKey, object $entity): void
    {
        if ($stateKey === null || ! isset($this->entitiesByState[$stateKey])) {
            return;
        }

        $entities = &$this->entitiesByState[$stateKey];
        foreach ($entities as $index => $trackedEntity) {
            if ($trackedEntity === $entity) {
                unset($entities[$index]);
                $entities = array_values($entities); // Re-index array

                break;
            }
        }
    }
}
