<?php

declare(strict_types=1);

namespace App\Framework\Database;

use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Database\Config\PoolConfig;
use App\Framework\Database\Driver\DriverConfig;
use App\Framework\Database\Exception\ConnectionFailedException;
use App\Framework\Database\Exception\DatabaseException;
use App\Framework\Database\Exception\PoolExhaustedException;
use App\Framework\Database\ValueObjects\SqlState;
use App\Framework\DateTime\Timer;
use App\Framework\Exception\Core\DatabaseErrorCode;
use App\Framework\Exception\ExceptionContext;

final class ConnectionPool
{
    /** @var array<string, ConnectionInterface> Connection storage by ID */
    private array $connections = [];

    /** @var \WeakMap<ConnectionInterface, ConnectionMetadata> Metadata mapped to connections */
    private \WeakMap $connectionMetadata;

    /** @var array<string, bool> Track which connections are in use */
    private array $inUse = [];

    private int $currentConnections = 0;

    private \DateTimeImmutable $lastHealthCheck;

    private int $totalConnectionsCreated = 0;

    private int $totalConnectionsDestroyed = 0;

    public function __construct(
        private readonly DriverConfig $driverConfig,
        private readonly PoolConfig $poolConfig,
        private readonly Timer $timer,
    ) {
        $this->connectionMetadata = new \WeakMap();
        $this->lastHealthCheck = new \DateTimeImmutable();

        // Initialize min connections if warmup is enabled
        if ($this->poolConfig->enableWarmup) {
            $this->initializeMinConnections();
        }
    }

    public function getConnection(): PooledConnection
    {
        // Perform periodic health checks
        $this->performHealthChecksIfNeeded();

        // Clean up idle connections
        $this->cleanupIdleConnections();

        // Lazy initialization - create min connections on first use
        if (empty($this->connections) && ! $this->poolConfig->enableWarmup) {
            $this->initializeMinConnections();
        }

        // Try to find a healthy, available connection
        $connection = $this->findAvailableHealthyConnection();
        if ($connection !== null) {
            return $connection;
        }

        // Create new connection if under limit
        if ($this->currentConnections < $this->poolConfig->maxConnections) {
            return $this->createNewConnection();
        }

        // Pool exhausted - throw dedicated exception
        throw PoolExhaustedException::noConnectionsAvailable(
            currentConnections: $this->currentConnections,
            maxConnections: $this->poolConfig->maxConnections,
            connectionsInUse: count($this->inUse)
        );
    }

    private function findAvailableHealthyConnection(): ?PooledConnection
    {
        foreach ($this->connections as $id => $connection) {
            if (isset($this->inUse[$id])) {
                continue; // Connection is in use
            }

            $metadata = $this->connectionMetadata[$connection] ?? null;
            if ($metadata === null || ! $metadata->isHealthy) {
                continue; // Connection is unhealthy
            }

            // Mark as in use and return
            $this->inUse[$id] = true;

            return new PooledConnection($connection, $this, $id);
        }

        return null;
    }

    private function createNewConnection(): PooledConnection
    {
        $retries = 0;
        $maxRetries = $this->poolConfig->maxRetries;

        while ($retries < $maxRetries) {
            try {
                $connection = DatabaseFactory::createConnection($this->driverConfig);
                $id = uniqid('conn_');

                // Store connection
                $this->connections[$id] = $connection;
                $this->inUse[$id] = true;
                $this->currentConnections++;
                $this->totalConnectionsCreated++;

                // Create metadata using WeakMap
                $now = new \DateTimeImmutable();
                $metadata = new ConnectionMetadata(
                    id: $id,
                    createdAt: $now,
                    lastUsedAt: $now,
                    lastHealthCheckAt: $now,
                );
                $this->connectionMetadata[$connection] = $metadata;

                return new PooledConnection($connection, $this, $id);

            } catch (DatabaseException $e) {
                // If it's already a DatabaseException (with SQLSTATE), just rethrow on max retries
                $retries++;
                if ($retries >= $maxRetries) {
                    throw $e;
                }

                // Exponential backoff
                $backoffMicroseconds = min(100000 * (2 ** $retries), 1000000); // Max 1 second
                $this->timer->sleep(Duration::fromMicroseconds($backoffMicroseconds));
            } catch (\Exception $e) {
                // Generic exception - wrap in PoolExhaustedException
                $retries++;
                if ($retries >= $maxRetries) {
                    throw PoolExhaustedException::cannotCreateConnection(
                        currentConnections: $this->currentConnections,
                        maxConnections: $this->poolConfig->maxConnections,
                        reason: "Failed after {$maxRetries} retries: " . $e->getMessage(),
                        previous: $e
                    );
                }

                // Exponential backoff
                $backoffMicroseconds = min(100000 * (2 ** $retries), 1000000); // Max 1 second
                $this->timer->sleep(Duration::fromMicroseconds($backoffMicroseconds));
            }
        }

        // Should never reach here, but just in case
        throw PoolExhaustedException::cannotCreateConnection(
            currentConnections: $this->currentConnections,
            maxConnections: $this->poolConfig->maxConnections,
            reason: 'Exhausted retry attempts without successful connection'
        );
    }

