<?php

namespace PortalPeople\queues\services;

use Yii;
use yii\base\Component;
use yii\db\Connection;
use yii\db\Query;

class QueueMonitorService extends Component
{
    public $supervisorQueues = ['queue', 'mailqueue', 'dfqueue', 'netsuitequeue'];
    public $heartbeatDirectory = '@runtime/queue-heartbeats';
    public $pendingWarning = 25;
    public $pendingCritical = 100;
    public $staleReserveSeconds = 1200;
    public $heartbeatFreshFor = 300;
    public $defaultTableName = '{{%queue}}';

    public function getDashboardData(): array
    {
        $discoveredComponents = $this->discoverQueueComponents();
        $settingsDefinitions = $this->settingsQueueDefinitions();

        $rows = [];
        $seen = [];

        foreach ($discoveredComponents as $componentId => $componentDefinition) {
            $settings = $settingsDefinitions[$componentId] ?? ($settingsDefinitions[$componentDefinition['channel']] ?? []);
            $rows[] = $this->buildQueueStatus($componentId, $componentDefinition, $settings);
            $seen[$componentId] = true;
        }

        foreach ($settingsDefinitions as $componentId => $settings) {
            if (isset($seen[$componentId])) {
                continue;
            }

            $channel = (string)($settings['channel'] ?? $componentId);
            $componentDefinition = [
                'component' => $componentId,
                'channel' => $channel,
                'tableName' => (string)($settings['tableName'] ?? $this->defaultTableName),
                'ttr' => isset($settings['ttr']) ? (int)$settings['ttr'] : null,
                'class' => (string)($settings['class'] ?? ''),
            ];

            $row = $this->buildQueueStatus($componentId, $componentDefinition, $settings);
            $row['componentMissing'] = true;
            $row['notes'][] = 'Queue exists in settings but no matching Yii component is registered.';
            $row['status'] = $this->promoteStatus($row['status'], 'warning');
            $rows[] = $row;
        }

        usort($rows, function (array $left, array $right): int {
            $leftRank = $this->statusRank($left['status']);
            $rightRank = $this->statusRank($right['status']);

            if ($leftRank !== $rightRank) {
                return $rightRank <=> $leftRank;
            }

            return strcmp($left['component'], $right['component']);
        });

        $summary = [
            'total' => count($rows),
            'healthy' => 0,
            'warning' => 0,
            'critical' => 0,
            'unknown' => 0,
        ];

        foreach ($rows as $row) {
            $status = $row['status'];
            if (!isset($summary[$status])) {
                $summary[$status] = 0;
            }
            $summary[$status]++;
        }

        return [
            'generatedAt' => time(),
            'summary' => $summary,
            'queues' => $rows,
        ];
    }

    protected function discoverQueueComponents(): array
    {
        $definitions = Yii::$app->getComponents();
        $queues = [];

        foreach ($definitions as $componentId => $definition) {
            if (!is_string($componentId)) {
                continue;
            }

            $normalized = $this->normalizeComponentDefinition($componentId, $definition);
            if ($normalized === null) {
                continue;
            }

            $queues[$componentId] = $normalized;
        }

        ksort($queues);

        return $queues;
    }

    protected function normalizeComponentDefinition(string $componentId, $definition): ?array
    {
        $className = null;
        $channel = $componentId === 'queue' ? 'default' : $componentId;
        $tableName = $this->defaultTableName;
        $ttr = null;

        if (is_array($definition)) {
            $className = isset($definition['class']) && is_string($definition['class']) ? $definition['class'] : null;
            if (isset($definition['channel']) && $definition['channel'] !== '') {
                $channel = (string)$definition['channel'];
            }
            if (isset($definition['tableName']) && $definition['tableName'] !== '') {
                $tableName = (string)$definition['tableName'];
            }
            if (isset($definition['ttr']) && is_numeric($definition['ttr'])) {
                $ttr = (int)$definition['ttr'];
            }
        } elseif (is_object($definition)) {
            $className = get_class($definition);
            $channel = (string)$this->objectProperty($definition, 'channel', $channel);
            $tableName = (string)$this->objectProperty($definition, 'tableName', $tableName);

            $objectTtr = $this->objectProperty($definition, 'ttr', null);
            if (is_numeric($objectTtr)) {
                $ttr = (int)$objectTtr;
            }
        }

        if (!$this->isQueueComponentId($componentId) && !$this->isQueueClass($className)) {
            return null;
        }

        return [
            'component' => $componentId,
            'class' => $className,
            'channel' => $channel,
            'tableName' => $tableName,
            'ttr' => $ttr,
        ];
    }

