<?php

namespace onespace\core\components;

use Yii;
use yii\queue\Queue;
use yii\queue\interfaces\DelayedCountInterface;
use yii\queue\interfaces\DoneCountInterface;
use yii\queue\interfaces\ReservedCountInterface;
use yii\queue\interfaces\StatisticsProviderInterface;
use yii\queue\interfaces\WaitingCountInterface;

/**
 * Discovers every configured yii\queue\Queue component and records its
 * depth (waiting/delayed/reserved/done) as a single Prometheus gauge.
 *
 * Currently, our systems are byult such that queues and channels have a 1:1 mapping
 * No channel labels are exposed, for this reason.
 *
 * Discovery is definition-aware: components already instantiated (e.g. any
 * queue listed in the app's `bootstrap` array) are type-checked directly
 * at zero extra cost. Components still sitting as a raw config
 * (array/string definition) are class-matched from the definition alone,
 * so unrelated components (db, cache, mailer, etc.) are never
 * force-instantiated just to be ruled out.
 *
 * Statistics access goes through StatisticsProviderInterface::getStatisticsProvider(),
 * per the yii2-queue contract - the queue itself does not expose the count
 * getters directly. The returned provider is only guaranteed to implement
 * the empty `StatisticsInterface` marker; each count is gated behind its
 * own sub-interface (WaitingCountInterface, DelayedCountInterface,
 * ReservedCountInterface, DoneCountInterface) so a driver that only
 * supports some of the four states doesn't blow up on the ones it can't
 * answer.
 *
 * Opt-in only - register it via the metrics module's `collectors` config:
 *
 * ```php
 * 'modules' => [
 *     'metrics' => [
 *         'class' => \onespace\core\modules\metrics\Module::class,
 *         'collectors' => [\onespace\core\components\QueueMetricsCollector::class],
 *     ],
 * ],
 * ```
 *
 * @psalm-api
 */
class QueueMetricsCollector implements MetricsCollectorInterface
{
    #[\Override]
    public function collect(MetricsComponent $metrics): void
    {
        $gauge = $metrics->gauge(
            'queue_jobs',
            'Number of queue jobs by state',
            ['queue', 'state']
        );

        foreach ($this->discoverQueues() as $id => $queue) {
            if (!$queue instanceof StatisticsProviderInterface) {
                continue; // driver doesn't expose stats at all - skip rather than guess
            }

            $stats = $queue->getStatisticsProvider();

            $counters = [
                'waiting'  => $stats instanceof WaitingCountInterface ? $stats->getWaitingCount() : null,
                'delayed'  => $stats instanceof DelayedCountInterface ? $stats->getDelayedCount() : null,
                'reserved' => $stats instanceof ReservedCountInterface ? $stats->getReservedCount() : null,
                'done'     => $stats instanceof DoneCountInterface ? $stats->getDoneCount() : null,
            ];

            foreach ($counters as $state => $value) {
                if ($value === null) {
                    continue; // this driver's provider doesn't support this state
                }
                $gauge->set((float) $value, [
                    'queue' => $id,
                    'state' => $state,
                ]);
            }
        }
    }

    /**
     * @return array<string, Queue> component id => queue instance
     */
    private function discoverQueues(): array
    {
        $queues = [];

        foreach (Yii::$app->getComponents(false) as $id => $definition) {
            if ($definition instanceof Queue) {
                // Already instantiated - e.g. every queue in this app is
                // eagerly loaded via `bootstrap`. No extra cost to check.
                $queues[$id] = $definition;
                continue;
            }

            if (is_object($definition)) {
                continue; // instantiated, but not a queue
            }

            $class = match (true) {
                is_string($definition) => $definition,
                is_array($definition) => $definition['class'] ?? null,
                default => null, // e.g. Closure definitions - can't cheaply inspect; skip
            };

            if ($class === null || !is_subclass_of($class, Queue::class)) {
                continue;
            }

            // Only pay the instantiation cost for components already
            // confirmed, by class name alone, to be queues.
            $queues[$id] = Yii::$app->get($id);
        }

        return $queues;
    }
}