<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Schema;
use Throwable;

class OperationsHealthCheck extends Command
{
    protected $signature = 'operations:health-check {--json : Output machine-readable JSON}';

    protected $description = 'Check production database, queue failures, scheduler liveness, disk space, and backup freshness';

    public function handle(): int
    {
        $checks = [
            'database' => $this->checkDatabase(),
            'failed_queue_jobs' => $this->checkFailedQueueJobs(),
            'scheduler' => $this->checkScheduler(),
            'disk_space' => $this->checkDiskSpace(),
            'backup' => $this->checkBackup(),
        ];

        $healthy = collect($checks)->every(fn (array $check): bool => $check['status'] !== 'fail');
        $report = [
            'schema_version' => 1,
            'status' => $healthy ? 'healthy' : 'unhealthy',
            'checked_at' => now()->utc()->toIso8601String(),
            'checks' => $checks,
        ];

        if ($this->option('json')) {
            $this->line(json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR));
        } else {
            foreach ($checks as $name => $check) {
                $this->line(strtoupper($check['status']).': '.$name.' - '.$check['message']);
            }

            $this->newLine();
            $healthy
                ? $this->info('Operational health check passed.')
                : $this->error('Operational health check failed.');
        }

        return $healthy ? self::SUCCESS : self::FAILURE;
    }

    /** @return array{status:string,message:string,meta?:array<string,mixed>} */
    private function checkDatabase(): array
    {
        try {
            DB::select('SELECT 1');

            return $this->result('pass', 'Database connectivity is healthy.', [
                'connection' => (string) config('database.default'),
            ]);
        } catch (Throwable $exception) {
            return $this->result('fail', 'Database connectivity check failed.', [
                'error' => class_basename($exception),
            ]);
        }
    }

    /** @return array{status:string,message:string,meta?:array<string,mixed>} */
    private function checkFailedQueueJobs(): array
    {
        try {
            if (! Schema::hasTable('failed_jobs')) {
                return $this->result('fail', 'The failed_jobs table is missing.');
            }

            $windowMinutes = (int) config('operations.failed_jobs_window_minutes', 60);
            $maximum = (int) config('operations.failed_jobs_max', 0);
            $since = now()->subMinutes($windowMinutes);
            $count = DB::table('failed_jobs')->where('failed_at', '>=', $since)->count();

            return $this->result(
                $count > $maximum ? 'fail' : 'pass',
                $count > $maximum
                    ? "{$count} failed queue job(s) detected in the last {$windowMinutes} minute(s)."
                    : "No excessive queue failures detected in the last {$windowMinutes} minute(s).",
                [
                    'recent_failed_jobs' => $count,
                    'window_minutes' => $windowMinutes,
                    'maximum_allowed' => $maximum,
                ]
            );
        } catch (Throwable $exception) {
            return $this->result('fail', 'Unable to inspect failed queue jobs.', [
                'error' => class_basename($exception),
            ]);
        }
    }

    /** @return array{status:string,message:string,meta?:array<string,mixed>} */
    private function checkScheduler(): array
    {
        $maximumAge = (int) config('operations.scheduler_max_age_seconds', 180);

        try {
            $heartbeat = Cache::get(OperationsSchedulerHeartbeat::CACHE_KEY);
        } catch (Throwable $exception) {
            return $this->result('fail', 'Unable to read scheduler heartbeat.', [
                'maximum_age_seconds' => $maximumAge,
                'error' => class_basename($exception),
            ]);
        }

        if (! is_string($heartbeat) || trim($heartbeat) === '') {
            return $this->result('fail', 'Scheduler heartbeat is missing.', [
                'maximum_age_seconds' => $maximumAge,
            ]);
        }

        try {
            $timestamp = Carbon::parse($heartbeat)->utc();
            $age = now()->utc()->getTimestamp() - $timestamp->getTimestamp();
        } catch (Throwable $exception) {
            return $this->result('fail', 'Scheduler heartbeat is invalid.', [
                'maximum_age_seconds' => $maximumAge,
            ]);
        }

        if ($age < -60) {
            return $this->result('fail', 'Scheduler heartbeat is unexpectedly in the future.', [
                'clock_skew_seconds' => abs($age),
                'heartbeat_at' => $timestamp->toIso8601String(),
            ]);
        }

        $age = max(0, $age);

        return $this->result(
            $age > $maximumAge ? 'fail' : 'pass',
            $age > $maximumAge
                ? "Scheduler heartbeat is stale ({$age} seconds old)."
                : "Scheduler heartbeat is fresh ({$age} seconds old).",
            [
                'age_seconds' => $age,
                'maximum_age_seconds' => $maximumAge,
                'heartbeat_at' => $timestamp->toIso8601String(),
            ]
        );
    }

    /** @return array{status:string,message:string,meta?:array<string,mixed>} */
    private function checkDiskSpace(): array
    {
        $path = storage_path();
        $minimumMb = (int) config('operations.disk_min_free_mb', 1024);
        $freeBytes = @disk_free_space($path);

        if ($freeBytes === false) {
            return $this->result('fail', 'Unable to read free disk space.', [
                'minimum_free_mb' => $minimumMb,
            ]);
        }

        $freeMb = (int) floor($freeBytes / 1024 / 1024);

        return $this->result(
            $freeMb < $minimumMb ? 'fail' : 'pass',
            $freeMb < $minimumMb
                ? "Free disk space is below the configured threshold ({$freeMb} MB available)."
                : "Free disk space is healthy ({$freeMb} MB available).",
            [
                'free_mb' => $freeMb,
                'minimum_free_mb' => $minimumMb,
            ]
        );
    }

    /** @return array{status:string,message:string,meta?:array<string,mixed>} */
    private function checkBackup(): array
    {
        if (! (bool) config('backups.schedule_enabled', false)) {
            return $this->result('warn', 'Scheduled backups are disabled; backup freshness is not being enforced.');
        }

        $maximumAgeHours = (int) config('operations.backup_max_age_hours', 26);

        try {
            $root = rtrim((string) config('backups.path', storage_path('backups')), DIRECTORY_SEPARATOR);
            if (! File::isDirectory($root)) {
                return $this->result('fail', 'Backup destination is missing.', [
                    'maximum_age_hours' => $maximumAgeHours,
                ]);
            }

            $directories = File::directories($root);
            if ($directories === []) {
                return $this->result('fail', 'No backup bundle exists.', [
                    'maximum_age_hours' => $maximumAgeHours,
                ]);
            }

            usort($directories, fn (string $left, string $right): int => File::lastModified($right) <=> File::lastModified($left));
            $latest = $directories[0];
            $manifestPath = $latest.DIRECTORY_SEPARATOR.'manifest.json';

            if (! File::isFile($manifestPath)) {
                return $this->result('fail', 'Latest backup bundle has no manifest.', [
                    'maximum_age_hours' => $maximumAgeHours,
                ]);
            }

            $manifest = json_decode(File::get($manifestPath), true, flags: JSON_THROW_ON_ERROR);
            $createdAtValue = is_array($manifest) ? (string) ($manifest['created_at'] ?? '') : '';
            if (($manifest['format_version'] ?? null) !== 1 || trim($createdAtValue) === '') {
                return $this->result('fail', 'Latest backup manifest is invalid.', [
                    'maximum_age_hours' => $maximumAgeHours,
                ]);
            }

            $createdAt = Carbon::parse($createdAtValue)->utc();
            $ageSeconds = now()->utc()->getTimestamp() - $createdAt->getTimestamp();
            if ($ageSeconds < -300) {
                return $this->result('fail', 'Latest backup timestamp is unexpectedly in the future.', [
                    'clock_skew_seconds' => abs($ageSeconds),
                    'created_at' => $createdAt->toIso8601String(),
                ]);
            }

            $ageHours = round(max(0, $ageSeconds) / 3600, 2);
        } catch (Throwable $exception) {
            return $this->result('fail', 'Unable to inspect the latest backup.', [
                'maximum_age_hours' => $maximumAgeHours,
                'error' => class_basename($exception),
            ]);
        }

        return $this->result(
            $ageHours > $maximumAgeHours ? 'fail' : 'pass',
            $ageHours > $maximumAgeHours
                ? "Latest backup is stale ({$ageHours} hours old)."
                : "Latest backup is fresh ({$ageHours} hours old).",
            [
                'age_hours' => $ageHours,
                'maximum_age_hours' => $maximumAgeHours,
                'created_at' => $createdAt->toIso8601String(),
            ]
        );
    }

    /**
     * @param array<string,mixed> $meta
     * @return array{status:string,message:string,meta?:array<string,mixed>}
     */
    private function result(string $status, string $message, array $meta = []): array
    {
        $result = ['status' => $status, 'message' => $message];

        if ($meta !== []) {
            $result['meta'] = $meta;
        }

        return $result;
    }
}