    public function releaseConnection(string $id): void
    {
        unset($this->inUse[$id]);

        // Update metadata if connection exists
        if (isset($this->connections[$id])) {
            $connection = $this->connections[$id];
            $metadata = $this->connectionMetadata[$connection] ?? null;
            if ($metadata !== null) {
                $metadata->lastUsedAt = new \DateTimeImmutable();
            }
        }
    }

    public function closeConnection(string $id): void
    {
        if (isset($this->connections[$id])) {
            // The WeakMap will automatically clean up when connection is garbage collected
            unset($this->connections[$id]);
            $this->totalConnectionsDestroyed++;
        }
        unset($this->inUse[$id]);
        $this->currentConnections--;
    }

    /**
     * Record query execution for metrics
     */
    public function recordQuery(string $connectionId, float $executionTimeMs, bool $successful = true): void
    {
        if (! $this->poolConfig->enableMetrics) {
            return;
        }

        if (! isset($this->connections[$connectionId])) {
            return;
        }

        $connection = $this->connections[$connectionId];
        $metadata = $this->connectionMetadata[$connection] ?? null;
        if ($metadata !== null) {
            $metadata->recordQuery($executionTimeMs, $successful);
        }
    }

    private function performHealthChecksIfNeeded(): void
    {
        if (! $this->poolConfig->enableHealthChecks) {
            return;
        }

        $now = new \DateTimeImmutable();
        $timeSinceLastCheck = $now->getTimestamp() - $this->lastHealthCheck->getTimestamp();

        if ($timeSinceLastCheck < $this->poolConfig->healthCheckIntervalSeconds) {
            return;
        }

        $this->lastHealthCheck = $now;
        $this->performHealthChecks();
    }

    private function performHealthChecks(): void
    {
        foreach ($this->connections as $id => $connection) {
            // Skip connections that are currently in use
            if (isset($this->inUse[$id])) {
                continue;
            }

            $metadata = $this->connectionMetadata[$connection] ?? null;
            if ($metadata === null) {
                continue;
            }

            // Check if health check is needed
            if (! $metadata->needsHealthCheck($this->poolConfig->healthCheckIntervalSeconds)) {
                continue;
            }

            $isHealthy = $this->checkConnectionHealth($connection);
            $metadata->updateHealthStatus($isHealthy);

            // Close unhealthy connections
            if (! $isHealthy) {
                $this->closeConnection($id);
            }
        }
    }

    private function checkConnectionHealth(ConnectionInterface $connection): bool
    {
        try {
            // Simple health check query
            $connection->queryScalar('SELECT 1');

            return true;
        } catch (\Exception) {
            return false;
        }
    }

    private function cleanupIdleConnections(): void
    {
        if ($this->currentConnections <= $this->poolConfig->minConnections) {
            return; // Don't go below minimum connections
        }

        $connectionsToClose = [];

        foreach ($this->connections as $id => $connection) {
            // Skip connections that are currently in use
            if (isset($this->inUse[$id])) {
                continue;
            }

            $metadata = $this->connectionMetadata[$connection] ?? null;
            if ($metadata === null) {
                continue;
            }

            if ($metadata->isIdle($this->poolConfig->maxIdleTimeSeconds)) {
                $connectionsToClose[] = $id;
            }
        }

        // Close idle connections, but maintain minimum pool size
        $maxToClose = $this->currentConnections - $this->poolConfig->minConnections;
        $toClose = array_slice($connectionsToClose, 0, $maxToClose);

        foreach ($toClose as $id) {
            $this->closeConnection($id);
        }
    }

