#!/usr/bin/env php
<?php
/**
 * Central Admin CLI — bin/central
 *
 * Perintah:
 *   php bin/central install              Bootstrap penuh: key, migrate, seed
 *   php bin/central key:generate         Membuat/mengganti APP_KEY pada .env
 *   php bin/central migrate              Menjalankan migrasi Core + semua module
 *   php bin/central migrate:status       Status migrasi
 *   php bin/central migrate:rollback     Rollback batch terakhir
 *   php bin/central seed                 Menjalankan seeder
 *   php bin/central sync                 Sinkronisasi registry App/Module + permission
 *   php bin/central user:create          Membuat user baru (interaktif / via opsi)
 *   php bin/central user:password        Mengganti password user
 *   php bin/central app:credentials      Menampilkan atau merotasi kredensial App
 *   php bin/central app:ping             Menandai App online + mencatat ping
 *   php bin/central maintenance          Prune rate limit/session/event lama
 *   php bin/central routes               Menampilkan daftar route
 *   php bin/central serve                Menjalankan server pengembangan
 */

declare(strict_types=1);

if (PHP_SAPI !== 'cli') {
    http_response_code(403);
    exit("bin/central hanya dapat dijalankan dari command line.\n");
}

define('BASE_PATH', dirname(__DIR__));

require BASE_PATH . '/bootstrap/app.php';

use CentralAdmin\Core\Database\Migrator;
use CentralAdmin\Core\Module\ModuleManager;
use CentralAdmin\Core\Security\Hasher;
use CentralAdmin\Core\Support\Config;
use CentralAdmin\Core\Support\Container;
use CentralAdmin\Core\Support\Env;
use CentralAdmin\Models\App;
use CentralAdmin\Models\AppCredential;
use CentralAdmin\Models\Permission;
use CentralAdmin\Models\User;
use CentralAdmin\Services\AppService;

/** @var \CentralAdmin\Core\Http\Kernel $kernel */
$kernel = \CentralAdmin\Core\Http\Kernel::instance();
$kernel->bootForCli();

/* ------------------------------------------------------------------ helper */

const CLI_GREEN = "\033[32m";
const CLI_YELLOW = "\033[33m";
const CLI_RED = "\033[31m";
const CLI_CYAN = "\033[36m";
const CLI_DIM = "\033[2m";
const CLI_RESET = "\033[0m";

function out(string $message = '', string $color = ''): void
{
    fwrite(STDOUT, ($color !== '' ? $color : '') . $message . CLI_RESET . PHP_EOL);
}

function ok(string $message): void { out('  ✓ ' . $message, CLI_GREEN); }
function warn(string $message): void { out('  ! ' . $message, CLI_YELLOW); }
function bad(string $message): void { out('  ✗ ' . $message, CLI_RED); }
function info(string $message): void { out('  - ' . $message, CLI_DIM); }

function heading(string $title): void
{
    out('');
    out('┌─ ' . $title, CLI_CYAN);
}

function fail(string $message, int $code = 1): never
{
    bad($message);
    exit($code);
}

function ask(string $question, string $default = '', bool $hidden = false): string
{
    $suffix = $default !== '' ? ' [' . $default . ']' : '';
    fwrite(STDOUT, $question . $suffix . ': ');
    if ($hidden) {
        @shell_exec('stty -echo 2>/dev/null');
    }
    $answer = trim((string) fgets(STDIN));
    if ($hidden) {
        @shell_exec('stty echo 2>/dev/null');
        fwrite(STDOUT, PHP_EOL);
    }
    return $answer !== '' ? $answer : $default;
}

/** Path migrasi: Core + seluruh App/Module yang terdaftar. */
function migrationPaths(): array
{
    $paths = [BASE_PATH . '/database/migrations'];
    $manager = app(ModuleManager::class);
    foreach ($manager->registry()->appIds() as $appId) {
        $instance = $manager->registry()->appInstance($appId);
        if ($instance === null) {
            continue;
        }
        foreach ($manager->registry()->modules($appId) as $manifest) {
            $class = $manifest->class;
            if (class_exists($class)) {
                /** @var \CentralAdmin\Core\Module\BaseModule $module */
                $module = new $class();
                if (is_dir($module->migrationsPath())) {
                    $paths[] = $module->migrationsPath();
                }
            }
        }
    }
    return array_values(array_unique($paths));
}

