<?php

declare(strict_types=1);

namespace App\Console\Commands;

use App\Models\Artwork;
use App\Services\Sitemaps\SitemapReleaseManager;
use App\Services\Vision\ArtworkVisionImageUrl;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Storage;
use Meilisearch\Client as MeilisearchClient;
use Throwable;

/**
 * Comprehensive service health check.
 *
 * Usage:
 *   php artisan health:check               # all checks
 *   php artisan health:check --only=meili  # single service
 *   php artisan health:check --json        # machine-readable JSON output
 */
class HealthCheckCommand extends Command
{
    protected $signature = 'health:check
        {--only= : Run only a named check (mysql|redis|cache|meilisearch|qdrant|reverb|vision|horizon|webserver|phpfpm|paths|ram|disk|load|s3|failed_jobs|queue_backlog|ssl|scheduler|sitemap|log_errors|app)}
        {--json  : Output results as JSON}';

    protected $description = 'Check health of all critical services (MySQL, Redis, Cache, Meilisearch, Qdrant, Reverb, Vision, Horizon, Nginx, PHP-FPM, writable paths, RAM, disk, load, S3/Contabo, failed jobs, queue backlog, SSL, scheduler, sitemap, log errors, App).';

    /** Collected results: [name => [status, message, details]] */
    private array $results = [];

    public function handle(): int
    {
        $only = $this->option('only') ? strtolower((string) $this->option('only')) : null;

        $checks = [
            'mysql'       => fn () => $this->checkMysql(),
            'redis'       => fn () => $this->checkRedis(),
            'cache'       => fn () => $this->checkCache(),
            'meilisearch' => fn () => $this->checkMeilisearch(),
            'qdrant'      => fn () => $this->checkQdrant(),
            'reverb'      => fn () => $this->checkReverb(),
            'vision'      => fn () => $this->checkVision(),
            'horizon'     => fn () => $this->checkHorizon(),
            'webserver'   => fn () => $this->checkWebserver(),
            'phpfpm'      => fn () => $this->checkPhpFpm(),
            'paths'       => fn () => $this->checkWritablePaths(),
            'ram'         => fn () => $this->checkRam(),
            'disk'        => fn () => $this->checkDisk(),
            'load'        => fn () => $this->checkLoad(),
            's3'          => fn () => $this->checkS3(),
            'failed_jobs' => fn () => $this->checkFailedJobs(),
            'queue_backlog' => fn () => $this->checkQueueBacklog(),
            'ssl'         => fn () => $this->checkSsl(),
            'scheduler'   => fn () => $this->checkScheduler(),
            'sitemap'     => fn () => $this->checkSitemap(),
            'log_errors'  => fn () => $this->checkLogErrors(),
            'app'         => fn () => $this->checkApp(),
        ];

        if ($only !== null) {
            if (! array_key_exists($only, $checks)) {
                $this->error("Unknown check '{$only}'. Available: " . implode(', ', array_keys($checks)));
                return self::FAILURE;
            }
            $checks = [$only => $checks[$only]];
        }

        foreach ($checks as $name => $check) {
            $check();
        }

        if ($this->option('json')) {
            $this->line(json_encode($this->results, JSON_PRETTY_PRINT));
            return $this->hasFailures() ? self::FAILURE : self::SUCCESS;
        }

        $this->renderTable();

        $failed = $this->countByStatus('fail');
        $warned = $this->countByStatus('warn');

        $this->newLine();
        if ($failed > 0) {
            $this->error("❌  {$failed} check(s) FAILED" . ($warned > 0 ? ", {$warned} warning(s)" : '') . '.');
            return self::FAILURE;
        }
        if ($warned > 0) {
            $this->warn("⚠️  All checks passed with {$warned} warning(s).");
            return self::SUCCESS;
        }
        $this->info('✅  All checks passed.');
        return self::SUCCESS;
    }

    // ── Individual checks ──────────────────────────────────────────────────────

    private function checkMysql(): void
    {
        try {
            DB::select('SELECT 1');
            $db      = config('database.connections.' . config('database.default') . '.database');
            $artworkCount = DB::table('artworks')->whereNull('deleted_at')->count();
            $this->pass('mysql', "Connected to `{$db}`. Artworks in DB: {$artworkCount}.", ['artwork_count' => $artworkCount]);
        } catch (Throwable $e) {
            $this->failCheck('mysql', 'Connection failed: ' . $e->getMessage());
        }
    }

    private function checkRedis(): void
    {
        try {
            $pong = Redis::ping();
            // ping returns "+PONG\r\n" string or true depending on driver
            $ok = $pong === true || str_contains((string) $pong, 'PONG');
            if ($ok) {
                $info = Redis::info('server');
                $version = $info['redis_version'] ?? ($info['Server']['redis_version'] ?? 'unknown');
                $this->pass('redis', "Reachable. Redis version: {$version}.", ['version' => $version]);
            } else {
                $this->failCheck('redis', 'Unexpected ping response: ' . var_export($pong, true));
            }
        } catch (Throwable $e) {
            $this->failCheck('redis', 'Connection failed: ' . $e->getMessage());
        }
    }

    private function checkCache(): void
    {
        try {
            $key   = '_healthcheck_' . uniqid('', true);
            $value = 'ok_' . time();
            Cache::put($key, $value, 10);
            $got = Cache::get($key);
            Cache::forget($key);

            $driver = config('cache.default');
            if ($got === $value) {
                $this->pass('cache', "Driver `{$driver}` read/write OK.", ['driver' => $driver]);
            } else {
                $this->failCheck('cache', "Driver `{$driver}`: wrote '{$value}' but read back " . var_export($got, true));
            }
        } catch (Throwable $e) {
            $this->failCheck('cache', 'Cache test failed: ' . $e->getMessage());
        }
    }

