<?php

declare(strict_types=1);

namespace App\Framework\Sse;

use App\Framework\Router\Result\SseEvent;
use App\Framework\Sse\ValueObjects\ConnectionId;
use App\Framework\Sse\ValueObjects\SseChannel;
use App\Framework\Sse\ValueObjects\SseConnection;

/**
 * SSE Connection Pool
 *
 * Manages active SSE connections and handles event broadcasting.
 *
 * Features:
 * - Connection lifecycle management
 * - Channel-based event routing
 * - Dead connection cleanup
 * - Connection statistics
 */
final class SseConnectionPool
{
    /**
     * @var array<string, SseConnection> Active connections indexed by connection ID
     */
    private array $connections = [];

    /**
     * @var int Maximum allowed connections
     */
    private int $maxConnections = 10000;

    /**
     * @var int Maximum connections per user
     */
    private int $maxConnectionsPerUser = 5;

    /**
     * Add a new connection to the pool
     */
    public function add(SseConnection $connection): void
    {
        // Check max connections limit
        if (count($this->connections) >= $this->maxConnections) {
            throw new \RuntimeException('Maximum connection limit reached');
        }

        // Check per-user connection limit
        if ($connection->isAuthenticated()) {
            $userConnections = $this->getConnectionsByUserId($connection->userId);

            if (count($userConnections) >= $this->maxConnectionsPerUser) {
                throw new \RuntimeException('Maximum connections per user limit reached');
            }
        }

        $this->connections[$connection->connectionId->toString()] = $connection;
    }

    /**
     * Remove a connection from the pool
     */
    public function remove(ConnectionId $connectionId): void
    {
        unset($this->connections[$connectionId->toString()]);
    }

    /**
     * Get a connection by ID
     */
    public function get(ConnectionId $connectionId): ?SseConnection
    {
        return $this->connections[$connectionId->toString()] ?? null;
    }

    /**
     * Check if connection exists
     */
    public function has(ConnectionId $connectionId): bool
    {
        return isset($this->connections[$connectionId->toString()]);
    }

    /**
     * Broadcast an event to all connections subscribed to a channel
     */
    public function broadcast(SseEvent $event, SseChannel $channel): int
    {
        $connections = $this->getSubscribedConnections($channel);
        $successCount = 0;

        foreach ($connections as $connection) {
            if ($this->sendToConnection($connection, $event)) {
                $successCount++;
            }
        }

        return $successCount;
    }

    /**
     * Send event to specific connection
     */
    private function sendToConnection(SseConnection $connection, SseEvent $event): bool
    {
        if (! $connection->isAlive()) {
            // Remove dead connection
            $this->remove($connection->connectionId);

            return false;
        }

        try {
            $connection->stream->sendEvent($event);

            return true;
        } catch (\Throwable $e) {
            // Connection write failed - remove it
            $this->remove($connection->connectionId);

            return false;
        }
    }

    /**
     * Get all connections subscribed to a channel
     *
     * @return array<SseConnection>
     */
    private function getSubscribedConnections(SseChannel $channel): array
    {
        return array_filter(
            $this->connections,
            fn (SseConnection $conn) => $conn->isSubscribedTo($channel)
        );
    }

    /**
     * Get all connections for a specific user
     *
     * @return array<SseConnection>
     */
    private function getConnectionsByUserId(?string $userId): array
    {
        if ($userId === null) {
            return [];
        }

        return array_filter(
            $this->connections,
            fn (SseConnection $conn) => $conn->userId === $userId
        );
    }

    /**
     * Send heartbeat to all connections
     */
    public function sendHeartbeats(int $heartbeatIntervalSeconds = 30): int
    {
        $sentCount = 0;

        foreach ($this->connections as $connection) {
            if ($connection->needsHeartbeat($heartbeatIntervalSeconds)) {
                try {
                    $connection->stream->sendHeartbeat();

                    // Update heartbeat timestamp
                    $updatedConnection = $connection->withHeartbeat();
                    $this->connections[$connection->connectionId->toString()] = $updatedConnection;

                    $sentCount++;
                } catch (\Throwable $e) {
                    // Connection failed - will be removed on next cleanup
                }
            }
        }

        return $sentCount;
    }

    /**
     * Remove all dead connections
     */
    public function cleanup(): int
    {
        $removedCount = 0;

        foreach ($this->connections as $connection) {
            if (! $connection->isAlive()) {
                $this->remove($connection->connectionId);
                $removedCount++;
            }
        }

        return $removedCount;
    }

    /**
     * Get all active connections
     *
     * @return array<SseConnection>
     */
    public function all(): array
    {
        return array_values($this->connections);
    }

    /**
     * Get connection count
     */
    public function count(): int
    {
        return count($this->connections);
    }

    /**
     * Get pool statistics
     */
    public function getStats(): array
    {
        $channelDistribution = [];
        $userDistribution = [];
        $totalAge = 0;

        foreach ($this->connections as $connection) {
            // Channel distribution
            foreach ($connection->getChannelNames() as $channelName) {
                $channelDistribution[$channelName] = ($channelDistribution[$channelName] ?? 0) + 1;
            }

            // User distribution
            if ($connection->userId !== null) {
                $userDistribution[$connection->userId] = ($userDistribution[$connection->userId] ?? 0) + 1;
            }

            // Average age
            $totalAge += $connection->getAge();
        }

        $totalConnections = count($this->connections);
        $avgAge = $totalConnections > 0 ? (int) ($totalAge / $totalConnections) : 0;

        return [
            'total_connections' => $totalConnections,
            'authenticated_connections' => count(array_filter($this->connections, fn ($c) => $c->isAuthenticated())),
            'anonymous_connections' => count(array_filter($this->connections, fn ($c) => ! $c->isAuthenticated())),
            'average_connection_age_seconds' => $avgAge,
            'connections_by_channel' => $channelDistribution,
            'connections_by_user' => count($userDistribution),
            'max_connections' => $this->maxConnections,
            'max_connections_per_user' => $this->maxConnectionsPerUser,
        ];
    }

    /**
     * Get connections subscribed to a channel (public method)
     *
     * @return array<SseConnection>
     */
    public function getConnectionsForChannel(SseChannel $channel): array
    {
        return $this->getSubscribedConnections($channel);
    }

    /**
     * Clear all connections (useful for testing)
     */
    public function clear(): void
    {
        $this->connections = [];
    }
}
