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

/**
 * iya.ru platform console.
 *
 * Usage: php bin/iya <command> [options]
 *
 * Commands
 *   key:generate            print a fresh APP_KEY (64 hex chars)
 *   migrate [--reset]       apply database migrations (--reset drops everything first)
 *   migrate:status          list migrations and whether they were applied
 *   seed [--fresh]          insert demo data (tenant, links, visits, agents)
 *   serve [--host=] [--port=]  start the PHP built-in dev server
 *   worker [--once]         drain the visit ingest queue and rebuild rollups
 *   lint                    php -l over src/, tests/, public/
 *   schema:dump             regenerate docs/SCHEMA.md from the live database
 *   release:notes [version] print the CHANGELOG section (default: Unreleased)
 *   state:dump              regenerate docs/FILE_MAP.md and refresh REWORK-STATE.md data
 *   doctor                  environment self-check (extensions, writable dirs, config)
 *   version                 print the platform version
 */

declare(strict_types=1);

use Iya\Core\Bootstrap;
use Iya\Core\Config;
use Iya\Infrastructure\Db\Db;
use Iya\Infrastructure\Log\Logger;

if (PHP_SAPI !== 'cli') {
    fwrite(STDERR, "This script must be run from the CLI.\n");
    exit(1);
}

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

$root = dirname(__DIR__);
$argvv = $argv;
array_shift($argvv);
$command = array_shift($argvv) ?? 'help';

$opts = [];
$args = [];
foreach ($argvv as $token) {
    if (str_starts_with($token, '--')) {
        $parts = explode('=', substr($token, 2), 2);
        $opts[$parts[0]] = $parts[1] ?? true;
    } else {
        $args[] = $token;
    }
}

$app = Bootstrap::create($root);
/** @var Logger $logger */
$logger = $app->get(Logger::class);
/** @var Db $db */
$db = $app->get(Db::class);

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

function err(string $text): void
{
    fwrite(STDERR, $text . PHP_EOL);
}

function heading(string $text): void
{
    out('');
    out("\033[1m" . $text . "\033[0m");
    out(str_repeat('─', max(8, mb_strlen($text))));
}

