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

declare(strict_types=1);

use Wccc\Application;
use Wccc\Config;
use Wccc\Database;

$root = dirname(__DIR__);
$autoload = $root . '/vendor/autoload.php';
if (!is_file($autoload)) {
    fwrite(STDERR, "Composer dependencies are missing. Run composer install first.\n");
    exit(1);
}
require $autoload;

$command = $argv[1] ?? 'help';

try {
    switch ($command) {
        case 'help':
        case '--help':
        case '-h':
            showHelp();
            exit(0);

        case 'migrate':
            migrate($root);
            exit(0);

        case 'user:create':
            createUser($root, $argv[2] ?? '');
            exit(0);

        case 'user:password':
            changePassword($root, $argv[2] ?? '');
            exit(0);

        case 'scheduler:run':
            printJson(app($root)->services->schedulerService->run());
            exit(0);

        case 'scheduler:full':
            printJson(app($root)->services->schedulerService->runFull());
            exit(0);

        case 'sync':
            $application = app($root);
            if (isset($argv[2]) && filter_var($argv[2], FILTER_VALIDATE_INT) !== false) {
                printJson($application->services->syncService->syncSource((int) $argv[2]));
            } else {
                printJson($application->services->syncService->syncAll());
            }
            exit(0);

        case 'conflicts:scan':
            $days = isset($argv[2]) && filter_var($argv[2], FILTER_VALIDATE_INT) !== false
                ? (int) $argv[2]
                : null;
            printJson(app($root)->services->conflictService->scan($days));
            exit(0);

        case 'notifications:dispatch':
            $limit = isset($argv[2]) && filter_var($argv[2], FILTER_VALIDATE_INT) !== false
                ? max(1, min(500, (int) $argv[2]))
                : 100;
            printJson(app($root)->services->notificationService->dispatch($limit));
            exit(0);

        case 'settings:show':
            $application = app($root);
            $values = [];
            foreach ($application->settings->defaults() as $key => $default) {
                $values[$key] = $application->settings->value($key);
            }
            printJson($values);
            exit(0);

        case 'sources:list':
            printTable(app($root)->services->calendarSources->all(), [
                'id', 'name', 'category', 'enabled', 'last_success_at', 'consecutive_failures',
            ]);
            exit(0);

        case 'mirrors:list':
            printTable(app($root)->services->mirrorTargets->all(), [
                'id', 'name', 'target_calendar_id', 'enabled', 'last_polled_at',
            ]);
            exit(0);

        case 'status':
            status($root);
            exit(0);

        default:
            fwrite(STDERR, "Unknown command: {$command}\n\n");
            showHelp();
            exit(2);
    }
} catch (Throwable $exception) {
    fwrite(STDERR, 'ERROR: ' . $exception->getMessage() . "\n");
    if (getenv('APP_DEBUG') === 'true') {
        fwrite(STDERR, (string) $exception . "\n");
    }
    exit(1);
}

function app(string $root): Application
{
    return Application::boot($root, false);
}

function migrate(string $root): void
{
    $config = new Config($root);
    $db = new Database($config);
    $schemaPath = $root . '/database/schema.sql';
    $schema = file_get_contents($schemaPath);
    if ($schema === false) {
        throw new RuntimeException("Unable to read {$schemaPath}.");
    }

    $statements = preg_split('/;\s*(?:\R|$)/', $schema);
    if (!is_array($statements)) {
        throw new RuntimeException('Unable to parse the database schema.');
    }

    $executed = 0;
    foreach ($statements as $statement) {
        $statement = trim($statement);
        if ($statement === '') {
            continue;
        }
        $db->pdo()->exec($statement);
        $executed++;
    }

    fwrite(STDOUT, "Database schema is ready. Executed {$executed} idempotent statements.\n");
}

function createUser(string $root, string $email): void
{
    $email = normalizeEmail($email);
    $application = app($root);
    $existing = $application->db->one('SELECT id FROM users WHERE email = :email', ['email' => $email]);
    if ($existing !== null) {
        throw new RuntimeException('That user already exists. Use user:password to change the password.');
    }

    $password = promptNewPassword();
    $hash = password_hash($password, PASSWORD_DEFAULT);
    if (!is_string($hash)) {
        throw new RuntimeException('Unable to hash the password.');
    }

    $application->db->execute(
        'INSERT INTO users (email, password_hash, created_at, updated_at)
         VALUES (:email, :password_hash, UTC_TIMESTAMP(), UTC_TIMESTAMP())',
        ['email' => $email, 'password_hash' => $hash]
    );
    fwrite(STDOUT, "Created administrative user {$email}.\n");
}

