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

use PBE\Node\Runtime;

function usage(): void
{
    echo "PBE node daemon\n\n";
    echo "Usage:\n";
    echo "  pbe-node [--config <file>]\n";
    echo "  pbe-node [--config=<file>]\n";
    echo "  pbe-node --setup-code [--config <file>]\n";
    echo "  pbe-node --operator-status [--config <file>]\n";
    echo "  pbe-node --reset-operator-wallet --yes [--config <file>]\n\n";
    echo "Options:\n";
    echo "  --config <file>          Node configuration file (default: config/config.php)\n";
    echo "  --setup-code             Show/create the one-time /wallet node setup code\n";
    echo "  --operator-status        Show the wallet bound to this node and exit\n";
    echo "  --reset-operator-wallet  Remove the node/wallet binding (requires --yes)\n";
    echo "  --yes                    Confirm destructive operator-wallet reset\n";
    echo "  -h, --help               Show this help and exit\n";
}

$configPath = dirname(__DIR__) . '/config/config.php';
$action = 'run';
$yes = false;
for ($i = 1; $i < $argc; $i++) {
    $arg = $argv[$i];
    if ($arg === '-h' || $arg === '--help') { usage(); exit(0); }
    if ($arg === '--setup-code') { $action = 'setup-code'; continue; }
    if ($arg === '--operator-status') { $action = 'operator-status'; continue; }
    if ($arg === '--reset-operator-wallet') { $action = 'reset'; continue; }
    if ($arg === '--yes') { $yes = true; continue; }
    if ($arg === '--config') {
        if (!isset($argv[$i + 1]) || str_starts_with($argv[$i + 1], '--')) {
            fwrite(STDERR, "Missing value for --config\n"); exit(2);
        }
        $configPath = $argv[++$i];
        continue;
    }
    if (str_starts_with($arg, '--config=')) {
        $configPath = substr($arg, strlen('--config='));
        if ($configPath === '') { fwrite(STDERR, "Missing value for --config\n"); exit(2); }
        continue;
    }
    fwrite(STDERR, "Unknown option: {$arg}\n\n"); usage(); exit(2);
}

$resolvedConfig = realpath($configPath);
if ($resolvedConfig === false || !is_file($resolvedConfig)) {
    fwrite(STDERR, "Config file not found: {$configPath}\n"); exit(2);
}

$runtime = new Runtime($resolvedConfig);
$operator = $runtime->operatorWallet;

if ($action === 'reset') {
    if (!$yes) {
        fwrite(STDERR, "Refusing to reset operator wallet without --yes\n");
        exit(2);
    }
    $operator->reset();
    echo "Operator wallet binding removed.\n";
    echo "Run pbe-node again to create a new one-time /wallet setup code.\n";
    exit(0);
}

if ($action === 'operator-status') {
    $op = $operator->status();
    echo "Node ID: " . $op['nodeId'] . "\n";
    echo "Configured: " . ($op['configured'] ? 'yes' : 'no') . "\n";
    echo "Wallet: " . ($op['address'] !== '' ? $op['address'] : 'not configured') . "\n";
    echo "Block production enabled: " . ($op['miningEnabled'] ? 'yes' : 'no') . "\n";
    echo "Mining authorization: " . (!empty($op['miningAuthorized']) ? 'yes' : 'no') . "\n";
    exit($op['configured'] ? 0 : 3);
}

if ($action === 'setup-code') {
    if ($operator->configured()) {
        $op = $operator->status();
        echo "Node operator wallet is already configured.\n";
        echo "Wallet: {$op['address']}\n";
        exit(0);
    }
    echo "PBE node setup code: " . $operator->ensureSetupToken() . "\n";
    echo "Open /wallet on your node, create/import a wallet, and choose 'Bind wallet to this node'.\n";
    exit(0);
}

if (!$operator->configured()) {
    $token = $operator->ensureSetupToken();
    $status = $runtime->node->status(true);
    fwrite(STDERR, "\nPBE NODE SETUP REQUIRED\n");
    fwrite(STDERR, "-----------------------\n");
    fwrite(STDERR, "This daemon will not start until an operator wallet is created/imported in /wallet and bound to this node.\n\n");
    fwrite(STDERR, "Node ID: " . $status['nodeId'] . "\n");
    fwrite(STDERR, "Network: " . $status['chainId'] . "\n");
    fwrite(STDERR, "RPC endpoint: " . $status['endpoint'] . "\n");
    fwrite(STDERR, "One-time setup code: {$token}\n\n");
    fwrite(STDERR, "1. Keep/start the RPC/web interface.\n");
    fwrite(STDERR, "2. Open /wallet and create or import your PBE wallet.\n");
    fwrite(STDERR, "3. In Node Operator Setup, enter the code above and bind this wallet.\n");
    fwrite(STDERR, "4. Run this pbe-node command again.\n\n");
    exit(3);
}

$status = $runtime->node->status(true);
$op = $operator->status();

