<?php

declare(strict_types=1);

namespace PortalPeople\monitor\services;

use Yii;
use yii\db\Query;

/**
 * MonitorService
 *
 * Provides all health-check logic for NGauge monitoring.
 * Consumed by the monitor module's web and API controllers so all surfaces
 * use the same underlying checks.
 *
 * Usage:
 *   $service = new MonitorService();
 *   $result  = $service->all();          // full dashboard
 *   $result  = $service->modules();      // module registration only
 *   $result  = $service->queues();       // queue depths + stalled
 *   $result  = $service->database();     // DB connectivity + latency
 *   $result  = $service->disk();         // disk usage
 *   $result  = $service->errors(60);     // error log summary (last N minutes)
 */
class MonitorService
{
    /** Alert if any queue channel has more than this many waiting jobs */
    public int $warnWaiting = 100;

    /** Alert if any queue channel has more than this many stalled jobs */
    public int $warnStalled = 5;

    /** Alert if oldest waiting job is older than this many seconds (default: 1 hour) */
    public int $warnAgeSeconds = 3600;

    /** Disk usage warning threshold as a percentage */
    public int $warnDiskPercent = 80;

    // =========================================================================
    //  Public API
    // =========================================================================

    /**
     * Run all checks and return a unified result.
     *
     * @return array{timestamp: string, status: string, modules: array, queues: array, database: array, disk: array, errors: array}
     */
    public function all(int $errorMinutes = 60): array
    {
        $modules      = $this->modules();
        $monitoring   = $this->moduleMonitoring();
        $queues       = $this->queues();
        $database     = $this->database();
        $disk         = $this->disk();
        $errors       = $this->errors($errorMinutes);

        $modulesOk = !in_array('unavailable', array_column($modules, 'status'), true);
        $queuesOk  = !in_array('warn', array_column($queues, 'status'), true)
                  && !in_array('error', array_column($queues, 'status'), true);
        $diskOk    = !in_array('warn', array_column($disk, 'status'), true);
        $monitoringOk = !in_array('warning', array_column($monitoring, 'status'), true)
                    && !in_array('error', array_column($monitoring, 'status'), true);
        $allOk     = $modulesOk && $queuesOk && $database['ok'] && $diskOk && $monitoringOk;

        return [
            'timestamp'  => date('c'),
            'status'     => $allOk ? 'healthy' : 'degraded',
            'modules'    => $modules,
            'monitoring' => $monitoring,
            'queues'     => $queues,
            'database'   => $database,
            'disk'       => $disk,
            'errors'     => $errors,
        ];
    }

    /**
     * Check registration and service-getter presence of all NGauge API modules.
     *
     * @return array<string, array{status: string, message: string}>
     */
    public function modules(): array
    {
        $checks = [];

        foreach ($this->moduleChecks() as $moduleId => $config) {
            $module = Yii::$app->getModule($moduleId);

            if ($module === null) {
                $checks[$moduleId] = [
                    'status'  => $config['optional'] ? 'optional' : 'unavailable',
                    'message' => $config['optional']
                        ? 'Not registered (optional — graceful fallback expected)'
                        : 'Not registered — API wrappers will return 503',
                ];
                continue;
            }

            $getter = $config['getter'];
            if (!method_exists($module, $getter)) {
                $checks[$moduleId] = [
                    'status'  => 'unavailable',
                    'message' => "Module loaded but missing getter: {$getter}()",
                ];
                continue;
            }

            $checks[$moduleId] = [
                'status'  => 'ok',
                'message' => 'Registered and service getter present',
            ];
        }

        return $checks;
    }