function ensureDatabase(): void
{
    try {
        db()->select('SELECT 1');
    } catch (Throwable $e) {
        fail('Tidak dapat terhubung ke database: ' . $e->getMessage()
            . "\n     Periksa DB_* pada file .env lalu coba lagi.");
    }
}

function envFile(): string
{
    return BASE_PATH . '/.env';
}

function writeEnvValue(string $key, string $value): bool
{
    $file = envFile();
    if (is_file($file)) {
        $content = (string) file_get_contents($file);
    } else {
        $example = BASE_PATH . '/.env.example';
        if (!is_file($example)) {
            return false;
        }
        $content = (string) file_get_contents($example);
    }

    $pattern = '/^' . preg_quote($key, '/') . '=.*$/m';
    $line = $key . '=' . $value;
    if (preg_match($pattern, $content) === 1) {
        $content = (string) preg_replace($pattern, $line, $content, 1);
    } else {
        $content = rtrim($content) . PHP_EOL . PHP_EOL . $line . PHP_EOL;
    }

    if (@file_put_contents($file, $content) === false) {
        return false;
    }
    @chmod($file, 0640);
    Env::set($key, $value);
    return true;
}

/** Key-value dari opsi CLI: --name="Super Admin" --role=admin */
function options(array $argv): array
{
    $options = [];
    foreach (array_slice($argv, 2) as $argument) {
        if (preg_match('/^--([a-z0-9\-_]+)(?:=(.*))?$/i', $argument, $matches) === 1) {
            $options[$matches[1]] = $matches[2] ?? '1';
        }
    }
    return $options;
}

/* --------------------------------------------------------------- perintah */

$command = $argv[1] ?? 'help';
$opts = options($argv);

