#!/usr/bin/env php
<?php

declare(strict_types=1);

use App\Config\Config;
use App\Core\Application;
use App\Core\Database;
use App\Security\AppKey;
use App\Support\EnvFile;

if (PHP_SAPI !== 'cli') {
    fwrite(STDERR, "This command is available only from PHP CLI.\n");
    exit(1);
}

/** @var Application $application */
$application = require dirname(__DIR__) . '/bootstrap/app.php';
$container = $application->container();
/** @var Config $config */
$config = $container->get(Config::class);

$arguments = $_SERVER['argv'] ?? [];
$command = $arguments[1] ?? 'help';
$options = array_slice($arguments, 2);

$useColour = function_exists('stream_isatty') && stream_isatty(STDOUT);
$colour = static function (string $text, string $code) use ($useColour): string {
    return $useColour ? "\033[{$code}m{$text}\033[0m" : $text;
};

$line = static function (string $text = ''): void {
    fwrite(STDOUT, $text . PHP_EOL);
};

$statusLine = static function (string $status, string $message) use ($line, $colour): void {
    $code = match ($status) {
        'PASS' => '32;1',
        'WARN' => '33;1',
        'FAIL' => '31;1',
        default => '36;1',
    };
    $line(sprintf('%s  %s', str_pad($colour($status, $code), 10), $message));
};

$normaliseIniBool = static function (string|false $value): bool {
    if ($value === false) {
        return false;
    }
    return in_array(strtolower(trim($value)), ['1', 'on', 'yes', 'true'], true);
};

$bytes = static function (string $value): int {
    $value = trim($value);
    if ($value === '' || $value === '-1') {
        return PHP_INT_MAX;
    }
    $unit = strtolower(substr($value, -1));
    $number = (float) $value;
    return match ($unit) {
        'g' => (int) ($number * 1024 * 1024 * 1024),
        'm' => (int) ($number * 1024 * 1024),
        'k' => (int) ($number * 1024),
        default => (int) $number,
    };
};

$runAppCheck = static function () use (
    $config,
    $statusLine,
    $line,
    $normaliseIniBool,
    $bytes
): int {
    $failures = 0;
    $warnings = 0;
    $check = static function (bool $passed, string $success, string $failure, bool $warningOnly = false) use (
        &$failures,
        &$warnings,
        $statusLine
    ): void {
        if ($passed) {
            $statusLine('PASS', $success);
            return;
        }

        if ($warningOnly) {
            $warnings++;
            $statusLine('WARN', $failure);
        } else {
            $failures++;
            $statusLine('FAIL', $failure);
        }
    };

    $line('ExamCert application and security check');
    $line(str_repeat('=', 44));

    $minimum = $config->string('app.minimum_php_version', '8.5.0');
    $check(
        version_compare(PHP_VERSION, $minimum, '>='),
        sprintf('PHP %s satisfies the required minimum %s.', PHP_VERSION, $minimum),
        sprintf('PHP %s is below the required minimum %s.', PHP_VERSION, $minimum)
    );

    foreach (['pdo', 'pdo_mysql', 'mbstring', 'intl', 'openssl', 'json', 'fileinfo', 'sodium'] as $extension) {
        $check(
            extension_loaded($extension),
            sprintf('Required extension ext-%s is loaded.', $extension),
            sprintf('Required extension ext-%s is missing.', $extension)
        );
    }

    $envFile = EXAMCERT_ROOT . '/.env';
    $check(is_file($envFile), '.env exists outside the public document root.', '.env is missing. Copy .env.example to .env.');

    if (is_file($envFile)) {
        $mode = fileperms($envFile);
        $mode = $mode === false ? 0 : ($mode & 0777);
        $check(
            ($mode & 0077) === 0,
            sprintf('.env permissions are private (%04o).', $mode),
            sprintf('.env permissions are too broad (%04o); set chmod 600.', $mode)
        );
    }

    $check(
        AppKey::isValid($config->string('app.key')),
        'APP_KEY contains at least 256 bits of key material.',
        'APP_KEY is missing or invalid. Run: php bin/console app:key --write'
    );

    $environment = $config->string('app.environment', 'production');
    $debug = $config->bool('app.debug');
    $productionLike = in_array($environment, ['production', 'staging'], true);

    $check(
        !$productionLike || !$debug,
        sprintf('APP_DEBUG is disabled in %s.', $environment),
        sprintf('APP_DEBUG must be false in %s.', $environment)
    );

    $url = $config->string('app.url');
    $check(
        !$productionLike || str_starts_with(strtolower($url), 'https://'),
        'APP_URL uses HTTPS.',
        'APP_URL must use HTTPS in staging/production.'
    );

    $check(
        $config->array('app.allowed_hosts') !== [],
        'At least one trusted application host is configured.',
        'APP_ALLOWED_HOSTS is empty and could not be derived from APP_URL.'
    );

    $check(
        !$productionLike || $config->bool('app.force_https'),
        'HTTPS enforcement is enabled.',
        'APP_FORCE_HTTPS must be true in staging/production.'
    );

    $check(
        !$productionLike || $config->bool('security.session.secure_cookie'),
        'Secure-only session cookies are enabled.',
        'SESSION_SECURE_COOKIE must be true in staging/production.'
    );

    $sameSite = ucfirst(strtolower($config->string('security.session.same_site', 'Lax')));
    $check(
        in_array($sameSite, ['Lax', 'Strict', 'None'], true),
        sprintf('Session SameSite policy is valid (%s).', $sameSite),
        'SESSION_SAME_SITE must be Lax, Strict, or None.'
    );

    foreach (['logs', 'cache', 'sessions', 'temporary', 'private_uploads'] as $pathKey) {
        $path = $config->string('paths.' . $pathKey);
        $check(
            $path !== '' && is_dir($path) && is_writable($path),
            sprintf('Private path %s is writable.', $pathKey),
            sprintf('Private path %s is missing or not writable.', $pathKey)
        );
    }

    $check(
        !$normaliseIniBool(ini_get('allow_url_include')),
        'allow_url_include is disabled.',
        'allow_url_include must be disabled.'
    );

    $check(
        !$normaliseIniBool(ini_get('short_open_tag')),
        'short_open_tag is disabled.',
        'short_open_tag should be disabled.',
        true
    );

    $check(
        !$productionLike || !$normaliseIniBool(ini_get('display_errors')),
        'display_errors is disabled for the active environment.',
        'display_errors must be disabled in staging/production.'
    );

    $check(
        !$normaliseIniBool(ini_get('expose_php')),
        'expose_php is disabled.',
        'expose_php should be disabled in the active PHP handler.',
        true
    );

    $check(
        (int) ini_get('max_input_vars') >= 5000,
        sprintf('max_input_vars is %d.', (int) ini_get('max_input_vars')),
        sprintf('max_input_vars is %d; Phase 01 target is at least 5000.', (int) ini_get('max_input_vars')),
        true
    );

    $check(
        $bytes((string) ini_get('memory_limit')) >= 256 * 1024 * 1024,
        sprintf('memory_limit is %s.', (string) ini_get('memory_limit')),
        sprintf('memory_limit is %s; target is at least 256M.', (string) ini_get('memory_limit')),
        true
    );

    $databaseConfigured = trim($config->string('database.database')) !== ''
        && trim($config->string('database.username')) !== '';
    $check(
        $databaseConfigured,
        'Database name and username are configured.',
        'Database credentials are not complete; readiness will remain HTTP 503.',
        true
    );

    $line();
    $line(sprintf('Result: %d failure(s), %d warning(s).', $failures, $warnings));
    return $failures === 0 ? 0 : 1;
};

