<?php

namespace MongoDB\Tests\UnifiedSpecTests;

use MongoDB\Driver\Monitoring\CommandFailedEvent;
use MongoDB\Driver\Monitoring\CommandStartedEvent;
use MongoDB\Driver\Monitoring\CommandSubscriber;
use MongoDB\Driver\Monitoring\CommandSucceededEvent;
use MongoDB\Model\BSONArray;

use function array_filter;
use function array_flip;
use function get_class;
use function microtime;
use function MongoDB\Driver\Monitoring\addSubscriber;
use function MongoDB\Driver\Monitoring\removeSubscriber;
use function PHPUnit\Framework\assertArrayHasKey;
use function PHPUnit\Framework\assertIsObject;
use function PHPUnit\Framework\assertIsString;
use function PHPUnit\Framework\assertNotEmpty;
use function sprintf;

/**
 * EventCollector handles "storeEventsAsEntities" for client entities.
 *
 * Unlike EventObserver, this does not support ignoring command monitoring
 * events for specific commands. That said, internal/security commands that
 * bypass command monitoring will still be ignored.
 */
final class EventCollector implements CommandSubscriber
{
    /** @var array */
    private static $supportedEvents = [
        'PoolCreatedEvent' => null,
        'PoolReadyEvent' => null,
        'PoolClearedEvent' => null,
        'PoolClosedEvent' => null,
        'ConnectionCreatedEvent' => null,
        'ConnectionReadyEvent' => null,
        'ConnectionClosedEvent' => null,
        'ConnectionCheckOutStartedEvent' => null,
        'ConnectionCheckOutFailedEvent' => null,
        'ConnectionCheckedOutEvent' => null,
        'ConnectionCheckedInEvent' => null,
        'CommandStartedEvent' => CommandStartedEvent::class,
        'CommandSucceededEvent' => CommandSucceededEvent::class,
        'CommandFailedEvent' => CommandFailedEvent::class,
    ];

    /** @var string */
    private $clientId;

    /** @var Context */
    private $context;

    /** @var array */
    private $collectEvents = [];

    /** @var BSONArray */
    private $eventList;

    public function __construct(BSONArray $eventList, array $collectEvents, string $clientId, Context $context)
    {
        assertNotEmpty($collectEvents);

        foreach ($collectEvents as $event) {
            assertIsString($event);
            assertArrayHasKey($event, self::$supportedEvents);

            /* CMAP events are "supported" only in the sense that we recognize
             * them in the test format; however, PHPC does not implement
             * connection pooling so these events cannot be collected. */
            if (self::$supportedEvents[$event] !== null) {
                $this->collectEvents[self::$supportedEvents[$event]] = 1;
            }
        }

        $this->clientId = $clientId;
        $this->context = $context;
        $this->eventList = $eventList;
    }

    /**
     * @see https://php.net/manual/en/mongodb-driver-monitoring-commandsubscriber.commandfailed.php
     */
    public function commandFailed(CommandFailedEvent $event): void
    {
        $this->handleCommandMonitoringEvent($event);
    }

    /**
     * @see https://php.net/manual/en/mongodb-driver-monitoring-commandsubscriber.commandstarted.php
     */
    public function commandStarted(CommandStartedEvent $event): void
    {
        $this->handleCommandMonitoringEvent($event);
    }

    /**
     * @see https://php.net/manual/en/mongodb-driver-monitoring-commandsubscriber.commandsucceeded.php
     */
    public function commandSucceeded(CommandSucceededEvent $event): void
    {
        $this->handleCommandMonitoringEvent($event);
    }

    public function start(): void
    {
        addSubscriber($this);
    }

    public function stop(): void
    {
        removeSubscriber($this);
    }

    /** @param CommandStartedEvent|CommandSucceededEvent|CommandFailedEvent $event */
    private function handleCommandMonitoringEvent($event): void
    {
        assertIsObject($event);

        if (! $this->context->isActiveClient($this->clientId)) {
            return;
        }

        if (! isset($this->collectEvents[get_class($event)])) {
            return;
        }

        $log = [
            'name' => self::getEventName($event),
            'observedAt' => microtime(true),
            'commandName' => $event->getCommandName(),
            'connectionId' => self::getConnectionId($event),
            'requestId' => $event->getRequestId(),
            'operationId' => $event->getOperationId(),
        ];

        /* Note: CommandStartedEvent.command and CommandSucceededEvent.reply can
         * be omitted from logged events. */

        if ($event instanceof CommandStartedEvent) {
            $log['databaseName'] = $event->getDatabaseName();
        }

        if ($event instanceof CommandSucceededEvent) {
            $log['duration'] = $event->getDurationMicros();
        }

        if ($event instanceof CommandFailedEvent) {
            $log['failure'] = $event->getError()->getMessage();
            $log['duration'] = $event->getDurationMicros();
        }

        $this->eventList[] = $log;
    }

    /** @param CommandStartedEvent|CommandSucceededEvent|CommandFailedEvent $event */
    private static function getConnectionId($event): string
    {
        $server = $event->getServer();

        return sprintf('%s:%d', $server->getHost(), $server->getPort());
    }

    private static function getEventName(object $event): string
    {
        static $eventNamesByClass = null;

        if ($eventNamesByClass === null) {
            $eventNamesByClass = array_flip(array_filter(self::$supportedEvents));
        }

        return $eventNamesByClass[get_class($event)];
    }
}
