<?php

namespace App\Console\Commands;

use App\Jobs\ReleaseQueueProbe;
use App\Services\BackupService;
use Carbon\CarbonImmutable;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
use Symfony\Component\Process\Process;

class ReleaseVerify extends Command
{
    protected $signature = 'release:verify
        {--output= : Release manifest path; defaults to storage/app/release-manifest.json}
        {--require-backup : Require and verify the newest backup bundle}
        {--skip-queue-probe : Skip the live queue-worker execution probe}
        {--skip-scheduler-heartbeat : Skip scheduler heartbeat freshness verification}
        {--queue-timeout=15 : Seconds to wait for the queue probe}
        {--scheduler-max-age=180 : Maximum accepted scheduler heartbeat age in seconds}';

    protected $description = 'Verify production release runtime health and write a non-secret release manifest';

    public function handle(BackupService $backups): int
    {
        $checks = [];
        $errors = [];
        $warnings = [];

        $productionExit = Artisan::call('production:check');
        $this->record(
            $checks,
            $errors,
            'production_configuration',
            $productionExit === self::SUCCESS,
            $productionExit === self::SUCCESS
                ? 'production:check passed'
                : 'production:check failed; run it directly for detailed configuration errors'
        );

        try {
            DB::select('select 1');
            $this->record($checks, $errors, 'database_connectivity', true, 'Database connection responded.');
        } catch (\Throwable $exception) {
            $this->record($checks, $errors, 'database_connectivity', false, 'Database connection failed: '.$exception->getMessage());
        }

        $this->verifyQueueTables($checks, $errors);
        $this->verifyScheduleRegistration($checks, $errors);

        if (! $this->option('skip-scheduler-heartbeat')) {
            $this->verifySchedulerHeartbeat($checks, $errors);
        } else {
            $warnings[] = 'Scheduler heartbeat verification was skipped.';
        }

        if (! $this->option('skip-queue-probe')) {
            $this->verifyQueueProbe($checks, $errors);
        } else {
            $warnings[] = 'Queue-worker execution probe was skipped.';
        }

        $backup = $this->verifyLatestBackup($backups, (bool) $this->option('require-backup'), $checks, $errors, $warnings);

        $output = $this->manifestPath();
        if ($this->isInside($output, public_path())) {
            $errors[] = 'Release manifest output must be outside the public web root.';
            $output = storage_path('app/release-manifest.json');
        }

        $manifest = [
            'schema_version' => 1,
            'status' => $errors === [] ? 'passed' : 'failed',
            'generated_at' => now()->utc()->toIso8601String(),
            'git_sha' => $this->gitSha(),
            'application' => [
                'name' => (string) config('app.name'),
                'environment' => (string) config('app.env'),
                'url' => (string) config('app.url'),
                'timezone' => (string) config('app.timezone'),
            ],
            'runtime' => [
                'php' => PHP_VERSION,
                'database_driver' => (string) config('database.default'),
                'queue_connection' => (string) config('queue.default'),
                'cache_store' => (string) config('cache.default'),
                'backup_schedule_enabled' => (bool) config('backups.schedule_enabled'),
            ],
            'backup' => $backup,
            'checks' => $checks,
            'warnings' => $warnings,
            'errors' => $errors,
        ];

        File::ensureDirectoryExists(dirname($output), 0700, true);
        File::put($output, json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR).PHP_EOL);
        @chmod($output, 0600);

        foreach ($warnings as $warning) {
            $this->warn('WARN: '.$warning);
        }

        foreach ($errors as $error) {
            $this->error('FAIL: '.$error);
        }

        $this->line('Release manifest: '.$output);
        $this->line('Git SHA: '.$manifest['git_sha']);

        if ($errors !== []) {
            $this->error('Release verification failed.');

            return self::FAILURE;
        }

        $this->info('Release verification passed.');

