#!/usr/bin/env php
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/src/bootstrap.php';

use PBE\Core\Config;
use PBE\Core\Crypto;
use PBE\Core\Genesis;
use PBE\Core\KeyFile;
use PBE\Node\OperatorWallet;
use PBE\Storage\ChainStore;
use PBE\Storage\Database;

$root = dirname(__DIR__);
$dataDir = trim((string)(getenv('PBE_DATA_DIR') ?: ''));
if ($dataDir === '') $dataDir = $root . '/storage/standalone';
$configPath = '';

function serviceUsage(): void
{
    echo "PBE Standalone 0.0.1\n\n";
    echo "Usage: pbe-service [--data-dir <dir>] [--config <file>]\n";
    echo "A fresh node creates its SQLite database, node identity, and local config automatically.\n";
}

for ($i = 1; $i < $argc; $i++) {
    $arg = $argv[$i];
    if ($arg === '-h' || $arg === '--help') { serviceUsage(); exit(0); }
    if ($arg === '--data-dir') {
        if (!isset($argv[++$i])) { fwrite(STDERR, "Missing --data-dir value\n"); exit(2); }
        $dataDir = $argv[$i]; continue;
    }
    if (str_starts_with($arg, '--data-dir=')) { $dataDir = substr($arg, 11); continue; }
    if ($arg === '--config') {
        if (!isset($argv[++$i])) { fwrite(STDERR, "Missing --config value\n"); exit(2); }
        $configPath = $argv[$i]; continue;
    }
    if (str_starts_with($arg, '--config=')) { $configPath = substr($arg, 9); continue; }
    fwrite(STDERR, "Unknown option: {$arg}\n"); exit(2);
}

$dataDir = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $dataDir);
if (!str_starts_with($dataDir, DIRECTORY_SEPARATOR) && !preg_match('/^[A-Za-z]:\\\\/', $dataDir)) {
    $dataDir = getcwd() . DIRECTORY_SEPARATOR . $dataDir;
}
if (!is_dir($dataDir) && !mkdir($dataDir, 0700, true) && !is_dir($dataDir)) {
    fwrite(STDERR, "Unable to create data directory: {$dataDir}\n"); exit(1);
}
$resolvedData = realpath($dataDir);
if ($resolvedData === false) { fwrite(STDERR, "Unable to resolve data directory\n"); exit(1); }
$dataDir = $resolvedData;
foreach (['keys', 'run', 'logs'] as $sub) @mkdir($dataDir . DIRECTORY_SEPARATOR . $sub, 0700, true);

$supervisorLockPath = $dataDir . DIRECTORY_SEPARATOR . 'run' . DIRECTORY_SEPARATOR . 'supervisor.lock';
$supervisorLock = fopen($supervisorLockPath, 'c+');
if ($supervisorLock === false || !flock($supervisorLock, LOCK_EX | LOCK_NB)) {
    fwrite(STDERR, "PBE standalone supervisor is already running for this data directory.\n");
    exit(4);
}
ftruncate($supervisorLock, 0);
fwrite($supervisorLock, (string)getmypid() . PHP_EOL);
fflush($supervisorLock);
register_shutdown_function(static function () use ($supervisorLock, $supervisorLockPath): void {
    @flock($supervisorLock, LOCK_UN); @fclose($supervisorLock); @unlink($supervisorLockPath);
});