try {
    switch ($command) {
        /* ------------------------------------------------------------ install */
        case 'install':
            heading('Instalasi Central Admin');

            if (env('APP_KEY', '') === '') {
                $key = 'base64:' . base64_encode(random_bytes(32));
                writeEnvValue('APP_KEY', $key)
                    ? ok('APP_KEY baru dibuat dan disimpan pada .env')
                    : warn('Gagal menulis .env — set APP_KEY manual.');
            } else {
                info('APP_KEY sudah ada, dilewati.');
            }

            ensureDatabase();
            ok('Koneksi database berhasil (' . db()->driver() . ')');

            $result = app(Migrator::class)->migrate(migrationPaths());
            foreach ($result['ran'] as $migration) {
                ok('migrate: ' . $migration);
            }
            if ($result['ran'] === []) {
                info('Tidak ada migrasi baru (' . count($result['skipped']) . ' migrasi sudah dijalankan sebelumnya).');
            } else {
                info('Batch ' . $result['batch'] . ' · ' . count($result['ran']) . ' migrasi dijalankan.');
            }

            out('');
            out('  Menjalankan seeder…');
            foreach (glob(BASE_PATH . '/database/seeders/*.php') ?: [] as $file) {
                /** @var callable $seeder */
                $seeder = require $file;
                $messages = (array) $seeder(Container::getInstance());
                out('  ' . basename($file), CLI_DIM);
                foreach ($messages as $message) {
                    ok((string) $message);
                }
            }

            $manager = app(ModuleManager::class);
            $sync = $manager->syncDatabase(true);
            $registry = $manager->registry();
            ok('Registry disinkronkan: ' . count($registry->apps()) . " App, {$sync['modules']} module, {$sync['attached']} pemasangan.");

            out('');
            out('  Instalasi selesai.', CLI_GREEN);
            out('  Langkah berikutnya:');
            info('1. Pastikan APP_URL, APP_ENV, dan SESSION_SECURE pada .env sesuai server.');
            info('2. Jalankan: php -S 0.0.0.0:8000 -t public public/router.php (atau arahkan document root ke public/).');
            info('3. Login memakai akun admin hasil seeding (default: admin@central-admin.test).');
            break;

        /* ------------------------------------------------------ key:generate */
        case 'key:generate':
            $key = 'base64:' . base64_encode(random_bytes(32));
            if (writeEnvValue('APP_KEY', $key)) {
                ok('APP_KEY baru disimpan pada .env');
                warn('Rotasi APP_KEY membuat webhook secret lama tidak dapat didekripsi. Rotasi kredensial App setelahnya.');
            } else {
                fail('Gagal menulis .env. Tambahkan manual: APP_KEY=' . $key);
            }
            break;

        /* ------------------------------------------------------------ migrate */
        case 'migrate':
            ensureDatabase();
            $paths = migrationPaths();
            $migrator = app(Migrator::class);
            try {
                $migrator->ensureRepository();
            } catch (Throwable $e) {
                fail('Gagal menyiapkan tabel migrations: ' . $e->getMessage());
            }

            $result = app(Migrator::class)->migrate($paths);
            if ($result['ran'] === []) {
                info('Tidak ada migrasi yang perlu dijalankan. Semua sudah terbaru.');
            }
            foreach ($result['ran'] as $migration) {
                ok('migrate: ' . $migration);
            }
            if ($result['ran'] !== []) {
                info('Batch ' . $result['batch'] . ' · ' . count($result['skipped']) . ' migrasi sebelumnya dilewati.');
            }
            info('Sumber: ' . implode(', ', array_map(static fn (string $p): string => str_replace(BASE_PATH . '/', '', $p), $paths)));
            break;

        /* ----------------------------------------------------- migrate:status */
        case 'migrate:status':
            ensureDatabase();
            $rows = app(Migrator::class)->status(migrationPaths());
            if ($rows === []) {
                warn('Belum ada file migrasi ditemukan.');
                break;
            }
            out(sprintf('  %-58s %-8s %s', 'MIGRATION', 'BATCH', 'STATUS'));
            foreach ($rows as $row) {
                out(sprintf(
                    '  %-58s %-8s %s',
                    (string) $row['migration'],
                    isset($row['batch']) ? (string) $row['batch'] : '-',
                    !empty($row['executed']) ? 'applied' : 'pending',
                    !empty($row['executed']) ? CLI_DIM : CLI_YELLOW
                ));
            }
            break;

        /* --------------------------------------------------- migrate:rollback */
        case 'migrate:rollback':
            ensureDatabase();
            $result = app(Migrator::class)->rollback(migrationPaths());
            if ($result === []) {
                info('Tidak ada migrasi yang di-rollback.');
            }
            foreach ($result as $migration) {
                warn('rollback: ' . $migration);
            }
            break;

        /* --------------------------------------------------------------- seed */
        case 'seed':
            ensureDatabase();
            foreach (glob(BASE_PATH . '/database/seeders/*.php') ?: [] as $file) {
                /** @var callable $seeder */
                $seeder = require $file;
                $messages = (array) $seeder(Container::getInstance());
                out('  ' . basename($file), CLI_DIM);
                foreach ($messages as $message) {
                    ok((string) $message);
                }
            }
            out('');
            info('Seeder bersifat idempotent — aman dijalankan berulang.');
            break;

        /* --------------------------------------------------------------- sync */
        case 'sync':
            ensureDatabase();
            $manager = app(ModuleManager::class);
            $result = $manager->syncDatabase(true);
            $installations = (int) db()->scalar('SELECT COUNT(*) FROM app_modules');
            $permissions = (int) db()->scalar('SELECT COUNT(*) FROM permissions');
            ok('Manifest App    : ' . count($manager->registry()->apps()));
            ok("Module terdaftar: {$result['modules']}");
            ok("Pemasangan baru : {$result['attached']} (total {$installations})");
            ok("Permission      : {$permissions}");
            break;

        /* -------------------------------------------------------- user:create */
        case 'user:create':
            ensureDatabase();
            $name = (string) ($opts['name'] ?? ask('Nama user', 'Super Admin'));
            $email = (string) ($opts['email'] ?? ask('Email'));
            if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
                fail('Email tidak valid.');
            }
            if (User::findByEmail($email) !== null) {
                fail('Email sudah terdaftar.');
            }
            $role = (string) ($opts['role'] ?? 'super-admin');
            if (!array_key_exists($role, User::roles())) {
                fail('Role tidak dikenal. Pilihan: ' . implode(', ', array_keys(User::roles())));
            }
            $password = (string) ($opts['password'] ?? ask('Password (min ' . config('security.password_min_length', 10) . ' karakter)', '', true));
            if (strlen($password) < (int) config('security.password_min_length', 10)) {
                fail('Password terlalu pendek.');
            }

            $user = User::create([
                'uuid'       => uuid(),
                'name'       => $name,
                'email'      => $email,
                'password'   => Hasher::make($password),
                'role'       => $role,
                'status'     => User::STATUS_ACTIVE,
                'created_at' => now_utc(),
                'updated_at' => now_utc(),
            ]);
            ok("User '{$user->email}' dibuat dengan role {$role}.");
            break;

        /* ------------------------------------------------------ user:password */
        case 'user:password':
            ensureDatabase();
            $email = (string) ($opts['email'] ?? ask('Email user'));
            $user = User::findByEmail($email);
            if ($user === null) {
                fail('User tidak ditemukan.');
            }
            $password = (string) ($opts['password'] ?? ask('Password baru', '', true));
            if (strlen($password) < (int) config('security.password_min_length', 10)) {
                fail('Password terlalu pendek.');
            }
            $user->update([
                'password'              => Hasher::make($password),
                'remember_token'        => null,
                'failed_login_count'    => 0,
                'locked_until'          => null,
                'updated_at'            => now_utc(),
            ]);
            ok('Password diperbarui. Semua remember-token dicabut.');
            break;

        /* --------------------------------------------------- app:credentials */
        case 'app:credentials':
            ensureDatabase();
            $slug = (string) ($opts['slug'] ?? ask('Slug atau App ID'));
            $app = App::findBySlug($slug) ?? App::findByAppId($slug);
            if ($app === null) {
                fail('App tidak ditemukan.');
            }
            if (isset($opts['rotate'])) {
                $credentials = app(AppService::class)->rotateCredentials($app, null);
                warn('Kredensial lama dicabut. Simpan nilai berikut sekarang:');
            } else {
                $credentials = AppCredential::generateFor($app, null);
            }
            out('');
            out('  App        : ' . $app->name . ' (' . $app->app_id . ')', CLI_CYAN);
            out('  API Key    : ' . $credentials['api_key'], CLI_GREEN);
            out('  Webhook    : ' . $credentials['webhook_secret'], CLI_GREEN);
            out('  Dibuat     : ' . $credentials['generated_at'], CLI_DIM);
            out('');
            warn('Nilai di atas tidak dapat ditampilkan lagi. Simpan pada konfigurasi website.');
            break;

        /* ---------------------------------------------------------- app:ping */
        case 'app:ping':
            ensureDatabase();
            $slug = (string) ($opts['slug'] ?? ask('Slug atau App ID'));
            $app = App::findBySlug($slug) ?? App::findByAppId($slug);
            if ($app === null) {
                fail('App tidak ditemukan.');
            }
            $result = app(AppService::class)->ping($app);
            ok('Ping dicatat: ' . json_encode($result, JSON_UNESCAPED_SLASHES));
            break;

        /* ------------------------------------------------------ maintenance */
        case 'maintenance':
            ensureDatabase();
            $rateLimits = db()->execute('DELETE FROM rate_limits WHERE expires_at < ?', [now_utc()]);
            ok("Rate limit kedaluwarsa dihapus: {$rateLimits}");
            $tokens = db()->execute('DELETE FROM api_tokens WHERE expires_at < ? AND (revoked_at IS NOT NULL OR expires_at < ?)', [now_utc(), gmdate('Y-m-d H:i:s', time() - 86400 * 30)]);
            ok("Access token kedaluwarsa dibersihkan: {$tokens}");
            $logs = db()->execute('DELETE FROM api_request_logs WHERE created_at < ?', [gmdate('Y-m-d H:i:s', time() - 86400 * 30)]);
            ok("API request log > 30 hari dihapus: {$logs}");
            $deliveries = db()->execute('DELETE FROM webhook_deliveries WHERE created_at < ?', [gmdate('Y-m-d H:i:s', time() - 86400 * 30)]);
            ok("Riwayat webhook > 30 hari dihapus: {$deliveries}");

            $sessionPath = storage_path('framework/sessions');
            $removed = 0;
            $maxAge = max(1, (int) config('session.lifetime', 120)) * 60 * 2;
            foreach (glob($sessionPath . '/sess_*') ?: [] as $file) {
                if (filemtime($file) !== false && (time() - (int) filemtime($file)) > $maxAge) {
                    @unlink($file);
                    $removed++;
                }
            }
            ok("File session lama dihapus: {$removed}");
            break;

        /* ------------------------------------------------------------ routes */
        case 'routes':
            $rows = router()->routeTable();
            out(sprintf('  %-10s %-42s %s', 'METHOD', 'URI', 'NAME'));
            foreach ($rows as $row) {
                out(sprintf('  %-10s %-42s %s', (string) $row['method'], (string) $row['uri'], (string) ($row['name'] ?? ''), CLI_DIM));
            }
            out('');
            info(count($rows) . ' route terdaftar.');
            break;

        /* ------------------------------------------------------------- serve */
        case 'serve':
            $host = (string) ($opts['host'] ?? '0.0.0.0');
            $port = (string) ($opts['port'] ?? '8000');
            out("Menjalankan server pengembangan pada http://{$host}:{$port}", CLI_GREEN);
            info('Tekan Ctrl+C untuk berhenti. Gunakan ini hanya untuk development.');
            passthru(sprintf('%s -S %s:%s -t %s %s', PHP_BINARY, $host, $port, escapeshellarg(BASE_PATH . '/public'), escapeshellarg(BASE_PATH . '/public/router.php')));
            break;

        /* -------------------------------------------------------------- help */
        case 'help':
        case '--help':
        case '-h':
        default:
            out('');
            out('  CENTRAL ADMIN CLI', CLI_CYAN);
            out('  ────────────────────────────────────────────────────────────');
            out('  php bin/central install                 Instalasi lengkap (key, migrate, seed)');
            out('  php bin/central key:generate            Buat/ganti APP_KEY');
            out('  php bin/central migrate                 Jalankan migrasi Core + module');
            out('  php bin/central migrate:status          Status migrasi');
            out('  php bin/central migrate:rollback        Rollback batch terakhir');
            out('  php bin/central seed                    Jalankan seeder');
            out('  php bin/central sync                    Sinkronkan registry & permission');
            out('  php bin/central user:create             Buat user baru');
            out('  php bin/central user:password           Ganti password user');
            out('  php bin/central app:credentials         Tampilkan kredensial App baru');
            out('  php bin/central app:credentials --rotate  Rotasi kredensial App');
            out('  php bin/central app:ping                Catat ping App');
            out('  php bin/central maintenance             Bersihkan data kedaluwarsa');
            out('  php bin/central routes                  Daftar route');
            out('  php bin/central serve [--port=8000]     Server pengembangan');
            out('');
            out('  Opsi: --name= --email= --password= --role= --slug= --host= --port=', CLI_DIM);
            out('');
            break;
    }
} catch (Throwable $e) {
    fail($e->getMessage() . "\n     " . $e->getFile() . ':' . $e->getLine());
}

exit(0);