function changePassword(string $root, string $email): void
{
    $email = normalizeEmail($email);
    $application = app($root);
    $existing = $application->db->one('SELECT id FROM users WHERE email = :email', ['email' => $email]);
    if ($existing === null) {
        throw new RuntimeException('That user does not exist. Use user:create first.');
    }

    $password = promptNewPassword();
    $hash = password_hash($password, PASSWORD_DEFAULT);
    if (!is_string($hash)) {
        throw new RuntimeException('Unable to hash the password.');
    }

    $application->db->execute(
        'UPDATE users SET password_hash = :password_hash, updated_at = UTC_TIMESTAMP() WHERE email = :email',
        ['email' => $email, 'password_hash' => $hash]
    );
    fwrite(STDOUT, "Updated the password for {$email}.\n");
}

function normalizeEmail(string $email): string
{
    $email = mb_strtolower(trim($email));
    if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
        throw new RuntimeException('Provide a valid email address after the command.');
    }
    return $email;
}

function promptNewPassword(): string
{
    $password = promptHidden('New password: ');
    $confirm = promptHidden('Confirm password: ');
    if ($password !== $confirm) {
        throw new RuntimeException('The passwords did not match.');
    }
    if (strlen($password) < 14) {
        throw new RuntimeException('Use a password at least 14 characters long.');
    }
    return $password;
}

function promptHidden(string $prompt): string
{
    fwrite(STDOUT, $prompt);
    $sttyAvailable = PHP_OS_FAMILY !== 'Windows' && trim((string) shell_exec('command -v stty 2>/dev/null')) !== '';
    if ($sttyAvailable) {
        shell_exec('stty -echo');
    }
    try {
        $value = fgets(STDIN);
    } finally {
        if ($sttyAvailable) {
            shell_exec('stty echo');
        }
        fwrite(STDOUT, "\n");
    }
    if ($value === false) {
        throw new RuntimeException('Unable to read the password.');
    }
    return rtrim($value, "\r\n");
}

function status(string $root): void
{
    $application = app($root);
    $tables = ['calendar_sources', 'events', 'conflicts', 'mirror_targets', 'notification_queue'];
    $counts = [];
    foreach ($tables as $table) {
        $row = $application->db->one("SELECT COUNT(*) AS count FROM {$table}");
        $counts[$table] = (int) ($row['count'] ?? 0);
    }
    $counts['version'] = $application->config->appVersion();
    $counts['timezone'] = $application->config->timezone();
    $counts['simulation_mode'] = (bool) $application->settings->value('simulation_mode');
    $counts['php'] = PHP_VERSION;
    printJson($counts);
}

/** @param array<string,mixed> $value */
function printJson(array $value): void
{
    $json = json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    if (!is_string($json)) {
        throw new RuntimeException('Unable to encode command output.');
    }
    fwrite(STDOUT, $json . "\n");
}

/** @param list<array<string,mixed>> $rows
 *  @param list<string> $columns
 */
function printTable(array $rows, array $columns): void
{
    if ($rows === []) {
        fwrite(STDOUT, "No records found.\n");
        return;
    }
    $widths = [];
    foreach ($columns as $column) {
        $widths[$column] = strlen($column);
    }
    foreach ($rows as $row) {
        foreach ($columns as $column) {
            $widths[$column] = min(50, max($widths[$column], strlen((string) ($row[$column] ?? ''))));
        }
    }
    foreach ($columns as $column) {
        fwrite(STDOUT, str_pad(strtoupper($column), $widths[$column] + 2));
    }
    fwrite(STDOUT, "\n");
    foreach ($rows as $row) {
        foreach ($columns as $column) {
            $value = (string) ($row[$column] ?? '');
            if (strlen($value) > $widths[$column]) {
                $value = substr($value, 0, max(0, $widths[$column] - 1)) . '…';
            }
            fwrite(STDOUT, str_pad($value, $widths[$column] + 2));
        }
        fwrite(STDOUT, "\n");
    }
}

function showHelp(): void
{
    fwrite(STDOUT, <<<'TEXT'
Wilhelm Calendar Control Center

Usage:
  bin/console migrate
  bin/console user:create EMAIL
  bin/console user:password EMAIL
  bin/console scheduler:run
  bin/console scheduler:full
  bin/console sync [SOURCE_ID]
  bin/console conflicts:scan [LOOKAHEAD_DAYS]
  bin/console notifications:dispatch [LIMIT]
  bin/console settings:show
  bin/console sources:list
  bin/console mirrors:list
  bin/console status

The production cron should run `bin/console scheduler:run` once per minute.
TEXT
    );
    fwrite(STDOUT, "\n");
}