if ($configPath === '') $configPath = $dataDir . DIRECTORY_SEPARATOR . 'config.php';
if (!is_file($configPath)) {
    $nodeKey = $dataDir . DIRECTORY_SEPARATOR . 'keys' . DIRECTORY_SEPARATOR . 'node.key.json';
    if (!is_file($nodeKey)) KeyFile::save($nodeKey, Crypto::generateKeypair(), 'node');
    $producerKey = $dataDir . DIRECTORY_SEPARATOR . 'keys' . DIRECTORY_SEPARATOR . 'producer.key.json';
    $sqlitePath = $dataDir . DIRECTORY_SEPARATOR . 'pbe.sqlite';
    $genesisPath = $root . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . 'mainnet' . DIRECTORY_SEPARATOR . 'genesis.json';
    if (!is_file($genesisPath)) { fwrite(STDERR, "Bundled mainnet genesis file missing: {$genesisPath}\n"); exit(1); }

    $cfg = [
        'deployment' => [
            'mode' => 'standalone',
            'localHost' => '127.0.0.1',
            'localPort' => 8024,
            'p2pHost' => '0.0.0.0',
            'p2pPort' => 8025,
            'publicRpc' => false,
        ],
        'database' => [
            'driver' => 'sqlite',
            'path' => $sqlitePath,
        ],
        'network' => [
            'genesisFile' => $genesisPath,
            'timeoutSeconds' => 5,
            'allowPrivatePeers' => false,
            'seeds' => [
                'https://paybyte.org',
                'https://node.paybyte.org',
            ],
        ],
        'node' => [
            // Leave blank: a public endpoint is learned automatically from a verified peer.
            'endpoint' => '',
            'keyFile' => $nodeKey,
        ],
        'producer' => [
            'keyFile' => $producerKey,
        ],
        'commerce' => [
            'webhooks' => [
                'enabled' => false,
                'manageToken' => '',
                'signingSecret' => '',
            ],
        ],
    ];
    $php = "<?php\ndeclare(strict_types=1);\nreturn " . var_export($cfg, true) . ";\n";
    if (file_put_contents($configPath, $php, LOCK_EX) === false) { fwrite(STDERR, "Unable to create {$configPath}\n"); exit(1); }
    echo "Created fresh PBE Mainnet config: {$configPath}\n";
}

$resolved = realpath($configPath);
if ($resolved === false) { fwrite(STDERR, "Unable to resolve config\n"); exit(2); }
$config = Config::fromFile($resolved);
if ((string)$config->get('deployment.mode', '') !== 'standalone') {
    fwrite(STDERR, "pbe-service requires deployment.mode=standalone\n"); exit(2);
}
$localHost = (string)$config->get('deployment.localHost', '127.0.0.1');
$localPort = (int)$config->get('deployment.localPort', 8024);
$p2pHost = (string)$config->get('deployment.p2pHost', '0.0.0.0');
$p2pPort = (int)$config->get('deployment.p2pPort', 8025);
if (strtolower((string)$config->get('database.driver', '')) !== 'sqlite') {
    fwrite(STDERR, "PBE Windows standalone requires SQLite.\n"); exit(2);
}
if (!extension_loaded('pdo_sqlite')) {
    fwrite(STDERR, "PBE Windows standalone requires the PDO SQLite extension.\n"); exit(1);
}
foreach ([$localPort, $p2pPort] as $port) {
    if ($port < 1 || $port > 65535) { fwrite(STDERR, "Invalid standalone port\n"); exit(2); }
}

$nodeKeyPath = $config->path('node.keyFile');
if (!is_file($nodeKeyPath)) KeyFile::save($nodeKeyPath, Crypto::generateKeypair(), 'node');
$genesis = Genesis::fromFile($config->path('network.genesisFile'));

// Initialize/migrate SQLite once in the supervisor before exposing HTTP workers.
$startupDatabase = new Database((array)$config->get('database', []));
$startupDatabase->ensureSchema($root);
if ((bool)$config->get('commerce.webhooks.enabled', false)) $startupDatabase->ensureCommerceWebhookSchema($root);
$startupStore = new ChainStore($startupDatabase);
$startupStore->migrateSchema($genesis);
$startupStore->initializeGenesis($genesis);
unset($startupStore, $startupDatabase);

$operator = new OperatorWallet($config, $genesis);
if (!$operator->configured()) {
    echo "Node operator setup code: " . $operator->ensureSetupToken() . "\n";
    echo "Open http://{$localHost}:{$localPort}/wallet/ and bind your wallet to this node.\n";
}

$env = getenv(); if (!is_array($env)) $env = [];
$env['PBE_CONFIG'] = $resolved;
$env['PBE_DATA_DIR'] = $dataDir;
$env['PBE_RUN_DIR'] = $dataDir . DIRECTORY_SEPARATOR . 'run';
$nullDevice = PHP_OS_FAMILY === 'Windows' ? 'NUL' : '/dev/null';
$logsDir = $dataDir . DIRECTORY_SEPARATOR . 'logs';