        return self::SUCCESS;
    }

    private function verifyQueueTables(array &$checks, array &$errors): void
    {
        if (config('queue.default') !== 'database') {
            $this->record($checks, $errors, 'queue_storage', false, 'Queue connection is not database.');

            return;
        }

        try {
            $jobsTable = (string) config('queue.connections.database.table', 'jobs');
            $failedTable = (string) config('queue.failed.table', 'failed_jobs');
            $pending = (int) DB::table($jobsTable)->count();
            $failed = (int) DB::table($failedTable)->count();

            $this->record(
                $checks,
                $errors,
                'queue_storage',
                true,
                "Database queue tables are available; pending={$pending}, failed={$failed}."
            );
        } catch (\Throwable $exception) {
            $this->record($checks, $errors, 'queue_storage', false, 'Database queue tables are unavailable: '.$exception->getMessage());
        }
    }

    private function verifyScheduleRegistration(array &$checks, array &$errors): void
    {
        $expected = [
            'operations:scheduler-heartbeat',
            'loans:update-overdue',
            'management:process-work',
            'expenses:process-recurring',
            'notifications:dispatch-reminders',
        ];

        if ((bool) config('backups.schedule_enabled')) {
            $expected[] = 'security:backup';
        }

        $commands = collect(app(Schedule::class)->events())
            ->map(fn ($event): string => (string) $event->command)
            ->all();

        $missing = collect($expected)
            ->reject(fn (string $command): bool => collect($commands)->contains(
                fn (string $registered): bool => Str::contains($registered, $command)
            ))
            ->values()
            ->all();

        $this->record(
            $checks,
            $errors,
            'scheduler_registration',
            $missing === [],
            $missing === []
                ? 'Required production schedules are registered.'
                : 'Missing scheduled commands: '.implode(', ', $missing)
        );
    }

    private function verifySchedulerHeartbeat(array &$checks, array &$errors): void
    {
        $value = Cache::get(OperationsSchedulerHeartbeat::CACHE_KEY);
        if (! is_string($value) || $value === '') {
            $this->record($checks, $errors, 'scheduler_heartbeat', false, 'No scheduler heartbeat is available.');

            return;
        }

        try {
            $heartbeat = CarbonImmutable::parse($value);
            $age = (int) abs(now()->utc()->diffInSeconds($heartbeat, false));
            $maxAge = max(30, (int) $this->option('scheduler-max-age'));

            $this->record(
                $checks,
                $errors,
                'scheduler_heartbeat',
                $age <= $maxAge,
                "Latest scheduler heartbeat age is {$age}s; maximum is {$maxAge}s."
            );
        } catch (\Throwable $exception) {
            $this->record($checks, $errors, 'scheduler_heartbeat', false, 'Scheduler heartbeat is invalid: '.$exception->getMessage());
        }
    }

    private function verifyQueueProbe(array &$checks, array &$errors): void
    {
        if (config('queue.default') !== 'database') {
            $this->record($checks, $errors, 'queue_worker_probe', false, 'Queue probe requires the database queue connection.');

            return;
        }

        $token = (string) Str::uuid();
        $key = ReleaseQueueProbe::cacheKey($token);
        Cache::forget($key);
        ReleaseQueueProbe::dispatch($token);

        $timeout = max(1, min(60, (int) $this->option('queue-timeout')));
        $deadline = microtime(true) + $timeout;
        $processedAt = null;

        while (microtime(true) < $deadline) {
            $processedAt = Cache::get($key);
            if (is_string($processedAt) && $processedAt !== '') {
                break;
            }
            usleep(250000);
        }

        Cache::forget($key);

        $this->record(
            $checks,
            $errors,
            'queue_worker_probe',
            is_string($processedAt) && $processedAt !== '',
            is_string($processedAt) && $processedAt !== ''
                ? 'Queue worker executed the release probe at '.$processedAt.'.'
                : "Queue worker did not execute the release probe within {$timeout}s."
        );
    }

    private function verifyLatestBackup(
        BackupService $backups,
        bool $required,
        array &$checks,
        array &$errors,
        array &$warnings,
    ): ?array {
        $root = (string) config('backups.path');
        $directories = File::isDirectory($root) ? File::directories($root) : [];
        usort($directories, fn (string $a, string $b): int => File::lastModified($b) <=> File::lastModified($a));
        $latest = $directories[0] ?? null;

        if ($latest === null) {
            if ($required) {
                $this->record($checks, $errors, 'latest_backup', false, 'No backup bundle exists in BACKUP_PATH.');
            } else {
                $warnings[] = 'No backup bundle was available for release verification.';
            }

            return null;
        }

        try {
            $verified = $backups->verify($latest);
            $this->record($checks, $errors, 'latest_backup', true, 'Newest backup bundle passed checksum verification.');

            return [
                'bundle' => basename($latest),
                'verified' => true,
                'database_driver' => (string) ($verified['database']['driver'] ?? ''),
                'private_files' => (int) $verified['private_files'],
                'private_bytes' => (int) $verified['bytes'],
                'modified_at' => CarbonImmutable::createFromTimestamp(File::lastModified($latest))->utc()->toIso8601String(),
            ];
        } catch (\Throwable $exception) {
            $this->record($checks, $errors, 'latest_backup', false, 'Newest backup bundle failed verification: '.$exception->getMessage());

            return [
                'bundle' => basename($latest),
                'verified' => false,
            ];
        }
    }

    private function record(array &$checks, array &$errors, string $name, bool $passed, string $detail): void
    {
        $checks[$name] = [
            'status' => $passed ? 'pass' : 'fail',
            'detail' => $detail,
        ];

        if (! $passed) {
            $errors[] = $detail;
        }
    }

    private function manifestPath(): string
    {
        $path = trim((string) $this->option('output'));
        if ($path === '') {
            return storage_path('app/release-manifest.json');
        }

        return $this->isAbsolutePath($path) ? $path : base_path($path);
    }

    private function gitSha(): string
    {
        $environment = trim((string) getenv('RELEASE_GIT_SHA'));
        if ($environment !== '') {
            return $environment;
        }

        try {
            $process = new Process(['git', 'rev-parse', 'HEAD'], base_path());
            $process->setTimeout(5);
            $process->run();
            if ($process->isSuccessful()) {
                $sha = trim($process->getOutput());
                if ($sha !== '') {
                    return $sha;
                }
            }
        } catch (\Throwable) {
            // A packaged release may intentionally exclude Git metadata.
        }

        return 'unknown';
    }

    private function isAbsolutePath(string $path): bool
    {
        return str_starts_with($path, DIRECTORY_SEPARATOR)
            || (bool) preg_match('/^[A-Za-z]:[\\\\\/]/', $path);
    }

    private function isInside(string $path, string $root): bool
    {
        $root = rtrim(str_replace('\\', '/', $root), '/').'/';
        $candidate = rtrim(str_replace('\\', '/', $path), '/').'/';

        return str_starts_with($candidate, $root);
    }
}
