<?php

declare(strict_types=1);

namespace App\Framework\Database\Services;

use App\Framework\Database\BatchRelationLoader;
use App\Framework\Database\Cache\EntityCacheManager;
use App\Framework\Database\Criteria\Criteria;
use App\Framework\Database\Criteria\CriteriaQuery;
use App\Framework\Database\DatabaseManager;
use App\Framework\Database\Events\EntityEventManager;
use App\Framework\Database\HydratorInterface;
use App\Framework\Database\IdentityMap;
use App\Framework\Database\LazyLoader;
use App\Framework\Database\Metadata\MetadataRegistry;
use App\Framework\Database\ValueObjects\SqlQuery;

final readonly class EntityFinder
{
    public function __construct(
        private DatabaseManager $databaseManager,
        private MetadataRegistry $metadataRegistry,
        private IdentityMap $identityMap,
        private LazyLoader $lazyLoader,
        private HydratorInterface $hydrator,
        private BatchRelationLoader $batchRelationLoader,
        private EntityEventManager $entityEventManager,
        private ?EntityCacheManager $cacheManager = null
    ) {
    }

    /**
     * Findet Entity - standardmäßig lazy loading
     */
    public function find(string $entityClass, mixed $id): ?object
    {
        // Use cache manager if available
        if ($this->cacheManager !== null) {
            return $this->cacheManager->findEntity($entityClass, $id, function () use ($entityClass, $id) {
                return $this->findWithLazyLoading($entityClass, $id);
            });
        }

        // Fallback to direct loading
        // 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 = SqlQuery::create("SELECT * FROM {$metadata->tableName} WHERE {$metadata->idColumn} = ?", [$id]);
        $result = $this->databaseManager->getConnection()->query($query);
        $data = $result->fetch();

        if (! $data) {
            return null;
        }

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

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

        // Entity Loaded Event dispatchen
        $this->entityEventManager->entityLoaded($entity, $entityClass, $id, $data, false);

        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 = SqlQuery::create("SELECT {$metadata->idColumn} FROM {$metadata->tableName} WHERE {$metadata->idColumn} = ?", [$id]);
        $result = $this->databaseManager->getConnection()->query($query);

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

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

        // Entity Loaded Event dispatchen (als Lazy)
        $this->entityEventManager->entityLoaded($entity, $entityClass, $id, [], true);

        return $entity;
    }

    /**
     * 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 = SqlQuery::select($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 = SqlQuery::select($metadata->tableName, [$metadata->idColumn]);
        $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;
    }

    /**
     * @deprecated Use findByCriteria() with DetachedCriteria instead for better type safety
     * Findet Entities nach Kriterien
     */
    public function findBy(string $entityClass, array $criteria, ?array $orderBy = null, ?int $limit = null): array
    {
        // Use cache manager if available
        if ($this->cacheManager !== null) {
            return $this->cacheManager->findCollection($entityClass, $criteria, $orderBy, $limit, null, function () use ($entityClass, $criteria, $orderBy, $limit) {
                return $this->findByWithoutCache($entityClass, $criteria, $orderBy, $limit);
            });
        }

        return $this->findByWithoutCache($entityClass, $criteria, $orderBy, $limit);
    }

    /**
     * Internal method for finding entities without cache
     */
    private function findByWithoutCache(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}";
        }

        $sqlQuery = SqlQuery::create($query, $params);
        $result = $this->databaseManager->getConnection()->query($sqlQuery);

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

        return $entities;
    }

    /**
     * @deprecated Use findOneByCriteria() with DetachedCriteria instead for better type safety
     * Findet eine Entity nach Kriterien
     */
    public function findOneBy(string $entityClass, array $criteria): ?object
    {
        $results = $this->findBy($entityClass, $criteria, limit: 1);

        return $results[0] ?? null;
    }

    /**
     * Findet Entities mit vorab geladenen Relationen (N+1 Solution)
     *
     * @param string $entityClass
     * @param array $criteria
     * @param array $relations Relations to preload ['comments', 'author', etc.]
     * @param array|null $orderBy
     * @param int|null $limit
     * @return array<object>
     */
    public function findWithRelations(
        string $entityClass,
        array $criteria = [],
        array $relations = [],
        ?array $orderBy = null,
        ?int $limit = null
    ): array {
        // Step 1: Load base entities (using existing findBy logic but eager)
        $entities = $this->findByEager($entityClass, $criteria, $orderBy, $limit);

        if (empty($entities) || empty($relations)) {
            return $entities;
        }

        // Step 2: Preload each specified relation in batch
        foreach ($relations as $relationName) {
            $this->batchRelationLoader->preloadRelation($entities, $relationName);
        }

        return $entities;
    }

    /**
     * Eager version of findBy - loads full entities immediately
     * Used internally by findWithRelations
     */
    private function findByEager(string $entityClass, array $criteria, ?array $orderBy = null, ?int $limit = null): array
    {
        $metadata = $this->metadataRegistry->getMetadata($entityClass);

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

        if (! empty($criteria)) {
            $conditions = [];
            foreach ($criteria as $field => $value) {
                if (is_array($value)) {
                    // Handle IN queries for batch loading
                    $placeholders = str_repeat('?,', count($value) - 1) . '?';
                    $columnName = $metadata->getColumnName($field);
                    $conditions[] = "{$columnName} IN ({$placeholders})";
                    $params = array_merge($params, array_values($value));
                } else {
                    $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}";
        }

        $sqlQuery = SqlQuery::create($query, $params);
        $result = $this->databaseManager->getConnection()->query($sqlQuery);

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

            // Check identity map first
            if ($this->identityMap->has($entityClass, $idValue)) {
                $entity = $this->identityMap->get($entityClass, $idValue);

                // If it's a lazy ghost, initialize it for 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;
    }

    /**
     * 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();
    }

    /**
     * Execute criteria query and return entities
     */
    public function findByCriteria(Criteria $criteria): array
    {
        $metadata = $this->metadataRegistry->getMetadata($criteria->entityClass);
        $criteriaQuery = new CriteriaQuery($criteria, $metadata->tableName);

        $sql = $criteriaQuery->toSql();
        $parameters = $criteriaQuery->getParameters();

        $query = SqlQuery::create($sql, $parameters);
        $result = $this->databaseManager->getConnection()->query($query);

        $entities = [];
        foreach ($result->fetchAll() as $data) {
            // Check if it's a projection query (non-entity result)
            $projection = $criteria->getProjection();
            if ($projection && count($projection->getAliases()) > 0) {
                // Return raw data for projection queries
                $entities[] = $data;
            } else {
                // Normal entity hydration
                $idValue = $data[$metadata->idColumn];

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

        return $entities;
    }

    /**
     * Execute criteria query and return first result
     */
    public function findOneByCriteria(Criteria $criteria): ?object
    {
        $criteria->setMaxResults(1);
        $results = $this->findByCriteria($criteria);

        return $results[0] ?? null;
    }

    /**
     * Count entities matching criteria
     */
    public function countByCriteria(Criteria $criteria): int
    {
        $metadata = $this->metadataRegistry->getMetadata($criteria->entityClass);

        // Create count criteria
        $countCriteria = clone $criteria;
        $countCriteria->setProjection(\App\Framework\Database\Criteria\Projections::count());
        $countCriteria->setMaxResults(null);
        $countCriteria->setFirstResult(null);

        $criteriaQuery = new CriteriaQuery($countCriteria, $metadata->tableName);

        $sql = $criteriaQuery->toSql();
        $parameters = $criteriaQuery->getParameters();

        $result = $this->databaseManager->getConnection()->queryScalar($sql, $parameters);

        return (int) $result;
    }

    /**
     * Helper methods for lazy loading
     */
    public function isLazyGhost(object $entity): bool
    {
        return $this->lazyLoader->isLazyGhost($entity);
    }

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