    protected function buildQueueStatus(string $componentId, array $component, array $settings): array
    {
        $channel = (string)($settings['channel'] ?? $component['channel'] ?? $componentId);
        $tableName = (string)($settings['tableName'] ?? $component['tableName'] ?? $this->defaultTableName);

        $requiresSupervisor = array_key_exists('supervisor', $settings)
            ? $this->boolValue($settings['supervisor'])
            : (in_array($componentId, $this->supervisorQueues, true) || in_array($channel, $this->supervisorQueues, true));

        $expectedProcesses = isset($settings['processes']) && is_numeric($settings['processes'])
            ? (int)$settings['processes']
            : (isset($settings['procs']) && is_numeric($settings['procs']) ? (int)$settings['procs'] : ($requiresSupervisor ? 1 : 0));

        $pendingWarning = isset($settings['pending_warning']) && is_numeric($settings['pending_warning'])
            ? (int)$settings['pending_warning']
            : (int)$this->pendingWarning;

        $pendingCritical = isset($settings['pending_critical']) && is_numeric($settings['pending_critical'])
            ? (int)$settings['pending_critical']
            : (int)$this->pendingCritical;

        if ($pendingCritical < $pendingWarning) {
            $pendingCritical = $pendingWarning;
        }

        $staleReserveSeconds = isset($settings['stale_seconds']) && is_numeric($settings['stale_seconds'])
            ? (int)$settings['stale_seconds']
            : (int)$this->staleReserveSeconds;

        $heartbeatFreshFor = isset($settings['heartbeat_fresh_for']) && is_numeric($settings['heartbeat_fresh_for'])
            ? (int)$settings['heartbeat_fresh_for']
            : (int)$this->heartbeatFreshFor;

        $metrics = $this->getQueueMetrics($tableName, $channel, $staleReserveSeconds);

        $heartbeatFile = $this->resolveHeartbeatPath($componentId, $settings);
        $heartbeatAt = $this->readHeartbeatTimestamp($heartbeatFile);
        $heartbeatAge = $heartbeatAt !== null ? max(0, time() - $heartbeatAt) : null;

        $supervisorConfig = $this->resolveSupervisorConfig($componentId);

        $status = 'healthy';
        $notes = [];

        if (!$metrics['available']) {
            $status = 'unknown';
            $notes[] = $metrics['error'] ?: 'Queue table could not be inspected.';
        } else {
            if ($metrics['pending'] >= $pendingCritical) {
                $status = 'critical';
                $notes[] = 'Pending jobs are above critical threshold.';
            } elseif ($metrics['pending'] >= $pendingWarning) {
                $status = $this->promoteStatus($status, 'warning');
                $notes[] = 'Pending jobs are above warning threshold.';
            }

            if ($requiresSupervisor && $metrics['pending'] > 0 && $metrics['running'] === 0) {
                $status = $this->promoteStatus($status, 'warning');
                $notes[] = 'Pending jobs detected but no running workers found.';
            }

            if ($metrics['staleRunning'] > 0) {
                $status = $this->promoteStatus($status, 'warning');
                $notes[] = 'Stale running jobs detected (reserved for too long).';
            }
        }

        if ($requiresSupervisor && !$supervisorConfig['configured']) {
            $status = $this->promoteStatus($status, 'warning');
            $notes[] = 'No supervisor config entry detected for this queue.';
        }

        if ($heartbeatAt !== null && $heartbeatAge !== null && $heartbeatAge > $heartbeatFreshFor) {
            $status = $this->promoteStatus($status, 'warning');
            $notes[] = 'Heartbeat file is stale.';
        } elseif ($requiresSupervisor && $heartbeatAt === null) {
            $notes[] = 'No heartbeat file found (optional).';
        }

        if (empty($notes) && $metrics['available']) {
            $notes[] = 'Queue appears healthy.';
        }

        return [
            'component' => $componentId,
            'class' => $component['class'] ?? null,
            'channel' => $channel,
            'tableName' => $tableName,
            'requiresSupervisor' => $requiresSupervisor,
            'expectedProcesses' => max(0, $expectedProcesses),
            'supervisorConfigured' => $supervisorConfig['configured'],
            'supervisorConfigFiles' => [
                'global' => $supervisorConfig['globalFile'],
                'perQueue' => $supervisorConfig['perQueueFile'],
            ],
            'pending' => $metrics['pending'],
            'running' => $metrics['running'],
            'completed24h' => $metrics['completed24h'],
            'staleRunning' => $metrics['staleRunning'],
            'oldestPendingAt' => $metrics['oldestPendingAt'],
            'oldestPendingAge' => $metrics['oldestPendingAge'],
            'heartbeatFile' => $heartbeatFile,
            'heartbeatAt' => $heartbeatAt,
            'heartbeatAge' => $heartbeatAge,
            'status' => $status,
            'notes' => $notes,
            'commands' => $this->commandHints($componentId),
            'componentMissing' => false,
        ];
    }

