<?php

declare(strict_types=1);

namespace App\Framework\GraphQL\Subscriptions;

use App\Framework\GraphQL\Execution\ExecutionContext;
use App\Framework\GraphQL\Execution\QueryExecutor;
use App\Framework\GraphQL\Schema\FieldDefinition;
use App\Framework\GraphQL\Schema\Schema;
use App\Framework\Http\WebSocketConnectionInterface;

/**
 * Manages GraphQL subscriptions and publishes updates
 *
 * Coordinates between subscription resolvers and WebSocket connections
 */
final class SubscriptionManager
{
    public function __construct(
        private readonly SubscriptionRegistry $registry,
        private readonly Schema $schema,
        private readonly QueryExecutor $executor
    ) {
    }

    /**
     * Subscribe to a GraphQL subscription
     */
    public function subscribe(
        WebSocketConnectionInterface $connection,
        string $subscriptionName,
        array $variables = []
    ): SubscriptionId {
        $id = SubscriptionId::generate();

        $this->registry->register(
            $id,
            $connection,
            $subscriptionName,
            $variables
        );

        return $id;
    }

    /**
     * Unsubscribe from a subscription
     */
    public function unsubscribe(SubscriptionId $id): void
    {
        $this->registry->unregister($id);
    }

    /**
     * Publish data to all subscribers of a subscription
     *
     * @param array<string, mixed> $payload Data to publish
     * @param callable|null $filter Optional filter function to determine which subscribers receive the update
     */
    public function publish(string $subscriptionName, array $payload, ?callable $filter = null): int
    {
        $subscribers = $this->registry->getByName($subscriptionName);
        $published = 0;

        foreach ($subscribers as $subscriber) {
            // Apply filter if provided
            if ($filter !== null && ! $filter($subscriber['variables'], $payload)) {
                continue;
            }

            // Get subscription field from schema
            $subscriptionField = $this->schema->getSubscription($subscriptionName);

            if ($subscriptionField === null) {
                continue;
            }

            // Resolve subscription data
            $data = $this->resolveSubscriptionData(
                $subscriptionField,
                $payload,
                $subscriber['variables']
            );

            // Send to subscriber
            if ($subscriber['connection']->isConnected()) {
                $subscriber['connection']->sendJson([
                    'type' => 'next',
                    'id' => $subscriber['id']->toString(),
                    'payload' => [
                        'data' => [$subscriptionName => $data],
                    ],
                ]);

                $published++;
            } else {
                // Clean up disconnected subscription
                $this->unsubscribe($subscriber['id']);
            }
        }

        return $published;
    }

    /**
     * Resolve subscription data using the field resolver
     */
    private function resolveSubscriptionData(
        FieldDefinition $field,
        array $payload,
        array $variables
    ): mixed {
        $context = ExecutionContext::create();

        // Call resolver with payload as root value
        return $field->resolve($payload, $variables, $context);
    }

    /**
     * Clean up all subscriptions for a connection
     */
    public function cleanupConnection(WebSocketConnectionInterface $connection): void
    {
        $this->registry->removeByConnection($connection);
    }

    /**
     * Get subscription statistics
     */
    public function getStats(): array
    {
        return $this->registry->getStats();
    }
}