    /**
     * Run monitoring checks supplied by individual modules.
     *
     * @return array<string, array{name: string, status: string, checks: array<int, array{id: string, label: string, status: string, message: string}>}>
     */
    public function moduleMonitoring(): array
    {
        $results = [];

        foreach (Yii::$app->getModules() as $moduleId => $_moduleConfig) {
            $module = Yii::$app->getModule($moduleId);
            if ($module === null || !method_exists($module, 'getMonitoringConfig')) {
                continue;
            }

            $config = $module->getMonitoringConfig();
            if (!is_array($config) || !isset($config['checks']) || !is_array($config['checks'])) {
                continue;
            }

            $checks = [];
            $status = 'ok';
            $messages = [];

            foreach ($config['checks'] as $check) {
                if (!is_array($check)) {
                    continue;
                }

                $checkResult = $this->evaluateMonitoringCheck($check);
                $checks[] = $checkResult;

                if ($checkResult['status'] === 'error') {
                    $status = 'error';
                } elseif ($checkResult['status'] === 'warning' && $status !== 'error') {
                    $status = 'warning';
                }

                if ($checkResult['message'] !== '') {
                    $messages[] = $checkResult['message'];
                }
            }

            $jobs = $config['jobs'] ?? [];
            if (is_string($jobs)) {
                $jobs = [$jobs];
            }

            if (is_array($jobs)) {
                foreach ($jobs as $index => $jobPath) {
                    if (!is_string($jobPath) || $jobPath === '') {
                        continue;
                    }

                    $jobCheck = [
                        'id' => 'jobs-' . $index,
                        'label' => 'Jobs directory',
                        'type' => 'directory',
                        'path' => $jobPath,
                    ];

                    $jobResult = $this->evaluateMonitoringCheck($jobCheck);
                    $checks[] = $jobResult;

                    if ($jobResult['status'] === 'error') {
                        $status = 'error';
                    } elseif ($jobResult['status'] === 'warning' && $status !== 'error') {
                        $status = 'warning';
                    }

                    if ($jobResult['message'] !== '') {
                        $messages[] = $jobResult['message'];
                    }
                }
            }

            $results[$moduleId] = [
                'name' => (string)($config['name'] ?? $moduleId),
                'status' => $status,
                'checks' => $checks,
                'message' => $messages !== [] ? implode('; ', $messages) : 'All monitoring checks passed.',
            ];
        }

        ksort($results);

        return $results;
    }