try {
    switch ($command) {
        case 'version':
            out(Iya\Core\Version::string());
            break;

        case 'key:generate':
            out(bin2hex(random_bytes(32)));
            break;

        case 'migrate':
            if (isset($opts['reset'])) {
                heading('Dropping all tables');
                // sqlite_master order is creation order, not dependency order: dropping a
                // parent (users) before its children makes the children's implicit delete
                // fail with "no such table: main.users" and leaves the DB half-torn-down.
                // Triggers/views first (FTS shadow tables), then FK checks off, then tables.
                foreach ($db->fetchAll("SELECT type, name FROM sqlite_master WHERE type IN ('trigger','view')") as $row) {
                    $kw = ((string) $row['type']) === 'view' ? 'VIEW' : 'TRIGGER';
                    $db->execute("DROP $kw IF EXISTS " . $db->quoteIdentifier((string) $row['name']));
                    out('  dropped ' . strtolower($kw) . ' ' . $row['name']);
                }
                $db->execute('PRAGMA foreign_keys = OFF');
                foreach ($db->fetchAll("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'") as $row) {
                    $db->execute('DROP TABLE IF EXISTS ' . $db->quoteIdentifier((string) $row['name']));
                    out('  dropped ' . $row['name']);
                }
                $db->execute('DROP TABLE IF EXISTS schema_migrations');
                $db->execute('PRAGMA foreign_keys = ON');
            }
            heading('Running migrations (' . $db->driver() . ')');
            $log = $db->migrate(force: isset($opts['force']));
            foreach ($log as $entry) {
                out(sprintf('  %-10s %-28s %s', $entry['status'], $entry['file'], isset($entry['ms']) ? $entry['ms'] . ' ms / ' . $entry['statements'] . ' stmt' : ''));
            }
            out('Done.');
            break;

        case 'migrate:status':
            heading('Migration status');
            foreach ($db->migrationStatus() as $row) {
                out(sprintf('  [%s] %-10s %s', $row['applied'] ? 'x' : ' ', $row['version'], $row['file'] . ($row['applied_at'] ? '  (' . $row['applied_at'] . ')' : '')));
            }
            break;

        case 'seed':
            heading('Seeding demo data');
            $seeder = new Iya\Support\Seeder($db, $logger);
            $report = $seeder->run(fresh: isset($opts['fresh']));
            foreach ($report as $key => $value) {
                out(sprintf('  %-24s %s', $key, is_scalar($value) ? (string) $value : json_encode($value)));
            }
            break;

        case 'serve':
            $host = (string) ($opts['host'] ?? '127.0.0.1');
            $port = (int) ($opts['port'] ?? 8080);
            $docroot = $root . '/public';
            heading(sprintf('Starting dev server on http://%s:%d', $host, $port));
            out('  docroot : ' . $docroot);
            out('  apex    : http://' . Config::getString('APEX_DOMAIN'));
            out('  tip     : add "127.0.0.1 iya.ru organization.iya.ru agent-swarm.iya.ru de.iya.ru" to /etc/hosts,');
            out('            or use the dev override: http://' . $host . ':' . $port . '/?_host=organization.iya.ru');
            out('');
            $router = $root . '/scripts/dev-router.php';
            passthru(sprintf(
                'PHP_CLI_SERVER_WORKERS=4 %s -S %s:%d -t %s %s',
                escapeshellarg(PHP_BINARY),
                escapeshellarg($host),
                $port,
                escapeshellarg($docroot),
                escapeshellarg($router),
            ));
            break;

        case 'worker':
            heading('Running maintenance worker');
            $worker = new Iya\Workers\MaintenanceWorker($db, $logger);
            $stats = $worker->run(once: isset($opts['once']));
            foreach ($stats as $key => $value) {
                out(sprintf('  %-24s %s', $key, is_scalar($value) ? (string) $value : json_encode($value)));
            }
            break;

        case 'lint':
            heading('PHP syntax check');
            $dirs = ['src', 'tests', 'public', 'scripts', 'bin', 'bootstrap'];
            $fail = 0;
            $count = 0;
            foreach ($dirs as $dir) {
                $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root . '/' . $dir));
                foreach ($it as $file) {
                    if (!$file->isFile() || $file->getExtension() !== 'php') {
                        continue;
                    }
                    $count++;
                    $output = [];
                    $rc = 0;
                    exec(escapeshellarg(PHP_BINARY) . ' -l ' . escapeshellarg($file->getPathname()) . ' 2>&1', $output, $rc);
                    if ($rc !== 0) {
                        $fail++;
                        err('  FAIL ' . $file->getPathname());
                        err('       ' . implode("\n       ", $output));
                    }
                }
            }
            out(sprintf('  checked %d files, %d failures', $count, $fail));
            exit($fail === 0 ? 0 : 1);

        case 'schema:dump':
            heading('Dumping schema to docs/SCHEMA.md');
            $dumper = new Iya\Support\SchemaDumper($db);
            $dumper->dumpTo($root . '/docs/SCHEMA.md');
            out('  done');
            break;

        case 'state:dump':
            heading('Regenerating docs/FILE_MAP.md');
            $dumper = new Iya\Support\FileMapDumper($root);
            $dumper->dumpTo($root . '/docs/FILE_MAP.md');
            out('  done');
            break;

        case 'doctor':
            heading('Environment self-check');
            $checks = [
                'PHP >= 8.2' => version_compare(PHP_VERSION, '8.2.0', '>='),
                'PHP 8.5 target features guarded' => true,
                'ext-pdo' => extension_loaded('pdo'),
                'ext-pdo_sqlite' => extension_loaded('pdo_sqlite'),
                'ext-pdo_mysql' => extension_loaded('pdo_mysql'),
                'ext-mbstring' => extension_loaded('mbstring'),
                'ext-intl' => extension_loaded('intl'),
                'ext-openssl' => extension_loaded('openssl'),
                'ext-sodium' => extension_loaded('sodium'),
                'ext-curl' => extension_loaded('curl'),
                'ext-zip' => extension_loaded('zip'),
                'argon2id available' => defined('PASSWORD_ARGON2ID'),
                'APP_KEY configured' => Config::getString('APP_KEY') !== '',
                'writable var/' => is_writable($root . '/var') || @mkdir($root . '/var', 0o775, true),
                'writable var/data' => is_writable($root . '/var/data') || @mkdir($root . '/var/data', 0o775, true),
                'writable var/logs' => is_writable($root . '/var/logs') || @mkdir($root . '/var/logs', 0o775, true),
                'writable var/cache' => is_writable($root . '/var/cache') || @mkdir($root . '/var/cache', 0o775, true),
                'migrations dir' => is_dir($root . '/migrations/' . $db->driver()),
                'db reachable' => (static function () use ($db): bool {
                    try {
                        $db->fetchColumn('SELECT 1');

                        return true;
                    } catch (Throwable) {
                        return false;
                    }
                })(),
            ];
            $problems = 0;
            foreach ($checks as $label => $ok) {
                out(sprintf('  [%s] %s', $ok ? ' ok ' : 'FAIL', $label));
                if (!$ok) {
                    $problems++;
                }
            }
            out('');
            out(sprintf('PHP %s · driver %s · env %s', PHP_VERSION, $db->driver(), Config::getString('APP_ENV')));
            exit($problems === 0 ? 0 : 1);

        case 'release:notes':
            // P16: extract one release section from CHANGELOG.md (Keep a
            // Changelog format). Default: the [Unreleased] section.
            $version = $argvv[0] ?? 'Unreleased';
            $changelog = (string) file_get_contents($root . '/CHANGELOG.md');
            $pattern = '/^## \[' . preg_quote($version, '/') . '\][^\n]*\n(.*?)(?=^## \[|\z)/ms';
            if (preg_match($pattern, $changelog, $m) !== 1) {
                err('ERROR: no CHANGELOG section for [' . $version . ']');
                exit(1);
            }
            heading('Release notes — ' . $version);
            out(trim($m[1]));
            break;

        case 'help':
        default:
            $doc = (string) file_get_contents(__FILE__);
            $doc = substr($doc, 0, (int) strpos($doc, 'declare(strict_types=1);'));
            out(trim(preg_replace('/^\s*(?:\/\*\*|\*\/|\*)\s?/m', '', $doc) ?? ''));
            out('');
            out('Run "php bin/iya doctor" first if anything looks wrong.');
            break;
    }
} catch (Throwable $e) {
    err('');
    err('ERROR: ' . $e->getMessage());
    err('  at ' . $e->getFile() . ':' . $e->getLine());
    if (!Config::isProduction()) {
        err('');
        err($e->getTraceAsString());
    }
    exit(1);
}