switch ($command) {
    case 'help':
    case '--help':
    case '-h':
        $line('ExamCert command console — Release ' . $config->string('app.version'));
        $line();
        $line('Available commands:');
        $line('  app:check              Validate PHP, environment, paths and security settings');
        $line('  app:key                Print a newly generated APP_KEY');
        $line('  app:key --write        Write a new key to .env when APP_KEY is empty');
        $line('  app:key --write --force  Replace an existing APP_KEY');
        $line('  db:check               Test the configured MySQL connection');
        $line('  route:list             Show registered HTTP routes');
        $line('  system:info            Show non-sensitive runtime information');
        exit(0);

    case 'app:check':
    case 'security:check':
        exit($runAppCheck());

    case 'app:key':
        $key = AppKey::generate();
        $write = in_array('--write', $options, true);
        $force = in_array('--force', $options, true);

        if (!$write) {
            $line($key);
            exit(0);
        }

        $envFile = EXAMCERT_ROOT . '/.env';
        if (!is_file($envFile)) {
            $statusLine('FAIL', '.env does not exist. Copy .env.example to .env first.');
            exit(1);
        }

        $current = $config->string('app.key');
        if ($current !== '' && !$force) {
            $statusLine('FAIL', 'APP_KEY already exists. Use --force only when intentionally rotating it.');
            exit(1);
        }

        EnvFile::set($envFile, 'APP_KEY', $key);
        $statusLine('PASS', 'A new APP_KEY was written to .env with private permissions.');
        exit(0);

    case 'db:check':
        /** @var Database $database */
        $database = $container->get(Database::class);

        if (!$database->isConfigured()) {
            $statusLine('FAIL', 'Database credentials are incomplete in .env.');
            exit(1);
        }

        try {
            if (!$database->ping()) {
                $statusLine('FAIL', 'MySQL did not respond to the health query.');
                exit(1);
            }

            $statusLine('PASS', 'MySQL connection succeeded.');
            $line('Server version: ' . $database->serverVersion());
            $line('Database: ' . $config->string('database.database'));
            exit(0);
        } catch (Throwable $exception) {
            $statusLine('FAIL', 'MySQL connection failed. Review logs/php-error.log and logs/app-*.log.');
            exit(1);
        }

    case 'route:list':
        $routes = $application->router()->list();
        $line(str_pad('METHOD', 18) . str_pad('PATH', 34) . 'NAME');
        $line(str_repeat('-', 72));
        foreach ($routes as $route) {
            $line(
                str_pad(implode('|', $route['methods']), 18)
                . str_pad($route['path'], 34)
                . ($route['name'] ?? '-')
            );
        }
        exit(0);

    case 'system:info':
        $line('ExamCert release: ' . $config->string('app.version'));
        $line('Environment: ' . $config->string('app.environment'));
        $line('PHP: ' . PHP_VERSION . ' (' . PHP_SAPI . ')');
        $line('OS: ' . PHP_OS_FAMILY);
        $line('Timezone: ' . date_default_timezone_get());
        $line('Application root: ' . EXAMCERT_ROOT);
        $line('APP_URL: ' . $config->string('app.url'));
        $line('Allowed hosts: ' . implode(', ', $config->array('app.allowed_hosts')));
        exit(0);

    default:
        $statusLine('FAIL', sprintf('Unknown command "%s".', $command));
        $line('Run: php bin/console help');
        exit(1);
}