// Prevent two daemon processes for the same node identity from running from
// this installation at the same time. The handle remains open for daemon life.
$runDir = trim((string)(getenv('PBE_RUN_DIR') ?: ''));
if ($runDir === '') $runDir = dirname(__DIR__) . '/storage/run';
if (!is_dir($runDir) && !mkdir($runDir, 0700, true) && !is_dir($runDir)) {
    fwrite(STDERR, "Unable to create runtime directory: {$runDir}\n"); exit(1);
}
$lockPath = $runDir . '/' . preg_replace('/[^A-Za-z0-9_.-]/', '_', (string)$status['nodeId']) . '.lock';
$lockHandle = fopen($lockPath, 'c+');
if ($lockHandle === false || !flock($lockHandle, LOCK_EX | LOCK_NB)) {
    fwrite(STDERR, "PBE node already running for node ID {$status['nodeId']}\n"); exit(1);
}
ftruncate($lockHandle, 0);
fwrite($lockHandle, (string)getmypid() . PHP_EOL . $resolvedConfig . PHP_EOL);
fflush($lockHandle);
register_shutdown_function(static function () use ($lockHandle, $lockPath): void {
    flock($lockHandle, LOCK_UN); fclose($lockHandle); @unlink($lockPath);
});

echo "PBE Node " . $status['softwareVersion'] . "
";
echo "Config: " . $resolvedConfig . "
";
echo "Network: " . $status['chainId'] . "
";
echo "Node ID: " . $status['nodeId'] . "
";
echo "Endpoint: " . $status['endpoint'] . "
";
echo "Height: " . $status['height'] . "
";
echo "Operator wallet: " . $op['address'] . "
";
echo "Producer key: " . ($status['producerKeyPresent'] ? 'present' : 'not created') . "
";
echo "Block production: " . ($status['miningEnabled'] ? 'enabled' : 'disabled') . "
";
echo "Mining authorization: " . ($status['miningAuthorized'] ? 'valid' : 'not configured') . "
";
echo "Confirmed holdings: " . number_format(((int)$status['participationBalance']) / 100000000, 8, '.', '') . " PBE
";
echo "Holding multiplier: " . $status['holdingMultiplier'] . "x
";
echo "Hybrid ticket: Argon2id, one ticket per wallet per 15-second slot
";
echo "Eligibility window: " . $status['participationMinDelaySeconds'] . '-' . $status['participationMaxDelaySeconds'] . " seconds into each slot
";
if (!empty($status['miningRequested']) && !$status['miningAuthorized']) {
    echo "NOTICE: block production is enabled locally but the wallet must authorize this mining key in /wallet.
";
}

$removedPeers = $runtime->node->maintainPeerTable();
if ($removedPeers > 0) echo "Peer maintenance: removed {$removedPeers} self/incompatible record(s)\n";

$bootstrap = $runtime->node->bootstrapIfNeeded();
if ($bootstrap['attempted']) {
    if ($bootstrap['completed']) {
        echo "Peer bootstrap: complete (seed contacts={$bootstrap['seedsContacted']}, learned={$bootstrap['peersLearned']})\n";
    } else {
        echo "Peer bootstrap: waiting for first successful seed contact\n";
    }
} else {
    echo "Peer bootstrap: previously completed; using cached/gossiped peers\n";
}

$lastMempoolGossip = 0;
$lastPeerDiscovery = 0;
$lastBootstrapRetry = 0;
$lastCommerceWebhookProcess = 0;
while (true) {
    try {
        if (!$runtime->store->bootstrapCompleted() && time() - $lastBootstrapRetry >= 15) {
            $retry = $runtime->node->bootstrapIfNeeded();
            $lastBootstrapRetry = time();
            if ($retry['completed']) {
                echo '[' . date('c') . "] peer bootstrap completed; learned {$retry['peersLearned']} peer(s)\n";
            }
        }

        if ($runtime->store->bootstrapCompleted() && time() - $lastPeerDiscovery >= 60) {
            $learned = $runtime->node->discoverPeersOnce();
            if ($learned > 0) echo '[' . date('c') . "] peer gossip learned {$learned} new peer(s)\n";
            $lastPeerDiscovery = time();
        }

        $synced = $runtime->node->syncOnce();
        if ($synced > 0) {
            $syncStatus = $runtime->node->status(true);
            echo '[' . date('c') . "] synchronized {$synced} block(s); tip=#{$syncStatus['height']} peer-target=#{$syncStatus['bestKnownPeerHeight']} lag={$syncStatus['syncLagBlocks']}\n";
        }

        $block = $runtime->node->produceBlock();
        if ($block !== null) {
            $peers = $runtime->node->broadcastBlock($block);
            $role = $runtime->node->blockProductionRole($block);
            echo '[' . date('c') . '] produced block #' . $block->height . ' ' . substr($block->hashHex(), 0, 16)
                . '… role=' . $role . ' reward=' . $block->rewardAddress . " broadcast={$peers}\n";
        }

        if (time() - $lastMempoolGossip >= 5) {
            foreach ($runtime->store->mempool(50) as $tx) $runtime->node->broadcastTransaction($tx);
            $lastMempoolGossip = time();
        }

        if (time() - $lastCommerceWebhookProcess >= 1) {
            $webhooks = $runtime->commerceWebhooks->process(100, 100);
            if (($webhooks['queued'] ?? 0) > 0 || ($webhooks['delivered'] ?? 0) > 0 || ($webhooks['retried'] ?? 0) > 0) {
                echo '[' . date('c') . '] commerce webhooks: checked=' . $webhooks['checked'] . ' queued=' . $webhooks['queued'] . ' delivered=' . $webhooks['delivered'] . ' retried=' . $webhooks['retried'] . "\n";
            }
            $lastCommerceWebhookProcess = time();
        }
    } catch (Throwable $e) {
        fwrite(STDERR, '[' . date('c') . '] ' . $e::class . ': ' . $e->getMessage() . "\n");
    }
    sleep(1);
}