    protected function getQueueMetrics(string $tableName, string $channel, int $staleReserveSeconds): array
    {
        if (!Yii::$app->has('db')) {
            return [
                'available' => false,
                'error' => 'DB component is not configured.',
                'pending' => 0,
                'running' => 0,
                'completed24h' => 0,
                'staleRunning' => 0,
                'oldestPendingAt' => null,
                'oldestPendingAge' => null,
            ];
        }

        $database = Yii::$app->get('db');
        if (!$database instanceof Connection) {
            return [
                'available' => false,
                'error' => 'DB component is not a valid yii\\db\\Connection.',
                'pending' => 0,
                'running' => 0,
                'completed24h' => 0,
                'staleRunning' => 0,
                'oldestPendingAt' => null,
                'oldestPendingAge' => null,
            ];
        }

        $rawTableName = $database->schema->getRawTableName($tableName);
        $schema = $database->schema->getTableSchema($rawTableName, true);

        if ($schema === null) {
            return [
                'available' => false,
                'error' => "Queue table '{$rawTableName}' was not found.",
                'pending' => 0,
                'running' => 0,
                'completed24h' => 0,
                'staleRunning' => 0,
                'oldestPendingAt' => null,
                'oldestPendingAge' => null,
            ];
        }

        $now = time();

        try {
            $pending = (int)(new Query())
                ->from($rawTableName)
                ->where(['channel' => $channel, 'reserved_at' => null, 'done_at' => null])
                ->count('*', $database);

            $running = (int)(new Query())
                ->from($rawTableName)
                ->where(['channel' => $channel, 'done_at' => null])
                ->andWhere(['not', ['reserved_at' => null]])
                ->count('*', $database);

            $completed24h = (int)(new Query())
                ->from($rawTableName)
                ->where(['channel' => $channel])
                ->andWhere(['not', ['done_at' => null]])
                ->andWhere(['>', 'done_at', $now - 86400])
                ->count('*', $database);

            $staleRunning = (int)(new Query())
                ->from($rawTableName)
                ->where(['channel' => $channel, 'done_at' => null])
                ->andWhere(['not', ['reserved_at' => null]])
                ->andWhere(['<', 'reserved_at', $now - $staleReserveSeconds])
                ->count('*', $database);

            $oldestPendingAt = (new Query())
                ->from($rawTableName)
                ->where(['channel' => $channel, 'reserved_at' => null, 'done_at' => null])
                ->min('pushed_at', $database);

            $oldestPendingAt = is_numeric($oldestPendingAt) ? (int)$oldestPendingAt : null;
            $oldestPendingAge = $oldestPendingAt !== null ? max(0, $now - $oldestPendingAt) : null;

            return [
                'available' => true,
                'error' => null,
                'pending' => $pending,
                'running' => $running,
                'completed24h' => $completed24h,
                'staleRunning' => $staleRunning,
                'oldestPendingAt' => $oldestPendingAt,
                'oldestPendingAge' => $oldestPendingAge,
            ];
        } catch (\Throwable $exception) {
            return [
                'available' => false,
                'error' => $exception->getMessage(),
                'pending' => 0,
                'running' => 0,
                'completed24h' => 0,
                'staleRunning' => 0,
                'oldestPendingAt' => null,
                'oldestPendingAge' => null,
            ];
        }
    }

    protected function settingsQueueDefinitions(): array
    {
        $raw = Yii::$app->params['settings']['queues'] ?? [];
        if (!is_array($raw)) {
            return [];
        }

        $normalized = [];
        $isList = array_keys($raw) === range(0, count($raw) - 1);

        if ($isList) {
            foreach ($raw as $entry) {
                if (is_string($entry) && $entry !== '') {
                    $normalized[$entry] = ['supervisor' => true];
                    continue;
                }

                if (!is_array($entry)) {
                    continue;
                }

                $name = $entry['name'] ?? $entry['component'] ?? $entry['id'] ?? $entry['channel'] ?? null;
                if (!is_string($name) || $name === '') {
                    continue;
                }

                unset($entry['name'], $entry['component'], $entry['id']);
                $normalized[$name] = $entry;
            }

            ksort($normalized);
            return $normalized;
        }

        foreach ($raw as $name => $entry) {
            if (!is_string($name) || $name === '') {
                continue;
            }

            if (is_array($entry)) {
                $normalized[$name] = $entry;
                continue;
            }

            if (is_string($entry) && $entry !== '') {
                $normalized[$name] = ['channel' => $entry];
                continue;
            }

            if (is_bool($entry) || is_numeric($entry)) {
                $normalized[$name] = ['supervisor' => $this->boolValue($entry)];
            }
        }

        ksort($normalized);

        return $normalized;
    }