    /**
     * Query queue table for per-channel statistics.
     *
     * Channels monitored: default, mailqueue, netsuite, df
     *
     * Per channel:
     *   waiting           — pushed, not reserved, not done, delay elapsed
     *   reserved          — currently being processed
     *   done              — completed (retained when deleteReleased = false)
     *   stalled           — reserved but TTR expired, not done (likely dead workers)
     *   oldest_waiting_sec — age in seconds of the oldest waiting job
     *
     * @return array<string, array{status: string, channel: string, waiting: int, reserved: int, done: int, stalled: int, oldest_waiting_sec: int|null, message: string}>
     */
    public function queues(): array
    {
        $channels = [
            'queue'         => 'default',
            'mailqueue'     => 'mailqueue',
            'netsuitequeue' => 'netsuite',
            'dfqueue'       => 'df',
        ];

        $results = [];
        $now     = time();

        foreach ($channels as $componentId => $channel) {
            if (!Yii::$app->has($componentId)) {
                $results[$componentId] = [
                    'status'             => 'optional',
                    'channel'            => $channel,
                    'waiting'            => 0,
                    'reserved'           => 0,
                    'done'               => 0,
                    'stalled'            => 0,
                    'oldest_waiting_sec' => null,
                    'message'            => 'Component not registered in this app config',
                ];
                continue;
            }

            try {
                $db    = Yii::$app->db;
                $table = '{{%queue}}';

                $waiting = (int) (new Query())
                    ->from($table)
                    ->where(['channel' => $channel, 'reserved_at' => null, 'done_at' => null])
                    ->andWhere('[[pushed_at]] + [[delay]] <= :now', [':now' => $now])
                    ->count('*', $db);

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

                $done = (int) (new Query())
                    ->from($table)
                    ->where(['channel' => $channel])
                    ->andWhere(['not', ['done_at' => null]])
                    ->count('*', $db);

                $stalled = (int) (new Query())
                    ->from($table)
                    ->where(['channel' => $channel, 'done_at' => null])
                    ->andWhere(['not', ['reserved_at' => null]])
                    ->andWhere('[[reserved_at]] + [[ttr]] < :now', [':now' => $now])
                    ->count('*', $db);

                $oldestPushed = (new Query())
                    ->select('MIN([[pushed_at]] + [[delay]])')
                    ->from($table)
                    ->where(['channel' => $channel, 'reserved_at' => null, 'done_at' => null])
                    ->andWhere('[[pushed_at]] + [[delay]] <= :now', [':now' => $now])
                    ->scalar($db);

                $oldestAgeSec = $oldestPushed !== null ? ($now - (int) $oldestPushed) : null;

                $status   = 'ok';
                $warnings = [];

                if ($waiting > $this->warnWaiting) {
                    $status     = 'warn';
                    $warnings[] = "waiting={$waiting} exceeds threshold of {$this->warnWaiting}";
                }
                if ($stalled > $this->warnStalled) {
                    $status     = 'warn';
                    $warnings[] = "stalled={$stalled} exceeds threshold of {$this->warnStalled}";
                }
                if ($oldestAgeSec !== null && $oldestAgeSec > $this->warnAgeSeconds) {
                    $status     = 'warn';
                    $warnings[] = sprintf(
                        'oldest job %s old (threshold %s)',
                        $this->humanDuration($oldestAgeSec),
                        $this->humanDuration($this->warnAgeSeconds)
                    );
                }

                $results[$componentId] = [
                    'status'             => $status,
                    'channel'            => $channel,
                    'waiting'            => $waiting,
                    'reserved'           => $reserved,
                    'done'               => $done,
                    'stalled'            => $stalled,
                    'oldest_waiting_sec' => $oldestAgeSec,
                    'message'            => $warnings !== [] ? implode('; ', $warnings) : 'Queue healthy',
                ];
            } catch (\Throwable $e) {
                $results[$componentId] = [
                    'status'             => 'error',
                    'channel'            => $channel,
                    'waiting'            => 0,
                    'reserved'           => 0,
                    'done'               => 0,
                    'stalled'            => 0,
                    'oldest_waiting_sec' => null,
                    'message'            => 'Query failed: ' . $e->getMessage(),
                ];
            }
        }

        return $results;
    }

    /**
     * Check database connectivity and measure query latency.
     *
     * @return array{ok: bool, latency_ms: float|null, message: string}
     */
    public function database(): array
    {
        try {
            $start = microtime(true);
            Yii::$app->db->open();
            Yii::$app->db->createCommand('SELECT 1')->queryScalar();
            $latency = round((microtime(true) - $start) * 1000, 2);

            return [
                'ok'         => true,
                'latency_ms' => $latency,
                'message'    => "Connected — query latency {$latency}ms",
            ];
        } catch (\Throwable $e) {
            return [
                'ok'         => false,
                'latency_ms' => null,
                'message'    => 'Connection failed: ' . $e->getMessage(),
            ];
        }
    }