/** @return array{proc:resource,name:string,command:array<int,string>,log:string} */
function startChild(string $name, array $command, array $env, string $nullDevice, string $logPath): array
{
    $descriptors = [
        0 => ['file', $nullDevice, 'r'],
        1 => ['file', $logPath, 'a'],
        2 => ['file', $logPath, 'a'],
    ];
    $pipes = [];
    $proc = proc_open($command, $descriptors, $pipes, null, $env, ['bypass_shell' => true]);
    if (!is_resource($proc)) throw new RuntimeException("Unable to start {$name}");
    return ['proc' => $proc, 'name' => $name, 'command' => $command, 'log' => $logPath];
}
function childAlive(array $child): bool
{
    if (!isset($child['proc']) || !is_resource($child['proc'])) return false;
    $s = proc_get_status($child['proc']);
    return !empty($s['running']);
}
function stopChild(array $child): void
{
    if (isset($child['proc']) && is_resource($child['proc'])) {
        @proc_terminate($child['proc']); usleep(200000); @proc_close($child['proc']);
    }
}
function serviceLog(string $message): void
{
    echo '[' . date('c') . '] ' . $message . PHP_EOL;
    @ob_flush(); @flush();
}

$commands = [
    'local' => [PHP_BINARY, '-S', "{$localHost}:{$localPort}", '-t', $root, $root . '/standalone-local-router.php'],
    'p2p' => [PHP_BINARY, '-S', "{$p2pHost}:{$p2pPort}", '-t', $root, $root . '/standalone-p2p-router.php'],
    'node' => [PHP_BINARY, $root . '/bin/pbe-node', '--config', $resolved],
];
$childLogs = [
    'local' => $logsDir . DIRECTORY_SEPARATOR . 'local-http.log',
    'p2p' => $logsDir . DIRECTORY_SEPARATOR . 'p2p.log',
    'node' => $logsDir . DIRECTORY_SEPARATOR . 'node.log',
];

$children = [];
$lastAttempt = ['local' => 0, 'p2p' => 0, 'node' => 0];
try {
    $children['local'] = startChild('local-http', $commands['local'], $env, $nullDevice, $childLogs['local']);
    $children['p2p'] = startChild('public-p2p', $commands['p2p'], $env, $nullDevice, $childLogs['p2p']);

    echo "PBE Standalone 0.0.1 · PBE Mainnet\n";
    echo "Dashboard: http://{$localHost}:{$localPort}/\n";
    echo "Public P2P listener: {$p2pHost}:{$p2pPort}\n";
    echo "Data directory: {$dataDir}\n";
    echo "Bootstrap peers: https://paybyte.org, https://node.paybyte.org\n";
    echo "Close/stop this supervisor to stop the standalone runtime.\n";

    while (true) {
        $now = time();
        foreach (['local', 'p2p'] as $name) {
            if (isset($children[$name]) && !childAlive($children[$name])) {
                stopChild($children[$name]); unset($children[$name]); $lastAttempt[$name] = $now;
                serviceLog("{$name} listener stopped; supervisor will retry");
            }
            if (!isset($children[$name]) && $now - $lastAttempt[$name] >= 2) {
                $lastAttempt[$name] = $now;
                $display = $name === 'local' ? 'local-http' : 'public-p2p';
                $children[$name] = startChild($display, $commands[$name], $env, $nullDevice, $childLogs[$name]);
                serviceLog("{$name} listener started");
            }
        }

        if (!isset($children['node']) && $operator->configured() && $now - $lastAttempt['node'] >= 2) {
            $lastAttempt['node'] = $now;
            $children['node'] = startChild('node', $commands['node'], $env, $nullDevice, $childLogs['node']);
            serviceLog('Operator wallet configured; blockchain daemon started');
        }
        if (isset($children['node']) && !childAlive($children['node'])) {
            stopChild($children['node']); unset($children['node']); $lastAttempt['node'] = $now;
            serviceLog('Node daemon stopped; supervisor will retry');
        }

        usleep(250000);
    }
} catch (Throwable $e) {
    fwrite(STDERR, "PBE service error: {$e->getMessage()}\n");
    exit(1);
} finally {
    foreach (array_reverse($children) as $child) stopChild($child);
}