    private function checkMeilisearch(): void
    {
        try {
            /** @var MeilisearchClient $client */
            $client = app(MeilisearchClient::class);
            $health = $client->health();

            if (($health['status'] ?? '') !== 'available') {
                $this->failCheck('meilisearch', 'Meilisearch reports unhealthy status: ' . json_encode($health));
                return;
            }

            $version = $client->version()['pkgVersion'] ?? 'unknown';

            // ── List ALL indexes ──────────────────────────────────────────────
            $allIndexes   = $client->getIndexes();
            $indexObjects = $allIndexes->getResults();
            $indexSummaries = [];

            // DB counts for coverage checks
            $dbPublicArtworks = DB::table('artworks')
                ->whereNull('deleted_at')
                ->where('is_public', 1)
                ->where('is_approved', 1)
                ->count();

            $artworkIndexName = (new Artwork())->searchableAs();
            $artworkIndexOk   = false;
            $warnings         = [];

            foreach ($indexObjects as $idx) {
                $idxName  = $idx->getUid();
                $stats    = $idx->stats();
                $docCount = (int) ($stats['numberOfDocuments'] ?? 0);
                $indexing = (bool) ($stats['isIndexing'] ?? false);
                $suffix   = $indexing ? ' [indexing]' : '';

                // Coverage check for artworks index
                if ($idxName === $artworkIndexName) {
                    if ($docCount === 0) {
                        $warnings[] = "❌ `{$idxName}`: EMPTY (DB has {$dbPublicArtworks} public+approved). Run: php artisan scout:import \"App\\\\Models\\\\Artwork\"";
                    } elseif ($dbPublicArtworks > 0 && $docCount < (int) ($dbPublicArtworks * 0.5)) {
                        $warnings[] = "⚠️  `{$idxName}`: {$docCount} docs < 50% of DB {$dbPublicArtworks}. Run: php artisan artworks:search-rebuild";
                    } else {
                        $artworkIndexOk = true;
                    }
                }

                $indexSummaries[] = "`{$idxName}`: {$docCount} docs{$suffix}";
            }

            // Check for pending tasks
            try {
                $pending = $client->getTasks(['statuses' => 'enqueued,processing'])->getTotal();
                if ($pending > 0) {
                    $warnings[] = "{$pending} task(s) still pending in Meilisearch queue";
                }
            } catch (Throwable) {
                // non-fatal
            }

            $indexList = implode(' | ', $indexSummaries) ?: 'none';
            $message   = "v{$version}. Indexes: {$indexList}.";

            if (! empty($warnings)) {
                $this->result('meilisearch', 'warn', $message . ' — ' . implode('; ', $warnings), [
                    'meili_version' => $version,
                    'indexes'       => $indexSummaries,
                    'warnings'      => $warnings,
                ]);
            } else {
                $this->pass('meilisearch', $message, [
                    'meili_version'    => $version,
                    'indexes'          => $indexSummaries,
                    'db_public_artworks' => $dbPublicArtworks,
                ]);
            }
        } catch (Throwable $e) {
            $this->failCheck('meilisearch', 'Unreachable or error: ' . $e->getMessage());
        }
    }