    /**
     * Check disk usage on key application directories.
     *
     * @return array<string, array{status: string, path: string, used_percent: int|null, free_gb: float|null, message: string}>
     */
    public function disk(): array
    {
        $paths = [
            'runtime' => Yii::getAlias('@runtime'),
            'root'    => Yii::getAlias('@app'),
        ];

        $results = [];

        foreach ($paths as $label => $path) {
            if (!is_dir($path)) {
                $results[$label] = [
                    'status'       => 'unknown',
                    'path'         => $path,
                    'used_percent' => null,
                    'free_gb'      => null,
                    'message'      => 'Path does not exist',
                ];
                continue;
            }

            $total = disk_total_space($path);
            $free  = disk_free_space($path);

            if ($total === false || $free === false || $total === 0.0) {
                $results[$label] = [
                    'status'       => 'unknown',
                    'path'         => $path,
                    'used_percent' => null,
                    'free_gb'      => null,
                    'message'      => 'Unable to determine disk space',
                ];
                continue;
            }

            $usedPercent = (int) round((($total - $free) / $total) * 100);
            $freeGb      = round($free / 1_073_741_824, 2);
            $status      = $usedPercent >= $this->warnDiskPercent ? 'warn' : 'ok';

            $results[$label] = [
                'status'       => $status,
                'path'         => $path,
                'used_percent' => $usedPercent,
                'free_gb'      => $freeGb,
                'message'      => "{$usedPercent}% used, {$freeGb} GB free"
                    . ($status === 'warn' ? " — ABOVE {$this->warnDiskPercent}% threshold" : ''),
            ];
        }

        return $results;
    }

    /**
     * Summarise recent errors and warnings from the application log.
     *
     * @return array{log_path: string, found: bool, error_count: int, warning_count: int, recent_errors: string[]}
     */
    public function errors(int $minutes = 60): array
    {
        $logPath = Yii::getAlias('@runtime/logs/app.log');

        if (!file_exists($logPath)) {
            return [
                'log_path'      => $logPath,
                'found'         => false,
                'error_count'   => 0,
                'warning_count' => 0,
                'recent_errors' => [],
            ];
        }

        $errors   = (int) trim((string) shell_exec(sprintf('grep -c "\\[error\\]" %s 2>/dev/null || echo 0', escapeshellarg($logPath))));
        $warnings = (int) trim((string) shell_exec(sprintf('grep -c "\\[warning\\]" %s 2>/dev/null || echo 0', escapeshellarg($logPath))));

        $recentRaw    = shell_exec(sprintf('grep "\\[error\\]" %s | tail -5 2>/dev/null', escapeshellarg($logPath))) ?? '';
        $recentErrors = array_values(array_filter(array_map('trim', explode("\n", $recentRaw))));

        return [
            'log_path'      => $logPath,
            'found'         => true,
            'error_count'   => $errors,
            'warning_count' => $warnings,
            'recent_errors' => $recentErrors,
        ];
    }

    /**
     * Return the wrapper → module map.
     *
     * @return array<string, string>
     */
    public function wrapperMap(): array
    {
        return [
            'api/modules/v1/controllers/BackupsController'       => 'ngauge-backup-sql',
            'api/modules/v1/controllers/BookingController'       => 'surf-bookings',
            'api/modules/v1/controllers/ConsoleController'       => 'ngauge-console-tools',
            'api/modules/v1/controllers/DataController'          => 'ngauge-datadash',
            'api/modules/v1/controllers/DocumentationController' => 'ngauge-api-docs',
            'api/modules/v1/controllers/NGaugeController'        => 'ngauge-packagemanager',
            'api/modules/v1/controllers/OcrController'           => 'ngauge-ocr',
            'api/modules/v1/controllers/OrderController'         => 'ngauge-orders',
            'api/modules/v1/controllers/ProductsController'      => 'ngauge-products',
            'api/modules/v1/controllers/ReportsController'       => 'ngauge-reports',
            'api/modules/v1/controllers/SchemaController'        => 'ngauge-schema',
            'api/modules/v1/controllers/SendgridController'      => 'ngauge-mail',
            'api/modules/v1/controllers/SendinblueController'    => 'ngauge-mail',
            'api/modules/v1/controllers/SiteController'          => 'ngauge-site-setup',
            'api/modules/v1/controllers/UserController'          => 'ngauge-users',
            'api/modules/v1/controllers/WpController'            => 'ngauge-wordpress-bridge',
            'api/modules/df/controllers/DataController'          => 'ngauge-datadash',
            'api/modules/df/controllers/DataServicesController'  => 'ngauge-datadash',
        ];
    }

