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

declare(strict_types=1);

use Bakkbone\Panel\App;
use Bakkbone\Panel\Core\Config;
use Bakkbone\Panel\Core\SecretBox;

require dirname(__DIR__) . '/vendor/autoload.php';

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

if ($command === 'keys:generate') {
    $dir = $args[0] ?? '/etc/bakkbone';
    foreach (['agent.key', 'data.key'] as $name) {
        $path = "{$dir}/{$name}";
        if (file_exists($path)) {
            fwrite(STDERR, "{$path} exists, leaving it alone\n");
            continue;
        }
        file_put_contents($path, SecretBox::generateKey() . "\n");
        chmod($path, 0640);
        echo "wrote {$path}\n";
    }
    exit(0);
}

$app = new App(Config::load(getenv('BAKKBONE_CONFIG') ?: '/etc/bakkbone/panel.php'));

switch ($command) {
    case 'admin:create':
        $username = $args[0] ?? 'root';
        if ($app->db()->value('SELECT 1 FROM admin_users WHERE username = ?', [$username]) !== null) {
            fwrite(STDERR, "admin {$username} already exists\n");
            exit(1);
        }
        // Piped in when unattended, so the terminal is only touched when there is one
        $tty = stream_isatty(STDIN);
        if ($tty) {
            fwrite(STDOUT, "Password for {$username}: ");
            system('stty -echo');
        }
        $password = trim((string) fgets(STDIN));
        if ($tty) {
            system('stty echo');
            echo "\n";
        }
        $app->passwordPolicy()->assert($password, $username);
        $app->db()->insert('admin_users', ['username' => $username, 'password_hash' => $app->hasher()->hash($password)]);
        echo "created admin {$username}\n";
        break;

    case 'token:create':
        $username = $args[0] ?? 'root';
        $name = $args[1] ?? 'billing';
        $id = $app->db()->value('SELECT id FROM admin_users WHERE username = ?', [$username]);
        if ($id === null) {
            fwrite(STDERR, "no admin {$username}\n");
            exit(1);
        }
        echo $app->tokens()->create('admin', (int) $id, $name), "\n";
        break;

    case 'jobs:replay':
        foreach ($app->jobs()->pending() as $job) {
            try {
                $app->jobs()->execute((int) $job['id']);
                echo "job {$job['id']} done\n";
            } catch (\Throwable $e) {
                echo "job {$job['id']} failed: {$e->getMessage()}\n";
            }
        }
        break;

    case 'autossl:run':
        foreach ($app->autoSsl()->run($args[0] ?? null) as $user => $sites) {
            foreach ($sites as $site => $result) {
                $detail = $result['reason'] ?? ($result['not_after'] ?? '');
                echo str_pad($user, 17), str_pad($site, 40), str_pad($result['status'], 9), $detail, "\n";
            }
        }
        break;

    case 'mail:configure':
        $app->jobs()->run('mail.configure', ['hostname' => $app->config()->require('hostname')]);
        $app->relays()->sync();
        echo "Exim and Dovecot configured\n";
        break;

    case 'monitor:check':
        $result = $app->monitor()->check();
        foreach ($result['changes'] as $c) {
            echo "{$c['status']}: {$c['message']}\n";
        }
        foreach ($result['alerted'] ?? [] as $channel => $outcome) {
            echo "alert {$channel}: {$outcome}\n";
        }
        break;

    case 'mail:sni':
        $result = $app->jobs()->run('mail.sni.rebuild', ['reload' => true]);
        echo "Mail SNI rebuilt for {$result['names']} names\n";
        break;

    case 'webmail:configure':
        $app->configureWebmail();
        echo "Roundcube configured\n";
        break;

    case 'ip:apply':
        $r = $app->ips()->apply();
        echo count($r['added'] ?? []), " addresses put back on the interface\n";
        break;

    case 'jail:setup':
        $app->jobs()->run('jail.setup', []);
        echo "Jailed shell set up: sshd chroots the bakkbone-jail group into /home/virtfs\n";
        break;

    case 'jail:mount':
        $r = $app->jobs()->run('jail.mount_all', []);
        echo count($r['mounted'] ?? []), " jails mounted\n";
        foreach ($r['failed'] ?? [] as $user => $error) {
            echo "Failed for {$user}: {$error}\n";
        }
        exit(($r['failed'] ?? []) === [] ? 0 : 1);

    case 'sqladmin:configure':
        $r = $app->jobs()->run('sqladmin.configure', []);
        foreach ($r['warnings'] ?? [] as $w) {
            echo "Warning: {$w}\n";
        }
        echo "phpMyAdmin configured\n";
        break;

    case 'ftp:configure':
        $app->ftp()->configure($app->config());
        echo "ProFTPD configured\n";
        break;

    case 'backup:tick':
        $result = $app->backupRunner()->tick();
        foreach ($result['tasks'] ?? [] as $t) {
            echo "task {$t['id']} {$t['kind']}: {$t['status']}\n", implode("\n", array_map(fn ($l) => "  {$l}", $t['log'])), "\n";
        }
        if (isset($result['skipped'])) {
            echo $result['skipped'], "\n";
        }
        break;

    case 'backup:now':
        $id = $app->backups()->queue($args[0] ?? null ? 'account' : 'scheduled', $args[0] ?? null, ['date' => date('Y-m-d')], 'cli');
        echo "queued backup task {$id}; it starts within five minutes, or run backup:tick now\n";
        break;

    case 'disk:refresh':
        $r = $app->disk()->refreshAll();
        echo "updated disk usage for {$r['updated']} account(s)\n";
        foreach ($r['failed'] as $user => $error) {
            echo "  {$user}: {$error}\n";
        }
        break;

    case 'sessions:purge':
        echo $app->sessions()->purgeExpired(), " expired sessions removed\n";
        echo $app->webmail()->purgeExpired(), " expired webmail sign-ins removed\n";
        echo $app->twoFactor()->purge(), " expired two-factor sign-ins removed\n";
        echo $app->loginGuard()->purge(), " day-old failed logins removed\n";
        echo $app->sqlAdmin()->purge(), " expired phpMyAdmin sessions closed\n";
        break;

    case 'webapps:health':
        foreach ($app->webApps()->healthTick() as $line) {
            echo $line, "\n";
        }
        break;

    case 'security:scan':
        $s = $app->firewall()->run();
        echo "{$s['counted']} failed mail, FTP and SSH logins counted\n";
        echo $s['ok'] ? "Firewall: {$s['blocked']} blocked, {$s['denied']} on the block list, {$s['allowed']} on the allow list\n" : "Firewall not updated: {$s['error']}\n";
        if ($s['ok'] && $s['error'] !== null) {
            echo "Log scan failed: {$s['error']}\n";
        }
        break;

    case 'security:unblock':
        $target = $args[0] ?? '';
        $n = $app->loginGuard()->unblock(filter_var($target, FILTER_VALIDATE_IP) ? 'ip' : 'user', $target);
        $app->firewall()->push();
        echo $n > 0 ? "Unblocked {$target}\n" : "{$target} wasn't blocked; its failed logins were cleared anyway\n";
        break;

    case 'security:remove-2fa':
        $row = $app->db()->one('SELECT id FROM admin_users WHERE username = ?', [$args[0] ?? '']);
        if ($row === null) {
            fwrite(STDERR, "No BHCP admin called " . ($args[0] ?? '') . "\n");
            exit(1);
        }
        $app->twoFactor()->disable('admin', (int) $row['id'], 'cli');
        echo "Two-factor authentication removed for {$args[0]}\n";
        break;

    default:
        echo "Usage: panel <command>\n";
        echo "  keys:generate [dir]           create agent and data keys\n";
        echo "  admin:create [username]       create an admin login\n";
        echo "  token:create <admin> <name>   create a BHCP API token (WHM API 1) for billing systems\n";
        echo "  jobs:replay                   retry jobs queued while the agent was down\n";
        echo "  autossl:run [username]        issue and renew certificates (run daily by a timer)\n";
        echo "  mail:configure                write and validate Exim and Dovecot configuration\n";
        echo "  monitor:check                 check services, disks and the mail queue, restart stopped services and send alerts (run by a timer)\n";
        echo "  mail:sni                      rebuild mail SNI from the installed certificates (runs on every certificate change)\n";
        echo "  webmail:configure             write Roundcube's configuration (after mail:configure)\n";
        echo "  sqladmin:configure            write phpMyAdmin's single sign-on configuration\n";
        echo "  ip:apply                      add the panel's extra IP addresses back to the interface (run at boot by bakkbone-ips.service)\n";
        echo "  jail:setup                    write the jailed shell's sshd rule and login shell\n";
        echo "  jail:mount                    build every jailed account's /home/virtfs jail (run at boot by bakkbone-jail.service)\n";
        echo "  ftp:configure                 write and validate ProFTPD configuration, then rebuild FTP logins\n";
        echo "  backup:tick                   run due and queued backups and restores (run by a timer)\n";
        echo "  backup:now [username]         queue a backup of every account, or one\n";
        echo "  sessions:purge                remove expired sessions, sign-in challenges, old failed logins and phpMyAdmin users (run by a timer)\n";
        echo "  security:scan                 count failed mail, FTP and SSH logins and update the firewall (run every minute by a timer)\n";
        echo "  security:unblock <ip|user>    lift a brute-force block, for when you've locked yourself out\n";
        echo "  security:remove-2fa <admin>   turn off two-factor for a BHCP admin who has lost their phone\n";
        echo "  disk:refresh                  measure every account's home folder for listaccts and quota figures (run by a timer)\n";
        echo "\n" . \Bakkbone\Panel\Core\Trademarks::notice() . "\n";
}