    private function checkQdrant(): void
    {
        $enabled    = (bool) config('vision.vector_gateway.enabled', true);
        $baseUrl    = rtrim((string) config('vision.vector_gateway.base_url', ''), '/');
        $apiKey     = (string) config('vision.vector_gateway.api_key', '');
        $collection = (string) config('vision.vector_gateway.collection', 'images');
        $endpoint   = (string) config('vision.vector_gateway.collections_endpoint', '/vectors/collections');

        if (! $enabled) {
            $this->warn_check('qdrant', 'VISION_VECTOR_GATEWAY_ENABLED=false — similarity search disabled, skipping.');
            return;
        }

        if (empty($baseUrl)) {
            $this->warn_check('qdrant', 'VISION_VECTOR_GATEWAY_URL not configured — skipping.');
            return;
        }

        try {
            // 1. Hit the gateway health endpoint
            $healthUrl = $baseUrl . '/health';
            $health    = $this->httpGetWithAuth($healthUrl, $apiKey, 5);

            if (($health['code'] ?? 0) === 0) {
                $this->failCheck('qdrant', "Vector gateway unreachable at {$baseUrl}/health");
                return;
            }

            if ($health['code'] >= 400) {
                $this->failCheck('qdrant', "Vector gateway /health returned HTTP {$health['code']} at {$baseUrl}");
                return;
            }

            // 2. List collections to verify the configured collection exists
            $collectionsUrl = $baseUrl . $endpoint;
            $resp           = $this->httpGetWithAuth($collectionsUrl, $apiKey, 8);
            $code           = $resp['code'] ?? 0;

            if ($code === 0) {
                $this->failCheck('qdrant', "Cannot reach {$collectionsUrl}");
                return;
            }

            if ($code >= 400) {
                $this->failCheck('qdrant', "Collections endpoint returned HTTP {$code} at {$collectionsUrl}");
                return;
            }

            $body = json_decode($resp['body'] ?? '', true);

            // Gateway returns { "collections": [...] } or flat array
            $collections = [];
            if (isset($body['collections']) && is_array($body['collections'])) {
                $collections = $body['collections'];
            } elseif (is_array($body)) {
                $collections = $body;
            }

            // Collection names may be strings or objects with a 'name' key
            $collectionNames = array_map(
                fn ($c) => is_string($c) ? $c : ($c['name'] ?? (string) json_encode($c)),
                $collections
            );

            $countLabel = count($collectionNames) . ' collection(s): ' . implode(', ', $collectionNames);

            if (! in_array($collection, $collectionNames, true)) {
                $this->failCheck('qdrant', "Configured collection `{$collection}` NOT FOUND. Available: {$countLabel}. Re-run: php artisan artworks:vector-index-backfill");
                return;
            }

            // 3. Try a test search to confirm similarity actually works using the
            // same URL-based request shape as VectorGatewayClient.
            $searchEndpoint = rtrim($baseUrl, '/') . (string) config('vision.vector_gateway.search_endpoint', '/vectors/search');
            $probeArtwork = Artwork::query()
                ->select(['id', 'hash', 'thumb_ext'])
                ->whereNull('deleted_at')
                ->where('is_public', 1)
                ->where('is_approved', 1)
                ->whereNotNull('hash')
                ->orderByDesc('published_at')
                ->first();

            if (! $probeArtwork) {
                $this->warn_check('qdrant', "Gateway reachable and collection `{$collection}` found, but there is no eligible artwork available for a similarity smoke probe.", [
                    'base_url' => $baseUrl,
                    'collection' => $collection,
                    'collections' => $collectionNames,
                ]);
                return;
            }

            /** @var ArtworkVisionImageUrl $imageUrlBuilder */
            $imageUrlBuilder = app(ArtworkVisionImageUrl::class);
            $probeImageUrl = $imageUrlBuilder->fromArtwork($probeArtwork);

            if ($probeImageUrl === null || $probeImageUrl === '') {
                $this->warn_check('qdrant', "Gateway reachable and collection `{$collection}` found, but the probe artwork #{$probeArtwork->id} has no usable image URL.", [
                    'probe_artwork_id' => (int) $probeArtwork->id,
                    'collection' => $collection,
                ]);
                return;
            }

            $payload  = json_encode(['url' => $probeImageUrl, 'limit' => 1]);

            $ch = curl_init($searchEndpoint);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT        => 10,
                CURLOPT_CONNECTTIMEOUT => 3,
                CURLOPT_POST           => true,
                CURLOPT_POSTFIELDS     => $payload,
                CURLOPT_HTTPHEADER     => array_filter([
                    'Content-Type: application/json',
                    'Accept: application/json',
                    $apiKey ? "X-API-Key: {$apiKey}" : '',
                ]),
            ]);
            $searchBody = curl_exec($ch);
            $searchCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);

            $searchOk = $searchCode >= 200 && $searchCode < 300;
            $searchResult = json_decode((string) $searchBody, true);

            // Count indexed vectors
            $vectorCount = null;
            if ($searchOk && isset($searchResult['total'])) {
                $vectorCount = (int) $searchResult['total'];
            }

            $details = [
                'base_url'        => $baseUrl,
                'collection'      => $collection,
                'collections'     => $collectionNames,
                'probe_artwork_id'=> (int) $probeArtwork->id,
                'probe_image_url' => $probeImageUrl,
                'search_http'     => $searchCode,
                'vector_count'    => $vectorCount,
            ];

            if (! $searchOk) {
                $this->warn_check('qdrant', "Gateway reachable, collection `{$collection}` found, but search probe returned HTTP {$searchCode}. Check vector gateway logs.", $details);
                return;
            }

            $vcLabel = $vectorCount !== null ? ", ~{$vectorCount} vectors indexed" : '';
            $this->pass('qdrant', "Gateway OK (HTTP {$health['code']}). Collection `{$collection}` present ({$countLabel}). Similarity search probe: HTTP {$searchCode}{$vcLabel}.", $details);

        } catch (Throwable $e) {
            $this->failCheck('qdrant', 'Check failed: ' . $e->getMessage());
        }
    }


    private function checkReverb(): void
    {
        // REVERB_HOST / REVERB_PORT = the public-facing hostname clients connect to.
        // REVERB_SERVER_HOST / REVERB_SERVER_PORT = internal bind address for the Reverb process.
        // We probe the public-facing host — that's what actually matters for users.
        $host   = config('broadcasting.connections.reverb.options.host') ?: env('REVERB_HOST', '');
        $port   = (int) (config('broadcasting.connections.reverb.options.port') ?: env('REVERB_PORT', 443));
        $scheme = config('broadcasting.connections.reverb.options.scheme') ?: env('REVERB_SCHEME', 'https');

        if (empty($host)) {
            $this->warn_check('reverb', 'REVERB_HOST not configured — skipping.');
            return;
        }

        // Reverb exposes an HTTP health endpoint at /apps/{appId}
        // We do a plain TCP connect as the minimal check; a refused connection means down.
        $timeout = 5;
        try {
            $errno  = 0;
            $errstr = '';
            $proto  = $scheme === 'https' ? 'ssl' : 'tcp';
            $fp     = @fsockopen("{$proto}://{$host}", $port, $errno, $errstr, $timeout);

            if ($fp === false) {
                $this->failCheck('reverb', "Cannot connect to {$host}:{$port} ({$scheme}) — {$errstr} [{$errno}]");
                return;
            }

            fclose($fp);

            // TCP succeeded — Reverb is alive. Attempt an HTTP probe for extra info,
            // but don't fail/warn on it: Reverb is a WebSocket server and may not
            // respond to plain HTTP GET requests with a meaningful status code.
            $url      = "{$scheme}://{$host}:{$port}/";
            $response = $this->httpGet($url, 3);
            $code     = $response['code'] ?? 0;
            $codeStr  = $code > 0 ? " (HTTP {$code})" : '';

            $this->pass('reverb', "Reachable at {$host}:{$port}{$codeStr}.", ['host' => $host, 'port' => $port, 'http_code' => $code]);
        } catch (Throwable $e) {
            $this->failCheck('reverb', 'Check failed: ' . $e->getMessage());
        }
    }

    private function checkVision(): void
    {
        $services = [
            'CLIP / Gateway' => rtrim((string) config('vision.gateway.base_url', ''), '/') . '/health',
            'Vector Gateway' => rtrim((string) config('vision.vector_gateway.base_url', ''), '/') . '/health',
        ];

        $allPassed = true;
        $messages  = [];

        foreach ($services as $label => $url) {
            if ($url === '/health' || $url === '') {
                $messages[] = "{$label}: not configured";
                continue;
            }

            $response = $this->httpGet($url, 5);
            $code     = $response['code'] ?? 0;

            if ($code >= 200 && $code < 300) {
                $messages[] = "{$label}: OK (HTTP {$code})";
            } elseif ($code === 0) {
                $allPassed = false;
                $messages[] = "{$label}: UNREACHABLE ({$url})";
            } else {
                $allPassed = false;
                $messages[] = "{$label}: HTTP {$code} ({$url})";
            }
        }

        $summary = implode(' | ', $messages);

        if ($allPassed) {
            $this->pass('vision', $summary);
        } else {
            $this->warn_check('vision', $summary);
        }
    }

    private function checkWebserver(): void
    {
        // ── 1. systemctl (Linux) ─────────────────────────────────────────────
        if (function_exists('exec') && PHP_OS_FAMILY !== 'Windows') {
            foreach (['nginx', 'apache2', 'httpd'] as $svc) {
                $out = [];
                $rc  = -1;
                @exec("systemctl is-active {$svc} 2>/dev/null", $out, $rc);
                if ($rc === 0) {
                    $pid = '';
                    @exec("systemctl show -p MainPID {$svc} 2>/dev/null", $pidOut, $pidRc);
                    if ($pidRc === 0 && preg_match('/MainPID=(\d+)/', implode('', $pidOut), $m)) {
                        $pid = " (PID {$m[1]})";
                    }
                    $this->pass('webserver', "Service `{$svc}` is active via systemctl{$pid}.", ['service' => $svc]);
                    return;
                }
            }
        }

        // ── 2. pgrep fallback ────────────────────────────────────────────────
        if (function_exists('exec') && PHP_OS_FAMILY !== 'Windows') {
            foreach (['nginx', 'apache2', 'httpd'] as $proc) {
                $pids = [];
                $rc   = -1;
                @exec("pgrep -x {$proc} 2>/dev/null", $pids, $rc);
                if ($rc === 0 && count($pids) > 0) {
                    $this->pass('webserver', "Process `{$proc}` found via pgrep (PIDs: " . implode(', ', array_slice($pids, 0, 5)) . ").", ['process' => $proc]);
                    return;
                }
            }
        }

        // ── 3. TCP probe — identify the actual server from the Server header ──
        foreach ([80, 443] as $port) {
            $scheme = $port === 443 ? 'ssl' : 'tcp';
            $fp = @fsockopen("{$scheme}://127.0.0.1", $port, $errno, $errstr, 3);
            if ($fp === false) {
                continue;
            }
            fclose($fp);
            $probeUrl = ($port === 443 ? 'https' : 'http') . '://127.0.0.1/';
            $resp     = $this->httpGet($probeUrl, 5);
            $rawServer = (string) ($resp['server'] ?? '');
            $server    = strtolower($rawServer);

            if ($rawServer === '') {
                $this->pass('webserver', "Port {$port} open (HTTP {$resp['code']}). Server header not exposed.");
            } elseif (str_contains($server, 'nginx')) {
                $this->pass('webserver', "nginx running (HTTP {$resp['code']}) — Server: {$rawServer}.", ['server' => $rawServer, 'port' => $port]);
            } elseif (str_contains($server, 'apache')) {
                $this->pass('webserver', "Apache running (HTTP {$resp['code']}) — Server: {$rawServer}.", ['server' => $rawServer, 'port' => $port]);
            } else {
                $this->pass('webserver', "Port {$port} open (HTTP {$resp['code']}) — Server: {$rawServer}.", ['server' => $rawServer, 'port' => $port]);
            }
            return;
        }

        $this->failCheck('webserver', 'No web server detected via systemctl/pgrep, and ports 80/443 are closed. Start nginx: sudo systemctl start nginx');
    }

    private function checkPhpFpm(): void
    {
        if (PHP_OS_FAMILY === 'Windows') {
            $this->warn_check('phpfpm', 'PHP-FPM is not applicable on this Windows setup. Nginx/mod_fcgid or similar is expected locally.');
            return;
        }

        // ── 1. systemctl ─────────────────────────────────────────────────────
        if (function_exists('exec') && PHP_OS_FAMILY !== 'Windows') {
            // Discover all php*-fpm service names via systemctl list-units
            $units = [];
            @exec("systemctl list-units --type=service --state=active --no-pager --plain 2>/dev/null | grep php", $units, $rc);

            foreach ($units as $line) {
                if (preg_match('/^(php[\d.\-]+fpm\.service)/i', trim($line), $m)) {
                    $svcFull = $m[1];
                    $this->pass('phpfpm', "Service `{$svcFull}` is active.", ['service' => $svcFull]);
                    return;
                }
            }

            // Fallback: check common names directly
            $candidates = ['php8.4-fpm', 'php8.3-fpm', 'php8.2-fpm', 'php8.1-fpm', 'php8.0-fpm', 'php-fpm'];
            foreach ($candidates as $svc) {
                $out = [];
                $rc  = -1;
                @exec("systemctl is-active {$svc} 2>/dev/null", $out, $rc);
                if ($rc === 0) {
                    $this->pass('phpfpm', "Service `{$svc}` is active via systemctl.", ['service' => $svc]);
                    return;
                }
            }

            // pgrep fallback
            $pids = [];
            $rc   = -1;
            @exec("pgrep -a php-fpm 2>/dev/null", $pids, $rc);
            if ($rc === 0 && count($pids) > 0) {
                $first = trim($pids[0]);
                $this->pass('phpfpm', "PHP-FPM master process found via pgrep: {$first}", ['processes' => array_slice($pids, 0, 3)]);
                return;
            }
        }

        // ── 2. Check common Unix socket paths ────────────────────────────────
        $sockets = [
            '/run/php/php8.4-fpm.sock',
            '/run/php/php8.3-fpm.sock',
            '/run/php/php8.2-fpm.sock',
            '/run/php/php8.1-fpm.sock',
            '/run/php/php-fpm.sock',
            '/var/run/php/php-fpm.sock',
        ];
        foreach ($sockets as $sock) {
            if (file_exists($sock) && filetype($sock) === 'socket') {
                $this->pass('phpfpm', "PHP-FPM socket found: {$sock}", ['socket' => $sock]);
                return;
            }
        }

        $this->warn_check('phpfpm', "Could not detect PHP-FPM via systemctl, pgrep, or known sockets. Verify manually: systemctl status php*-fpm");
    }

    private function checkWritablePaths(): void
    {
        $paths = [
            'storage/logs'              => storage_path('logs'),
            'storage/framework/cache'   => storage_path('framework/cache'),
            'storage/framework/sessions'=> storage_path('framework/sessions'),
            'storage/framework/views'   => storage_path('framework/views'),
            'storage/app'               => storage_path('app'),
            'storage/app/public'        => storage_path('app/public'),
            'bootstrap/cache'           => base_path('bootstrap/cache'),
        ];

        // Also check configured upload local paths (only when they point to local disk, not S3)
        $artworksDisk = (string) config('uploads.artworks.disk', 's3');
        if ($artworksDisk !== 's3') {
            $storageRoot = (string) config('uploads.storage_root', '');
            if (! empty($storageRoot)) {
                $paths['uploads/storage_root'] = $storageRoot;
            }
        }
        $originalsRoot = (string) config('uploads.local_originals_root', '');
        if (! empty($originalsRoot)) {
            $paths['uploads/originals'] = $originalsRoot;
        }

        $failed  = [];
        $missing = [];
        $ok      = [];

        foreach ($paths as $label => $path) {
            if (! file_exists($path)) {
                $missing[] = "{$label} ({$path}) — directory does not exist";
                continue;
            }
            if (! is_writable($path)) {
                $failed[] = "{$label} ({$path}) — NOT writable";
                continue;
            }
            // Confirm we can actually create and delete a temp file
            $probe = $path . '/.healthcheck_' . getmypid();
            if (@file_put_contents($probe, '1') === false) {
                $failed[] = "{$label} ({$path}) — write test failed";
            } else {
                @unlink($probe);
                $ok[] = $label;
            }
        }

        $summary = count($ok) . '/' . count($paths) . ' paths writable';

        if (! empty($failed) || ! empty($missing)) {
            $issues = array_merge($failed, $missing);
            $this->failCheck('paths', "{$summary}. Issues: " . implode('; ', $issues), [
                'ok'      => $ok,
                'failed'  => $failed,
                'missing' => $missing,
            ]);
        } else {
            $this->pass('paths', "{$summary}. Checked: " . implode(', ', $ok), ['ok' => $ok]);
        }
    }

    private function checkHorizon(): void
    {
        try {
            $queueDefault = (string) config('queue.default', 'sync');
            $scoutQueueConnection = (string) config('scout.queue.connection', $queueDefault);

            if ($queueDefault !== 'redis' && $scoutQueueConnection !== 'redis') {
                $this->warn_check('horizon', "Horizon skipped: queue.default={$queueDefault}, scout.queue.connection={$scoutQueueConnection}. Redis-backed Horizon is not required in this environment.", [
                    'queue_default' => $queueDefault,
                    'scout_queue_connection' => $scoutQueueConnection,
                ]);
                return;
            }

            // Horizon stores its status in Redis under the horizon:master-supervisor key prefix.
            // A simpler cross-version check: look for any horizon-related Redis key.
            $status = Cache::store('redis')->get('horizon:status');

            if ($status === null) {
                // Try reading directly from Redis
                $status = Redis::get('horizon:status');
            }

            if ($status === null) {
                if (function_exists('exec') && PHP_OS_FAMILY !== 'Windows') {
                    $processes = [];
                    $rc = -1;
                    @exec('pgrep -a -f "artisan horizon|php.*horizon" 2>/dev/null', $processes, $rc);
                    if ($rc === 0 && $processes !== []) {
                        $this->pass('horizon', 'Horizon process detected via pgrep even though no Redis status key was found.', [
                            'processes' => array_slice($processes, 0, 3),
                        ]);
                        return;
                    }
                }

                $this->warn_check('horizon', 'No Horizon status key in Redis and no Horizon process detected — Horizon may not be running or has never started.');
                return;
            }

            $status = is_string($status) ? strtolower(trim($status)) : strtolower((string) $status);

            if ($status === 'running') {
                $this->pass('horizon', "Horizon status: running.");
            } elseif ($status === 'paused') {
                $this->warn_check('horizon', "Horizon is PAUSED. Resume with: php artisan horizon:continue");
            } else {
                $this->failCheck('horizon', "Horizon status: {$status}. Start with: php artisan horizon");
            }
        } catch (Throwable $e) {
            $this->warn_check('horizon', 'Could not read Horizon status: ' . $e->getMessage());
        }
    }

    private function checkRam(): void
    {
        if (PHP_OS_FAMILY === 'Windows') {
            $this->warn_check('ram', 'RAM check not supported on Windows. Run on Linux server for real metrics.');
            return;
        }

        try {
            $meminfo = @file_get_contents('/proc/meminfo');
            if ($meminfo === false || $meminfo === '') {
                $this->warn_check('ram', '/proc/meminfo not available — cannot read RAM stats.');
                return;
            }

            preg_match('/MemTotal:\s+(\d+)/', $meminfo, $total);
            preg_match('/MemAvailable:\s+(\d+)/', $meminfo, $available);

            $totalKb     = (int) ($total[1] ?? 0);
            $availableKb = (int) ($available[1] ?? 0);

            if ($totalKb === 0) {
                $this->warn_check('ram', 'Could not parse MemTotal from /proc/meminfo.');
                return;
            }

            $usedKb      = $totalKb - $availableKb;
            $usedPct     = round(($usedKb / $totalKb) * 100, 1);
            $totalMb     = round($totalKb / 1024);
            $availableMb = round($availableKb / 1024);
            $usedMb      = round($usedKb / 1024);

            $details = [
                'total_mb'     => $totalMb,
                'used_mb'      => $usedMb,
                'available_mb' => $availableMb,
                'used_pct'     => $usedPct,
            ];

            $msg = "Total: {$totalMb} MB | Used: {$usedMb} MB ({$usedPct}%) | Available: {$availableMb} MB.";

            if ($usedPct >= 95) {
                $this->failCheck('ram', "RAM critical — {$usedPct}% used. {$msg}", $details);
            } elseif ($usedPct >= 85) {
                $this->warn_check('ram', "RAM high — {$usedPct}% used. {$msg}", $details);
            } else {
                $this->pass('ram', $msg, $details);
            }
        } catch (Throwable $e) {
            $this->warn_check('ram', 'RAM check failed: ' . $e->getMessage());
        }
    }

    private function checkDisk(): void
    {
        $paths = [
            'app root'    => base_path(),
            'storage'     => storage_path(),
        ];

        $warnThreshold = 85;
        $failThreshold = 95;
        $rows = [];
        $worst = 'pass';

        foreach ($paths as $label => $path) {
            if (! file_exists($path)) {
                continue;
            }

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

            if ($total === false || $total === 0.0 || $free === false) {
                $rows[] = "{$label}: unable to read";
                continue;
            }

            $used    = $total - $free;
            $usedPct = round(($used / $total) * 100, 1);
            $totalGb = round($total / (1024 ** 3), 1);
            $freeGb  = round($free  / (1024 ** 3), 1);

            $rows[] = "{$label}: {$freeGb} GB free / {$totalGb} GB total ({$usedPct}% used)";

            if ($usedPct >= $failThreshold && $worst !== 'fail') {
                $worst = 'fail';
            } elseif ($usedPct >= $warnThreshold && $worst === 'pass') {
                $worst = 'warn';
            }
        }

        $summary = implode(' | ', $rows);

        $this->result('disk', $worst, $summary, ['paths' => $rows]);
    }

    private function checkLoad(): void
    {
        if (PHP_OS_FAMILY === 'Windows') {
            $this->warn_check('load', 'Load average check not supported on Windows. Run on Linux server for real metrics.');
            return;
        }

        try {
            // sys_getloadavg() returns 1/5/15-min averages
            $load = sys_getloadavg();

            if (! is_array($load) || count($load) < 3) {
                $this->warn_check('load', 'sys_getloadavg() returned unexpected result.');
                return;
            }

            [$l1, $l5, $l15] = $load;
            $l1  = round((float) $l1,  2);
            $l5  = round((float) $l5,  2);
            $l15 = round((float) $l15, 2);

            // Determine CPU count for relative comparison
            $cpuCount = 1;
            if (function_exists('exec')) {
                $cpuOut = [];
                @exec('nproc 2>/dev/null', $cpuOut, $cpuRc);
                if ($cpuRc === 0 && isset($cpuOut[0]) && (int) $cpuOut[0] > 0) {
                    $cpuCount = (int) $cpuOut[0];
                }
            }

            $relLoad = round($l1 / $cpuCount * 100, 1);
            $details = [
                'load_1'    => $l1,
                'load_5'    => $l5,
                'load_15'   => $l15,
                'cpu_count' => $cpuCount,
                'load_pct'  => $relLoad,
            ];

            $msg = "1 min: {$l1} | 5 min: {$l5} | 15 min: {$l15} | CPUs: {$cpuCount} ({$relLoad}% relative load).";

            if ($relLoad >= 200) {
                $this->failCheck('load', "System overloaded — {$relLoad}% of CPU capacity. {$msg}", $details);
            } elseif ($relLoad >= 100) {
                $this->warn_check('load', "Load exceeds CPU count — {$relLoad}% of capacity. {$msg}", $details);
            } else {
                $this->pass('load', $msg, $details);
            }
        } catch (Throwable $e) {
            $this->warn_check('load', 'Load check failed: ' . $e->getMessage());
        }
    }

    private function checkS3(): void
    {
        try {
            $key    = config('filesystems.disks.s3.key');
            $bucket = config('filesystems.disks.s3.bucket');
            $endpoint = config('filesystems.disks.s3.endpoint');

            if (empty($key) || empty($bucket)) {
                $this->warn_check('s3', 'S3/Contabo not configured — AWS_ACCESS_KEY_ID or AWS_BUCKET env vars missing.');
                return;
            }

            // Force throw=true so suppressed errors surface as exceptions.
            $disk = Storage::build([
                ...config('filesystems.disks.s3'),
                'throw'  => true,
                'report' => false,
            ]);

            $probeKey = '_health_probe_' . time() . '.txt';
            $payload  = 'healthcheck:' . now()->toIso8601String();

            // Write
            $disk->put($probeKey, $payload);

            // Read back
            $readBack = $disk->get($probeKey);

            // Delete (best-effort)
            try { $disk->delete($probeKey); } catch (Throwable) {}

            if ($readBack !== $payload) {
                $this->failCheck('s3', "S3/Contabo probe file read-back mismatch (bucket: {$bucket}, endpoint: {$endpoint}).", ['bucket' => $bucket, 'endpoint' => $endpoint]);
                return;
            }

            $this->pass('s3', "S3/Contabo write+read+delete OK (bucket: {$bucket}, endpoint: {$endpoint}).", ['bucket' => $bucket, 'endpoint' => $endpoint]);
        } catch (Throwable $e) {
            $this->failCheck('s3', 'S3/Contabo check failed: ' . $e->getMessage(), ['endpoint' => config('filesystems.disks.s3.endpoint', '')]);
        }
    }

    private function checkFailedJobs(): void
    {
        try {
            $count = DB::table('failed_jobs')->count();
            if ($count === 0) {
                $this->pass('failed_jobs', 'No failed jobs.');
            } else {
            $this->warn_check('failed_jobs', "{$count} failed job(s) in the `failed_jobs` table. Run `php artisan queue:flush` to clear after investigation.", ['count' => $count]);
            }
        } catch (Throwable $e) {
            $this->warn_check('failed_jobs', 'Could not query failed_jobs table: ' . $e->getMessage());
        }
    }

    private function checkQueueBacklog(): void
    {
        try {
            $count = DB::table('jobs')->count();
            $details = ['count' => $count];
            $msg = "{$count} job(s) pending in the `jobs` table.";

            if ($count >= 5000) {
                $this->failCheck('queue_backlog', "Queue backlog critical — {$msg}", $details);
            } elseif ($count >= 500) {
                $this->warn_check('queue_backlog', "Queue backlog high — {$msg}", $details);
            } else {
                $this->pass('queue_backlog', $msg, $details);
            }
        } catch (Throwable $e) {
            $this->warn_check('queue_backlog', 'Could not query jobs table: ' . $e->getMessage());
        }
    }

    private function checkSsl(): void
    {
        $appUrl = rtrim((string) config('app.url', ''), '/');

        if (! str_starts_with($appUrl, 'https://')) {
            $this->warn_check('ssl', "APP_URL (`{$appUrl}`) is not HTTPS — SSL check skipped.");
            return;
        }

        $host = parse_url($appUrl, PHP_URL_HOST);

        if (empty($host)) {
            $this->warn_check('ssl', 'Could not parse host from APP_URL.');
            return;
        }

        try {
            $context = stream_context_create(['ssl' => [
                'capture_peer_cert' => true,
                'verify_peer'       => true,
                'verify_peer_name'  => true,
                'SNI_enabled'       => true,
            ]]);

            $fp = @stream_socket_client(
                "ssl://{$host}:443",
                $errno,
                $errstr,
                10,
                STREAM_CLIENT_CONNECT,
                $context
            );

            if ($fp === false) {
                $this->failCheck('ssl', "SSL connection to {$host}:443 failed — {$errstr} [{$errno}].");
                return;
            }

            $params = stream_context_get_params($fp);
            fclose($fp);

            $cert = $params['options']['ssl']['peer_certificate'] ?? null;

            if ($cert === null) {
                $this->warn_check('ssl', "Connected to {$host}:443 but could not capture certificate.");
                return;
            }

            $certInfo = openssl_x509_parse($cert);
            $validTo  = (int) ($certInfo['validTo_time_t'] ?? 0);

            if ($validTo === 0) {
                $this->warn_check('ssl', "Could not parse certificate expiry for {$host}.");
                return;
            }

            $daysLeft = (int) ceil(($validTo - time()) / 86400);
            $expiry   = date('Y-m-d', $validTo);
            $details  = ['host' => $host, 'expires' => $expiry, 'days_left' => $daysLeft];

            if ($daysLeft <= 0) {
                $this->failCheck('ssl', "SSL certificate for {$host} has EXPIRED ({$expiry}).", $details);
            } elseif ($daysLeft <= 7) {
                $this->failCheck('ssl', "SSL certificate for {$host} expires in {$daysLeft} day(s) ({$expiry}) — renew immediately!", $details);
            } elseif ($daysLeft <= 30) {
                $this->warn_check('ssl', "SSL certificate for {$host} expires in {$daysLeft} day(s) ({$expiry}).", $details);
            } else {
                $this->pass('ssl', "SSL certificate for {$host} is valid for {$daysLeft} more day(s) (expires {$expiry}).", $details);
            }
        } catch (Throwable $e) {
            $this->failCheck('ssl', "SSL check for {$host} failed: " . $e->getMessage());
        }
    }

    private function checkScheduler(): void
    {
        // The scheduler tick key is written by the scheduled health:tick command.
        // If Redis is not the cache driver, we can't check it.
        if (config('cache.default') !== 'redis' && config('queue.default') !== 'redis') {
            $this->warn_check('scheduler', 'Scheduler check requires Redis cache or queue — skipping in this environment.');
            return;
        }

        try {
            $raw = Redis::get('health:scheduler_last_tick');

            if ($raw === null || $raw === false) {
                $this->warn_check('scheduler', 'No scheduler tick recorded yet. Ensure cron `* * * * * php artisan schedule:run` is configured and has run at least once. (Key: health:scheduler_last_tick)');
                return;
            }

            $lastTick = (int) $raw;
            $age      = time() - $lastTick;
            $details  = ['last_tick' => date('Y-m-d H:i:s', $lastTick), 'age_seconds' => $age];

            if ($age > 300) {
                $this->failCheck('scheduler', "Scheduler last ran " . gmdate('H:i:s', $age) . " ago — cron may have stopped.", $details);
            } elseif ($age > 120) {
                $this->warn_check('scheduler', "Scheduler last ran {$age}s ago (expected ≤ 60s).", $details);
            } else {
                $this->pass('scheduler', "Scheduler is ticking — last run {$age}s ago ({$details['last_tick']}).", $details);
            }
        } catch (Throwable $e) {
            $this->warn_check('scheduler', 'Scheduler check failed: ' . $e->getMessage());
        }
    }

    private function checkSitemap(): void
    {
        try {
            $releases = app(SitemapReleaseManager::class)->listReleases();

            if ($releases === []) {
                $this->failCheck('sitemap', 'No sitemap releases found. Run `php artisan skinbase:sitemaps:publish` to build one.');
                return;
            }

            $latest = $releases[0];
            $releaseId = (string) ($latest['release_id'] ?? 'unknown');
            $builtAtRaw = (string) ($latest['built_at'] ?? $latest['published_at'] ?? '');

            if ($builtAtRaw === '') {
                $this->warn_check('sitemap', "Latest sitemap release [{$releaseId}] is missing a build timestamp.", [
                    'release_id' => $releaseId,
                    'status' => (string) ($latest['status'] ?? 'unknown'),
                ]);
                return;
            }

            $builtAt = Carbon::parse($builtAtRaw);
            $ageSeconds = max(0, $builtAt->diffInSeconds(now()));
            $builtAtLabel = $builtAt->toAtomString();
            $details = [
                'release_id' => $releaseId,
                'built_at' => $builtAtLabel,
                'age_seconds' => $ageSeconds,
                'status' => (string) ($latest['status'] ?? 'unknown'),
            ];
            $message = "Latest sitemap release [{$releaseId}] built at {$builtAtLabel} ({$ageSeconds}s ago).";

            if ($ageSeconds > 72 * 3600) {
                $this->failCheck('sitemap', 'Sitemap build is stale — ' . $message, $details);
            } elseif ($ageSeconds > 36 * 3600) {
                $this->warn_check('sitemap', 'Sitemap build is getting old — ' . $message, $details);
            } else {
                $this->pass('sitemap', $message, $details);
            }
        } catch (Throwable $e) {
            $this->warn_check('sitemap', 'Could not inspect sitemap releases: ' . $e->getMessage());
        }
    }

    private function checkLogErrors(): void
    {
        $logFile = storage_path('logs/laravel.log');

        if (! file_exists($logFile)) {
            $this->warn_check('log_errors', 'laravel.log does not exist yet.');
            return;
        }

        try {
            // Read the last ~50 KB so we cover roughly 100+ log lines without loading the whole file.
            $handle = @fopen($logFile, 'r');
            if ($handle === false) {
                $this->warn_check('log_errors', 'Could not open laravel.log for reading.');
                return;
            }

            fseek($handle, max(0, filesize($logFile) - 51200)); // last ~50 KB
            $tail = fread($handle, 51200);
            fclose($handle);

            // Split into lines and take the last 100
            $lines = array_slice(explode("\n", $tail), -100);

            $errorCount    = 0;
            $criticalCount = 0;
            $lastError     = '';

            foreach ($lines as $line) {
                if (preg_match('/\.(ERROR|CRITICAL|ALERT|EMERGENCY)/', $line)) {
                    $errorCount++;
                    if (preg_match('/\.CRITICAL|\.ALERT|\.EMERGENCY/', $line)) {
                        $criticalCount++;
                    }
                    if ($lastError === '') {
                        $lastError = substr(trim($line), 0, 200);
                    }
                }
            }

            $details = ['errors' => $errorCount, 'critical' => $criticalCount, 'last_error' => $lastError];
            $suffix  = $lastError !== '' ? " Last: {$lastError}" : '';

            if ($criticalCount > 0) {
                $this->warn_check('log_errors', "{$criticalCount} CRITICAL/ALERT/EMERGENCY and {$errorCount} total error line(s) in last 100 log lines.{$suffix}", $details);
            } elseif ($errorCount > 0) {
                $this->warn_check('log_errors', "{$errorCount} ERROR line(s) in last 100 log lines.{$suffix}", $details);
            } else {
                $this->pass('log_errors', 'No ERROR/CRITICAL entries in last 100 log lines.', $details);
            }
        } catch (Throwable $e) {
            $this->warn_check('log_errors', 'Log check failed: ' . $e->getMessage());
        }
    }

    private function checkApp(): void
    {
        $appUrl = rtrim((string) config('app.url', ''), '/');

        if (empty($appUrl) || str_contains($appUrl, '.test') || str_contains($appUrl, 'localhost')) {
            $this->warn_check('app', "APP_URL is `{$appUrl}` — looks like a local/dev URL, skipping HTTP probe.");
            return;
        }

        // Probe the app homepage
        $response = $this->httpGet($appUrl . '/', 10);
        $code     = $response['code'] ?? 0;

        if ($code === 200) {
            $ttfb = $response['ttfb'] ?? 0;
            $this->pass('app', "Homepage responded HTTP 200. TTFB: {$ttfb}ms.", ['url' => $appUrl, 'ttfb_ms' => $ttfb]);
        } elseif ($code > 0) {
            $this->warn_check('app', "Homepage returned HTTP {$code}. URL: {$appUrl}", ['url' => $appUrl, 'http_code' => $code]);
        } else {
            $this->failCheck('app', "Homepage unreachable. URL: {$appUrl}");
        }
    }

    // ── Helpers ────────────────────────────────────────────────────────────────

    private function httpGet(string $url, int $timeout = 5): array
    {
        $start = microtime(true);
        try {
            $ch = curl_init($url);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT        => $timeout,
                CURLOPT_CONNECTTIMEOUT => 3,
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_MAXREDIRS      => 3,
                CURLOPT_SSL_VERIFYPEER => true,
                CURLOPT_USERAGENT      => 'SkinbaseHealthCheck/1.0',
                CURLOPT_HTTPHEADER     => ['Accept: application/json, text/html'],
                CURLOPT_HEADERFUNCTION => function ($ch, $header) use (&$responseHeaders) {
                    $responseHeaders[] = $header;
                    return strlen($header);
                },
            ]);
            $responseHeaders = [];
            $body = curl_exec($ch);
            $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);
            $ttfb = (int) round((microtime(true) - $start) * 1000);

            // Extract Server header for Apache detection
            $serverHeader = '';
            foreach ($responseHeaders as $h) {
                if (stripos($h, 'Server:') === 0) {
                    $serverHeader = trim(substr($h, 7));
                    break;
                }
            }

            return ['code' => $code, 'body' => $body ?: '', 'ttfb' => $ttfb, 'server' => $serverHeader];
        } catch (Throwable) {
            return ['code' => 0, 'body' => '', 'ttfb' => 0, 'server' => ''];
        }
    }

    /**
     * httpGet variant that sends the vector gateway auth header.
     */
    private function httpGetWithAuth(string $url, string $apiKey, int $timeout = 5): array
    {
        $start = microtime(true);
        try {
            $headers = ['Accept: application/json'];
            if (! empty($apiKey)) {
                $headers[] = "X-API-Key: {$apiKey}";
            }

            $ch = curl_init($url);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT        => $timeout,
                CURLOPT_CONNECTTIMEOUT => 3,
                CURLOPT_FOLLOWLOCATION => false,
                CURLOPT_SSL_VERIFYPEER => true,
                CURLOPT_USERAGENT      => 'SkinbaseHealthCheck/1.0',
                CURLOPT_HTTPHEADER     => $headers,
            ]);
            $body = curl_exec($ch);
            $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);
            $ttfb = (int) round((microtime(true) - $start) * 1000);
            return ['code' => $code, 'body' => $body ?: '', 'ttfb' => $ttfb];
        } catch (Throwable) {
            return ['code' => 0, 'body' => '', 'ttfb' => 0];
        }
    }

    private function pass(string $name, string $message, array $details = []): void
    {
        $this->result($name, 'pass', $message, $details);
    }

    private function failCheck(string $name, string $message, array $details = []): void
    {
        $this->result($name, 'fail', $message, $details);
    }

    private function warn_check(string $name, string $message, array $details = []): void
    {
        $this->result($name, 'warn', $message, $details);
    }

    private function result(string $name, string $status, string $message, array $details = []): void
    {
        $this->results[$name] = [
            'status'  => $status,
            'message' => $message,
            'details' => $details,
        ];
    }

    private function renderTable(): void
    {
        $this->newLine();
        $this->line(' <fg=white;options=bold>SERVICE         STATUS   MESSAGE</>');
        $this->line(' ' . str_repeat('─', 90));

        foreach ($this->results as $name => $r) {
            [$icon, $color] = match ($r['status']) {
                'pass'  => ['✅', 'green'],
                'warn'  => ['⚠️ ', 'yellow'],
                default => ['❌', 'red'],
            };

            $label   = str_pad(strtoupper($name), 15);
            $status  = str_pad(strtoupper($r['status']), 7);
            $message = $r['message'];

            $this->line(" {$icon} <fg={$color}>{$label} {$status}</> {$message}");
        }

        $this->line(' ' . str_repeat('─', 90));
    }

    private function hasFailures(): bool
    {
        return $this->countByStatus('fail') > 0;
    }

    private function countByStatus(string $status): int
    {
        return count(array_filter($this->results, fn ($r) => $r['status'] === $status));
    }
}