    // =========================================================================
    //  Helpers
    // =========================================================================

    /**
     * Module health check configuration.
     *
     * @return array<string, array{getter: string, optional: bool}>
     */
    private function moduleChecks(): array
    {
        return [
            'ngauge-backup-sql'       => ['getter' => 'getNotificationService',  'optional' => false],
            'surf-bookings'           => ['getter' => 'getBookingService',        'optional' => false],
            'ngauge-console-tools'    => ['getter' => 'getConsoleService',        'optional' => false],
            'ngauge-datadash'         => ['getter' => 'getDataService',           'optional' => false],
            'ngauge-packagemanager'   => ['getter' => 'getPackageService',        'optional' => false],
            'ngauge-orders'           => ['getter' => 'getOrderService',          'optional' => false],
            'ngauge-products'         => ['getter' => 'getProductsService',       'optional' => false],
            'ngauge-reports'          => ['getter' => 'getReportService',         'optional' => false],
            'ngauge-schema'           => ['getter' => 'getSchemaService',         'optional' => false],
            'ngauge-mail'             => ['getter' => 'getSendgridService',       'optional' => false],
            'ngauge-site-setup'       => ['getter' => 'getSiteSetupService',      'optional' => false],
            'ngauge-users'            => ['getter' => 'getUserService',           'optional' => false],
            'ngauge-wordpress-bridge' => ['getter' => 'getWpService',             'optional' => false],
            'ngauge-api-docs'         => ['getter' => 'getDocumentationService',  'optional' => true],
            'ngauge-ocr'              => ['getter' => 'getOcrService',            'optional' => true],
        ];
    }

    private function evaluateMonitoringCheck(array $check): array
    {
        $id = (string)($check['id'] ?? $check['name'] ?? 'check');
        $label = (string)($check['label'] ?? $id);
        $type = (string)($check['type'] ?? 'directory');

        switch ($type) {
            case 'component':
                $componentId = (string)($check['component'] ?? '');
                $exists = $componentId !== '' && Yii::$app->has($componentId);
                return [
                    'id' => $id,
                    'label' => $label,
                    'status' => $exists ? 'ok' : 'warning',
                    'message' => $exists ? 'Component registered.' : sprintf('Component %s is not registered.', $componentId),
                ];

            case 'module':
                $moduleId = (string)($check['moduleId'] ?? '');
                $exists = $moduleId !== '' && Yii::$app->getModule($moduleId) !== null;
                return [
                    'id' => $id,
                    'label' => $label,
                    'status' => $exists ? 'ok' : 'warning',
                    'message' => $exists ? 'Module registered.' : sprintf('Module %s is not registered.', $moduleId),
                ];

            case 'file':
                $path = $this->resolveMonitoringPath((string)($check['path'] ?? ''));
                $exists = $path !== '' && file_exists($path);
                return [
                    'id' => $id,
                    'label' => $label,
                    'status' => $exists ? 'ok' : 'warning',
                    'message' => $exists ? 'File exists.' : sprintf('File missing: %s', $path),
                ];

            case 'directory':
            default:
                $path = $this->resolveMonitoringPath((string)($check['path'] ?? ''));
                $exists = $path !== '' && is_dir($path);
                return [
                    'id' => $id,
                    'label' => $label,
                    'status' => $exists ? 'ok' : 'warning',
                    'message' => $exists ? 'Directory exists.' : sprintf('Directory missing: %s', $path),
                ];
        }
    }

    private function resolveMonitoringPath(string $path): string
    {
        if ($path === '') {
            return '';
        }

        return (string) Yii::getAlias($path);
    }

    /**
     * Convert seconds to a human-readable duration string.
     */
    public function humanDuration(int $seconds): string
    {
        if ($seconds < 60) {
            return "{$seconds}s";
        }
        if ($seconds < 3600) {
            return round($seconds / 60) . 'm';
        }
        return round($seconds / 3600, 1) . 'h';
    }
}
