<?php

declare(strict_types=1);

namespace App\Shared\Infrastructure\Tenant;

use App\Shared\Domain\Clock;

use function count;

use DateTimeImmutable;
use Doctrine\DBAL\DriverManager;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;

final class TenantEntityManagerFactory
{
    private const MAX_MANAGERS = 50;
    private const IDLE_TIMEOUT_SECONDS = 300; // 5 minutes

    /** @var array<string, array{manager: EntityManagerInterface, lastUsed: DateTimeImmutable}> */
    private array $managers = [];

    public function __construct(
        private readonly TenantRegistry $registry,
        private readonly Clock $clock,
        private readonly Configuration $ormConfiguration,
    ) {
    }

    public function getForTenant(TenantId $tenantId): EntityManagerInterface
    {
        $key = (string) $tenantId;

        // Evict idle connections first
        $this->evictIdleConnections();

        // Evict LRU if pool is full and we need a new manager
        if (!isset($this->managers[$key]) && count($this->managers) >= self::MAX_MANAGERS) {
            $this->evictLeastRecentlyUsed();
        }

        if (!isset($this->managers[$key])) {
            $this->managers[$key] = [
                'manager' => $this->createManagerForTenant($tenantId),
                'lastUsed' => $this->clock->now(),
            ];
        } else {
            // Health check before returning cached manager (AC4 requirement)
            $manager = $this->managers[$key]['manager'];
            if (!$manager->isOpen() || !$manager->getConnection()->isConnected()) {
                // Connection is dead, recreate the manager
                $this->closeAndRemove($key);
                $this->managers[$key] = [
                    'manager' => $this->createManagerForTenant($tenantId),
                    'lastUsed' => $this->clock->now(),
                ];
            } else {
                $this->managers[$key]['lastUsed'] = $this->clock->now();
            }
        }

        return $this->managers[$key]['manager'];
    }

    public function getPoolSize(): int
    {
        return count($this->managers);
    }

    private function evictLeastRecentlyUsed(): void
    {
        if (empty($this->managers)) {
            return;
        }

        $oldestKey = null;
        $oldestTime = null;

        foreach ($this->managers as $key => $data) {
            if ($oldestTime === null || $data['lastUsed'] < $oldestTime) {
                $oldestKey = $key;
                $oldestTime = $data['lastUsed'];
            }
        }

        if ($oldestKey !== null) {
            $this->closeAndRemove($oldestKey);
        }
    }

    private function evictIdleConnections(): void
    {
        $now = $this->clock->now();
        $keysToRemove = [];

        foreach ($this->managers as $key => $data) {
            $idleSeconds = $now->getTimestamp() - $data['lastUsed']->getTimestamp();
            if ($idleSeconds > self::IDLE_TIMEOUT_SECONDS) {
                $keysToRemove[] = $key;
            }
        }

        foreach ($keysToRemove as $key) {
            $this->closeAndRemove($key);
        }
    }

    private function closeAndRemove(string $key): void
    {
        if (isset($this->managers[$key])) {
            $manager = $this->managers[$key]['manager'];
            if ($manager->isOpen()) {
                $manager->close();
            }
            unset($this->managers[$key]);
        }
    }

    private function createManagerForTenant(TenantId $tenantId): EntityManagerInterface
    {
        $config = $this->registry->getConfig($tenantId);

        // Parse database URL and create connection parameters
        $connectionParams = $this->parseConnectionParams($config->databaseUrl);

        // Health check before creation
        /** @phpstan-ignore argument.type (Doctrine accepts both URL and explicit params) */
        $connection = DriverManager::getConnection($connectionParams);
        $this->healthCheck($connection);

        return new EntityManager($connection, $this->ormConfiguration);
    }

    /**
     * @return array<string, mixed>
     */
    private function parseConnectionParams(string $databaseUrl): array
    {
        // Handle SQLite in-memory specially
        if (str_starts_with($databaseUrl, 'sqlite:///:memory:')) {
            return [
                'driver' => 'pdo_sqlite',
                'memory' => true,
            ];
        }

        // For other databases, use URL parameter
        return ['url' => $databaseUrl];
    }

    private function healthCheck(\Doctrine\DBAL\Connection $connection): void
    {
        // Verify the database is accessible by executing a simple query
        // This implicitly connects and validates the connection
        $connection->executeQuery('SELECT 1');
    }
}