    protected function resolveHeartbeatPath(string $componentId, array $settings): string
    {
        $heartbeatFile = $settings['heartbeat_file'] ?? $settings['heartbeatFile'] ?? null;
        if (is_string($heartbeatFile) && $heartbeatFile !== '') {
            $resolved = Yii::getAlias($heartbeatFile, false);
            return $resolved !== false ? $resolved : $heartbeatFile;
        }

        $basePath = Yii::getAlias($this->heartbeatDirectory, false);
        if ($basePath === false) {
            $basePath = Yii::getAlias('@runtime') . '/queue-heartbeats';
        }

        return rtrim($basePath, '/') . '/' . $componentId . '.heartbeat';
    }

    protected function readHeartbeatTimestamp(string $heartbeatFile): ?int
    {
        if ($heartbeatFile === '' || !is_file($heartbeatFile)) {
            return null;
        }

        $mtime = @filemtime($heartbeatFile);
        if ($mtime === false || $mtime <= 0) {
            return null;
        }

        return (int)$mtime;
    }

    protected function resolveSupervisorConfig(string $componentId): array
    {
        $projectRoot = $this->projectRootPath();

        $globalFile = $projectRoot . '/console/config/supervisor.conf';
        $perQueueFile = $projectRoot . '/console/config/supervisor/' . $componentId . '.conf';

        $configured = is_file($perQueueFile) || $this->globalSupervisorConfigContainsQueue($globalFile, $componentId);

        return [
            'configured' => $configured,
            'globalFile' => $globalFile,
            'perQueueFile' => $perQueueFile,
        ];
    }

    protected function globalSupervisorConfigContainsQueue(string $globalFile, string $componentId): bool
    {
        if (!is_file($globalFile)) {
            return false;
        }

        $contents = @file_get_contents($globalFile);
        if (!is_string($contents) || $contents === '') {
            return false;
        }

        $pattern = '/yii\s+' . preg_quote($componentId, '/') . '\/(listen|run)\b/i';

        return (bool)preg_match($pattern, $contents);
    }

    protected function commandHints(string $componentId): array
    {
        $majorMinor = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;
        $pleskPhp = '/opt/plesk/php/' . $majorMinor . '/bin/php';
        $phpBin = is_executable($pleskPhp) ? $pleskPhp : 'php';

        $projectRoot = $this->projectRootPath();

        return [
            'listen' => $phpBin . ' yii ' . $componentId . '/listen --verbose=1 --color=0',
            'run' => $phpBin . ' yii ' . $componentId . '/run --isolate=0 --verbose=1',
            'log' => $projectRoot . '/console/runtime/logs/' . $componentId . '.log',
        ];
    }

    protected function projectRootPath(): string
    {
        $appPath = Yii::getAlias('@app', false);
        if (!is_string($appPath) || $appPath === '') {
            return getcwd() ?: '.';
        }

        return dirname($appPath);
    }

    protected function isQueueComponentId(string $componentId): bool
    {
        if ($componentId === 'queue') {
            return true;
        }

        return strlen($componentId) >= 5 && substr($componentId, -5) === 'queue';
    }

    protected function isQueueClass(?string $className): bool
    {
        if (!is_string($className) || $className === '') {
            return false;
        }

        $normalized = ltrim($className, '\\');

        return stripos($normalized, 'common\\components\\queue\\') !== false
            || stripos($normalized, 'yii\\queue\\') !== false;
    }

    protected function objectProperty($object, string $property, $default = null)
    {
        try {
            if (method_exists($object, 'hasProperty') && $object->hasProperty($property)) {
                return $object->{$property};
            }

            if (property_exists($object, $property)) {
                return $object->{$property};
            }
        } catch (\Throwable $exception) {
            return $default;
        }

        return $default;
    }

    protected function promoteStatus(string $current, string $candidate): string
    {
        return $this->statusRank($candidate) > $this->statusRank($current) ? $candidate : $current;
    }

    protected function statusRank(string $status): int
    {
        $map = [
            'unknown' => 0,
            'healthy' => 1,
            'warning' => 2,
            'critical' => 3,
        ];

        return $map[$status] ?? 0;
    }

    protected function boolValue($value, bool $default = false): bool
    {
        if ($value === null) {
            return $default;
        }

        if (is_bool($value)) {
            return $value;
        }

        if (is_numeric($value)) {
            return (int)$value === 1;
        }

        if (!is_string($value)) {
            return $default;
        }

        $normalized = strtolower(trim($value));

        if (in_array($normalized, ['1', 'true', 'yes', 'on', 'enabled'], true)) {
            return true;
        }

        if (in_array($normalized, ['0', 'false', 'no', 'off', 'disabled'], true)) {
            return false;
        }

        return $default;
    }
}
