<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Str;

class ProductionReadinessCheck extends Command
{
    protected $signature = 'production:check';

    protected $description = 'Fail fast when production runtime configuration contains unsafe or incomplete defaults';

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

        $this->require($errors, config('app.env') === 'production', 'APP_ENV must be production.');
        $this->require($errors, filled(config('app.key')), 'APP_KEY is missing. Generate and securely retain a production key.');
        $this->require($errors, config('app.debug') === false, 'APP_DEBUG must be false.');
        $this->require($errors, $this->isHttpsUrl((string) config('app.url')), 'APP_URL must be a valid HTTPS URL.');
        $this->require($errors, config('database.default') === 'mysql', 'DB_CONNECTION must be mysql for the supported production deployment.');
        $this->require($errors, config('queue.default') === 'database', 'QUEUE_CONNECTION must be database so jobs are handled by the supervised worker.');

        $this->require($errors, config('session.encrypt') === true, 'SESSION_ENCRYPT must be true.');
        $this->require($errors, config('session.secure') === true, 'SESSION_SECURE_COOKIE must be true under HTTPS.');
        $this->require($errors, config('session.http_only') === true, 'SESSION_HTTP_ONLY must be true.');
        $this->require($errors, config('session.same_site') === 'strict', 'SESSION_SAME_SITE must be strict unless an integration exception is explicitly reviewed.');

        $this->checkCreditScoreConfiguration($errors);

        $backupPath = (string) config('backups.path');
        $this->require($errors, $backupPath !== '', 'BACKUP_PATH must resolve to a non-empty path.');
        $this->require($errors, $this->isAbsolutePath($backupPath), 'BACKUP_PATH must be an absolute path on the production server.');
        $this->require($errors, ! $this->isInside($backupPath, public_path()), 'BACKUP_PATH must be outside the public web root.');
        $this->require($errors, ! $this->isInside($backupPath, storage_path('app/private')), 'BACKUP_PATH must be outside private document storage.');

        if ((bool) config('backups.schedule_enabled')) {
            $this->require($errors, is_dir($backupPath), 'Scheduled backups are enabled but BACKUP_PATH does not exist.');
            $this->require($errors, is_writable($backupPath), 'Scheduled backups are enabled but BACKUP_PATH is not writable.');
            $this->require($errors, filled(config('backups.mysqldump_binary')), 'MYSQLDUMP_BINARY must be configured for MySQL backups.');
        } else {
            $warnings[] = 'Scheduled backups are disabled. Enable them only after the protected backup destination is verified.';
        }

        if (config('mail.default') === 'log') {
            $warnings[] = 'MAIL_MAILER is still log. Configure a real transport if production email is required.';
        }

        if (config('cache.default') === 'array') {
            $warnings[] = 'CACHE_STORE is array. Use a persistent production cache such as database or Redis.';
        }

        $this->newLine();
        $this->info('Production readiness configuration check');

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

        if ($errors !== []) {
            foreach ($errors as $error) {
                $this->error('FAIL: '.$error);
            }

            $this->newLine();
            $this->error('Production configuration is NOT ready.');

            return self::FAILURE;
        }

        $this->newLine();
        $this->info('Production configuration gate passed.');
        $this->line('Next: verify /up, queue worker, scheduler, backup/restore, external providers, and run credit-score:production-check.');

        return self::SUCCESS;
    }

    private function checkCreditScoreConfiguration(array &$errors): void
    {
        $version = trim((string) config('credit_score.version', ''));
        $minimum = (int) config('credit_score.score.minimum', 0);
        $maximum = (int) config('credit_score.score.maximum', 0);
        $weights = config('credit_score.weights', []);
        $bands = config('credit_score.bands', []);
        $expectedComponents = [
            'repayment_behaviour',
            'loan_history',
            'affordability',
            'collateral',
            'profile_completeness',
            'conduct_flags',
        ];

        $this->require($errors, $version !== '', 'Credit-score version must be configured.');
        $this->require($errors, $minimum === 300 && $maximum === 850, 'Credit-score V1 display range must remain 300-850.');
        $this->require($errors, is_array($weights), 'Credit-score component weights must be configured as an array.');

        if (is_array($weights)) {
            $this->require(
                $errors,
                array_values(array_keys($weights)) === $expectedComponents,
                'Credit-score V1 component set or ordering changed unexpectedly. Introduce a new score version instead of mutating V1.',
            );
            $this->require($errors, array_sum(array_map('intval', $weights)) === 100, 'Credit-score component weights must total 100.');
            $this->require($errors, collect($weights)->every(fn ($weight) => is_numeric($weight) && (float) $weight >= 0), 'Credit-score weights must be non-negative numeric values.');
        }

        $this->require($errors, $this->bandsCoverRange($bands, $minimum, $maximum), 'Credit-score bands must cover 300-850 exactly once with valid risk levels.');
        $this->require($errors, (int) config('credit_score.operations.stale_after_hours', 0) > 0, 'Credit-score stale refresh window must be positive.');
        $this->require($errors, (int) config('credit_score.operations.refresh_chunk_size', 0) > 0, 'Credit-score refresh chunk size must be positive.');
        $this->require($errors, (int) config('credit_score.monitoring.recent_window_days', 0) > 0, 'Credit-score monitoring window must be positive.');
        $this->require($errors, (int) config('credit_score.monitoring.minimum_sample_size', 0) > 0, 'Credit-score monitoring minimum sample size must be positive.');
    }

    private function bandsCoverRange(mixed $bands, int $minimum, int $maximum): bool
    {
        if (! is_array($bands) || $bands === [] || $minimum >= $maximum) {
            return false;
        }

        $ranges = collect($bands)
            ->map(function ($band): ?array {
                if (! is_array($band)
                    || ! isset($band['minimum'], $band['maximum'])
                    || ! is_numeric($band['minimum'])
                    || ! is_numeric($band['maximum'])
                    || blank($band['risk_level'] ?? null)) {
                    return null;
                }

                return [(int) $band['minimum'], (int) $band['maximum']];
            });

        if ($ranges->contains(null)) {
            return false;
        }

        $expected = $minimum;
        foreach ($ranges->sortBy(fn (array $range) => $range[0])->values() as [$from, $to]) {
            if ($from !== $expected || $to < $from) {
                return false;
            }
            $expected = $to + 1;
        }

        return $expected === $maximum + 1;
    }

    private function require(array &$errors, bool $condition, string $message): void
    {
        if (! $condition) {
            $errors[] = $message;
        }
    }

    private function isHttpsUrl(string $url): bool
    {
        if (! filter_var($url, FILTER_VALIDATE_URL)) {
            return false;
        }

        return Str::lower((string) parse_url($url, PHP_URL_SCHEME)) === 'https';
    }

    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);
    }
}