    public function getStats(): array
    {
        $healthyConnections = 0;
        $unhealthyConnections = 0;
        $totalQueries = 0;
        $totalFailedQueries = 0;
        $connectionDetails = [];

        // Collect detailed metrics from WeakMap
        foreach ($this->connections as $id => $connection) {
            $metadata = $this->connectionMetadata[$connection] ?? null;
            if ($metadata !== null) {
                if ($metadata->isHealthy) {
                    $healthyConnections++;
                } else {
                    $unhealthyConnections++;
                }

                $totalQueries += $metadata->totalQueries;
                $totalFailedQueries += $metadata->failedQueries;

                if ($this->poolConfig->enableMetrics) {
                    $connectionDetails[$id] = $metadata->getStats();
                }
            }
        }

        $stats = [
            'pool_status' => [
                'total_connections' => $this->currentConnections,
                'active_connections' => count($this->inUse),
                'free_connections' => $this->currentConnections - count($this->inUse),
                'healthy_connections' => $healthyConnections,
                'unhealthy_connections' => $unhealthyConnections,
                'max_connections' => $this->poolConfig->maxConnections,
                'min_connections' => $this->poolConfig->minConnections,
            ],
            'lifetime_stats' => [
                'total_connections_created' => $this->totalConnectionsCreated,
                'total_connections_destroyed' => $this->totalConnectionsDestroyed,
                'total_queries' => $totalQueries,
                'total_failed_queries' => $totalFailedQueries,
                'failure_rate' => $totalQueries > 0 ? $totalFailedQueries / $totalQueries : 0,
            ],
            'configuration' => [
                'health_checks_enabled' => $this->poolConfig->enableHealthChecks,
                'metrics_enabled' => $this->poolConfig->enableMetrics,
                'warmup_enabled' => $this->poolConfig->enableWarmup,
                'health_check_interval_seconds' => $this->poolConfig->healthCheckIntervalSeconds,
                'max_idle_time_seconds' => $this->poolConfig->maxIdleTimeSeconds,
                'connection_timeout_seconds' => $this->poolConfig->connectionTimeoutSeconds,
                'max_retries' => $this->poolConfig->maxRetries,
            ],
        ];

        if ($this->poolConfig->enableMetrics && ! empty($connectionDetails)) {
            $stats['connection_details'] = $connectionDetails;
        }

        return $stats;
    }

    private function initializeMinConnections(): void
    {
        for ($i = 0; $i < $this->poolConfig->minConnections; $i++) {
            try {
                $connection = DatabaseFactory::createConnection($this->driverConfig);
                $id = uniqid('conn_');
                $this->connections[$id] = $connection;
                $this->currentConnections++;
                $this->totalConnectionsCreated++;

                // Create metadata using WeakMap
                $now = new \DateTimeImmutable();
                $metadata = new ConnectionMetadata(
                    id: $id,
                    createdAt: $now,
                    lastUsedAt: $now,
                    lastHealthCheckAt: $now,
                );
                $this->connectionMetadata[$connection] = $metadata;

            } catch (\Exception $e) {
                // Silent failure during warmup - connection will be created on-demand
                // This prevents bootstrap failures due to temporary database issues
            }
        }
    }

    /**
     * Force health check on all connections
     */
    public function forceHealthCheck(): void
    {
        $this->performHealthChecks();
    }

    /**
     * Get connection metadata for debugging
     */
    public function getConnectionMetadata(string $connectionId): ?array
    {
        if (! isset($this->connections[$connectionId])) {
            return null;
        }

        $connection = $this->connections[$connectionId];
        $metadata = $this->connectionMetadata[$connection] ?? null;

        return $metadata?->getStats();
    }

    public function __destruct()
    {
        // WeakMap will automatically clean up metadata when connections are destroyed
        $this->connections = [];
        $this->inUse = [];
        $this->currentConnections = 0;
    }
}
