<?php

declare(strict_types=1);

namespace App\Console\Commands;

use App\Models\Artwork;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
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|reverb|vision|horizon|app)}
        {--json  : Output results as JSON}';

    protected $description = 'Check health of all critical services (MySQL, Redis, Cache, Meilisearch, Reverb, Vision, Horizon, 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(),
            'reverb'      => fn () => $this->checkReverb(),
            'vision'      => fn () => $this->checkVision(),
            'horizon'     => fn () => $this->checkHorizon(),
            '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';
            $indexName  = (new Artwork())->searchableAs();
            $index      = $client->index($indexName);
            $stats      = $index->stats();
            $docCount   = (int) ($stats['numberOfDocuments'] ?? 0);
            $isIndexing = (bool) ($stats['isIndexing'] ?? false);

            // Expected: ≥ 50% of DB artworks should be indexed
            $dbCount = DB::table('artworks')
                ->whereNull('deleted_at')
                ->where('is_public', 1)
                ->where('is_approved', 1)
                ->count();

            $status  = 'pass';
            $message = "v{$version}. Index `{$indexName}`: {$docCount} docs (DB public+approved: {$dbCount}).";

            if ($isIndexing) {
                $message .= ' [currently re-indexing]';
            }

            if ($docCount === 0) {
                $status  = 'fail';
                $message = "Index `{$indexName}` is EMPTY (DB has {$dbCount} public+approved artworks). Run: php artisan scout:import \"App\\\\Models\\\\Artwork\"";
            } elseif ($dbCount > 0 && $docCount < (int) ($dbCount * 0.5)) {
                $status  = 'warn';
                $message .= " — indexed count is < 50% of DB count. Index may be stale. Run: php artisan artworks:search-rebuild";
            }

            // Check pending Meilisearch tasks
            try {
                $tasks = $client->getTasks(['statuses' => 'enqueued,processing']);
                $pendingCount = $tasks->getTotal();
                if ($pendingCount > 0) {
                    $message .= " ({$pendingCount} tasks still pending)";
                }
            } catch (Throwable) {
                // non-fatal
            }

            $this->result('meilisearch', $status, $message, [
                'index'           => $indexName,
                'indexed_docs'    => $docCount,
                'db_eligible'     => $dbCount,
                'is_indexing'     => $isIndexing,
                'meili_version'   => $version,
            ]);
        } catch (Throwable $e) {
            $this->failCheck('meilisearch', 'Unreachable or error: ' . $e->getMessage());
        }
    }

    private function checkReverb(): void
    {
        $host   = config('reverb.servers.reverb.options.host') ?? env('REVERB_HOST', '');
        $port   = (int) (config('reverb.servers.reverb.options.port') ?? env('REVERB_PORT', 443));
        $scheme = config('reverb.servers.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);

            // Try the HTTP health probe (Reverb answers 200 on /)
            $url      = "{$scheme}://{$host}:{$port}/";
            $response = $this->httpGet($url, 3);
            $code     = $response['code'] ?? 0;

            // Reverb returns 200 or 400 on the root — both mean it's alive
            if ($code >= 200 && $code < 500) {
                $this->pass('reverb', "Reachable at {$host}:{$port} (HTTP {$code}).", ['host' => $host, 'port' => $port]);
            } else {
                $this->warn_check('reverb', "TCP open but HTTP returned {$code}.", ['host' => $host, 'port' => $port]);
            }
        } 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 checkHorizon(): void
    {
        try {
            // 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) {
                $this->warn_check('horizon', 'No Horizon status key in Redis — 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 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'],
            ]);
            $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));
    }
}
