<?php
/*
 * Juggernaut v1
 *
 * BitTorrent research client with tracker, DHT and PEX discovery; peer-wire and extension
 * protocol support; verified metadata and piece storage; inbound connectivity and automatic router
 * port mapping; rarest-first downloading, adaptive request pipelining and endgame handling;
 * STANDARD and GREEDY policy modes; peer performance measurement; upload-price and marginal-return
 * estimation; connection optimisation; immediate completion shutdown; and low-overhead runtime
 * profiling.
 *
 */

// Runtime configuration.
declare(strict_types=1);
ini_set("memory_limit",  "-1");

// Research configuration.
define("RESEARCH_POLICY_STANDARD", "STANDARD");
define("RESEARCH_POLICY_GREEDY", "GREEDY");
define("RESEARCH_POLICY_MODE", RESEARCH_POLICY_GREEDY);
define("PUBLIC_TRACKER_LIST_URL", "https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt");
define("PUBLIC_TRACKER_LIST_TIMEOUT", 10);
define("HTTP_TRACKER_TIMEOUT", 10);
define("TRACKER_LIFECYCLE_ANNOUNCE_TIMEOUT", 3.0);
define("UDP_TRACKER_INITIAL_TIMEOUT", 15.0);
define("UDP_TRACKER_MAX_ATTEMPTS", 3);
define("UDP_TRACKER_BACKOFF_FACTOR", 2.0);
define("UDP_TRACKER_PROTOCOL_ID", "\x00\x00\x04\x17\x27\x10\x19\x80");
define("DESIRED_KNOWN_PEERS", 300);
define("MINIMUM_KNOWN_PEERS", 100);
define("DESIRED_CONNECTED_PEERS", 200);
define("MINIMUM_ESTABLISHED_DOWNLOAD_PEERS", 8);
define("MINIMUM_UNCHOKED_DOWNLOAD_PEERS", 3);
define("MINIMUM_USEFUL_DOWNLOAD_PEERS", 3);
define("SUPPLIER_USEFUL_RECENCY_SECONDS", 30.0);
define("DISCOVERY_AGGRESSIVE_PUBLIC_TRACKER_CONCURRENCY", 8);
define("DISCOVERY_NORMAL_PUBLIC_TRACKER_CONCURRENCY", 3);
define("DISCOVERY_AGGRESSIVE_TRACKER_STAGGER_SECONDS", 0.25);
define("DISCOVERY_NORMAL_TRACKER_STAGGER_SECONDS", 1.0);
define("DISCOVERY_MAINTENANCE_TRACKER_STAGGER_SECONDS", 30.0);
define("DESIRED_DOWNLOAD_PEERS", 25);
define("DESIRED_UPLOAD_PEERS", 15);
define("UPLOAD_LIMIT_BYTES_PER_SECOND", 10 * 1024 * 1024);
define("EXPLORE_RATIO", 0.10);
define("GREEDY_SHORT_RATE_WEIGHT", 0.70);
define("PEER_EVALUATION_INTERVAL", 10);
define("PEER_METRIC_SHORT_EWMA_ALPHA", 0.50);
define("PEER_METRIC_LONG_EWMA_ALPHA", 0.10);
define("STANDARD_OPTIMISTIC_UNCHOKE_INTERVAL", 30);
define("STANDARD_POLICY_REFRESH_INTERVAL", 0.50);
define("OPTIMISER_INTERVAL", 10);
define("CONNECTION_OPTIMISER_MIN_CONNECTION_AGE", 20.0);
define("CONNECTION_OPTIMISER_HYSTERESIS_RATIO", 0.25);
define("CONNECTION_OPTIMISER_MIN_RATE_GAIN", 32 * 1024);
define("CONNECTION_OPTIMISER_POOR_RATE_THRESHOLD", 8 * 1024);
define("CONNECTION_OPTIMISER_MAX_REPLACEMENTS_PER_INTERVAL", 2);
define("CONNECTION_OPTIMISER_DYNAMIC_MAX_REPLACEMENTS_PER_INTERVAL", 8);
define("CONNECTION_OPTIMISER_DYNAMIC_MARKET_THRESHOLD", 1000);
define("CONNECTION_OPTIMISER_DYNAMIC_REPLACEABLE_THRESHOLD", 32);
define("FRESH_SERVICE_WINDOW", 20.0);
define("FRESH_SERVICE_TURNOVER_MIN_AGE", 20.0);
define("FRESH_SERVICE_TURNOVER_MAX_PEER_VALUE", 128 * 1024);
define("FRESH_SERVICE_TURNOVER_MAX_PER_INTERVAL", 1);
define("FRESH_SERVICE_CANDIDATE_PRIORITY_SECONDS", 60.0);
define("PRICE_DECREASE_RATIO", 0.10);
define("PRICE_INCREASE_RATIO", 0.20);
define("PRICE_RESPONSE_COLLAPSE_RATIO", 0.50);
define("PRICE_RETEST_INTERVAL", 60.0);
define("UPLOAD_ALLOCATION_QUANTUM", 25 * 1024);
define("MARGINAL_RETURN_EWMA_ALPHA", 0.35);
define("MARGINAL_RETURN_MIN_UPLOAD_CHANGE", UPLOAD_ALLOCATION_QUANTUM * 0.25);
define("TARGET_PIPELINE_SECONDS", 2.0);
define("DOWNLOAD_PIPELINE_MIN_REQUESTS", 2);
define("DOWNLOAD_PIPELINE_MAX_REQUESTS", 64);
define("DOWNLOAD_PIPELINE_COLD_START_REQUESTS", 8);
define("DOWNLOAD_PIPELINE_LIVE_MIN_COMPLETIONS", 4);
define("DOWNLOAD_PIPELINE_LATENCY_HEADROOM", 1.25);
define("ENDGAME_UNFINISHED_BLOCK_THRESHOLD", 64);
define("ENDGAME_MAX_REQUEST_COPIES_PER_BLOCK", 2);
define("ENDGAME_DUPLICATE_MIN_AGE_SECONDS", 0.25);
define("CONNECT_TIMEOUT", 10);
define("PEER_COOLDOWN", 60);
define("PEER_CONNECT_RETRY_BASE_SECONDS", 5.0);
define("PEER_CONNECT_RETRY_MAX_SECONDS", 120.0);
define("PEER_CONNECT_RETRY_JITTER_RATIO", 0.30);
define("INBOUND_HANDSHAKE_OVERFLOW", 8);
define("INBOUND_HEADROOM_MAX", 32);
define("INBOUND_HEADROOM_WINDOW", 30.0);
define("INBOUND_ACCEPT_BURST", 32);
define("PORT_MAPPING_LIFETIME_SECONDS", 3600);
define("PORT_MAPPING_RENEW_FRACTION", 0.50);
define("PORT_MAPPING_UDP_TIMEOUT", 0.35);
define("PORT_MAPPING_UPNP_DISCOVERY_TIMEOUT", 4.0);
define("PORT_MAPPING_UPNP_MX_SECONDS", 2);
define("PORT_MAPPING_UPNP_SEARCH_REPETITIONS", 2);
define("PORT_MAPPING_HTTP_TIMEOUT", 35.0);
define("PORT_MAPPING_SOAP_TIMEOUT", 35.0);
define("PORT_MAPPING_UPNP_CONNECT_TIMEOUT", 5.0);
define("PORT_MAPPING_UPNP_MAX_RESPONSE_BYTES", 2097152);
define("PORT_MAPPING_DESCRIPTION", "Juggernaut");
define("DHT_K", 8);
define("DHT_ALPHA", 3);
define("DHT_QUERY_TIMEOUT", 5.0);
define("DHT_NODE_GOOD_SECONDS", 15 * 60);
define("DHT_NODE_BAD_FAILURES", 2);
define("DHT_LOOKUP_REFRESH_INTERVAL", 60.0);
define("DHT_AGGRESSIVE_LOOKUP_REFRESH_INTERVAL", 15.0);
define("DHT_MAINTENANCE_LOOKUP_REFRESH_INTERVAL", 180.0);
define("DHT_TOKEN_ROTATE_INTERVAL", 5 * 60);
define("DHT_PEER_STORE_TTL", 30 * 60);
define("DHT_MAX_PACKET_LENGTH", 65535);
define("DHT_CLIENT_VERSION", "JN01");
define("DHT_BOOTSTRAP_NODES", ["router.bittorrent.com:6881", "router.utorrent.com:6881", "dht.transmissionbt.com:6881"]);
define("BITTORRENT_PROTOCOL_NAME", "BitTorrent protocol");
define("BITTORRENT_HANDSHAKE_LENGTH", 68);
define("PEER_READ_CHUNK_SIZE", 64 * 1024);
define("PEER_INPUT_BUFFER_COMPACT_THRESHOLD", 256 * 1024);
define("PIECE_PRIORITY_REBUILD_MIN_INTERVAL", 0.10);
define("GREEDY_POLICY_REFRESH_INTERVAL", 0.50);
define("PEER_MESSAGE_MAX_LENGTH", 16 * 1024 * 1024);
define("PEER_BLOCK_MAX_LENGTH", 16 * 1024);
define("PEER_MESSAGE_CHOKE", 0);
define("PEER_MESSAGE_UNCHOKE", 1);
define("PEER_MESSAGE_INTERESTED", 2);
define("PEER_MESSAGE_NOT_INTERESTED", 3);
define("PEER_MESSAGE_HAVE", 4);
define("PEER_MESSAGE_BITFIELD", 5);
define("PEER_MESSAGE_REQUEST", 6);
define("PEER_MESSAGE_PIECE", 7);
define("PEER_MESSAGE_CANCEL", 8);
define("PEER_MESSAGE_PORT", 9);
define("PEER_MESSAGE_EXTENDED", 20);
define("PEER_EXTENSION_HANDSHAKE", 0);
define("PEER_EXTENSION_RESERVED_BYTE_INDEX", 5);
define("PEER_EXTENSION_RESERVED_MASK", 0x10);
define("PEER_DHT_RESERVED_BYTE_INDEX", 7);
define("PEER_DHT_RESERVED_MASK", 0x01);
define("UT_METADATA_EXTENSION_NAME", "ut_metadata");
define("UT_METADATA_LOCAL_ID", 1);
define("UT_METADATA_BLOCK_LENGTH", 16 * 1024);
define("UT_METADATA_MAX_SIZE", 10 * 1024 * 1024);
define("UT_METADATA_REQUEST", 0);
define("UT_METADATA_DATA", 1);
define("UT_METADATA_REJECT", 2);
define("UT_PEX_EXTENSION_NAME", "ut_pex");
define("UT_PEX_LOCAL_ID", 2);
define("PEX_INTERVAL", 60.0);
define("PEX_SERVICE_CHECK_INTERVAL", 1.0);
define("PEX_MAX_ADDED_PER_MESSAGE", 50);
define("PEX_MAX_DROPPED_PER_MESSAGE", 50);
define("PEX_MAX_INITIAL_CONTACTS", 200);
define("TORRENT_PATH_COMPONENT_MAX_LENGTH", 255);
define("CLIENT_ANNOUNCE_PORT", 6881);
define("TRACKER_METADATA_LEFT", 1);
define("RUNTIME_METADATA_REQUEST_TIMEOUT", 12.0);
define("RUNTIME_METADATA_REQUEST_PIPELINE", 4);
define("BASIC_DOWNLOAD_REQUEST_TIMEOUT", 15.0);
define("BASIC_PEER_AVAILABILITY_TIMEOUT", 10.0);
define("DOWNLOAD_STATUS_INTERVAL", 5.0);
define("TRACKER_TRANSFER_COUNTER_REFRESH_INTERVAL", 1.0);
define("RUNTIME_PROFILING_ENABLED", true);
define("PROGRESS_RATE_EWMA_ALPHA", 0.30);
define("BASIC_UPLOAD_BURST_SECONDS", 1.0);
define("BASIC_UPLOAD_MAX_PENDING_REQUESTS_PER_PEER", 256);
define("RUNTIME_SELECT_TIMEOUT_MICROSECONDS", 250000);
define("RUNTIME_PIECE_LOG_LIMIT", 20);
define("LOG_OPTIMISER_DECISIONS", true);
define("LOG_PEER_METRICS", true);
define("PEER_METRIC_LOG_LIMIT", 30);
define("TERMINAL_CONNECTION_PRUNE_INTERVAL", 1.0);

// Logging.
$GLOBALS["RUNTIME_LOG_REDIRECTS"] = [];
$GLOBALS["RUNTIME_PROGRESS_STATES"] = [];

function format_runtime_duration($seconds) {
    if((!is_int($seconds) && !is_float($seconds)) || !is_finite(floatval($seconds)) || $seconds < 0)
        throw new InvalidArgumentException("Runtime duration must be a finite non-negative number of seconds.");

    $total_seconds = intval(floor(floatval($seconds)));
    $hours = intdiv($total_seconds, 3600);
    $minutes = intdiv($total_seconds % 3600, 60);
    $remaining_seconds = $total_seconds % 60;

    return sprintf("%02d:%02d:%02d", $hours, $minutes, $remaining_seconds);
}

function estimate_runtime_eta_seconds($remaining_bytes, $rate_bytes_per_second) {
    if(!is_int($remaining_bytes) || $remaining_bytes < 0)
        throw new InvalidArgumentException("Runtime ETA remaining bytes must be a non-negative integer.");

    if((!is_int($rate_bytes_per_second) && !is_float($rate_bytes_per_second)) || !is_finite(floatval($rate_bytes_per_second)))
        throw new InvalidArgumentException("Runtime ETA rate must be finite.");

    if($remaining_bytes === 0)
        return 0.0;

    if($rate_bytes_per_second <= 0)
        return null;

    return $remaining_bytes / floatval($rate_bytes_per_second);
}

function calculate_runtime_progress_percent($verified_bytes, $total_bytes, $is_complete = false) {
    if(!is_int($verified_bytes) || $verified_bytes < 0 || !is_int($total_bytes) || $total_bytes < 0)
        throw new InvalidArgumentException("Runtime progress byte counts must be non-negative integers.");

    if(!is_bool($is_complete))
        throw new InvalidArgumentException("Runtime progress completion state must be boolean.");

    if($total_bytes === 0)
        return $is_complete ? 100.0 : 0.0;

    $progress_percent = (min($verified_bytes, $total_bytes) / $total_bytes) * 100.0;

    if(!$is_complete && $verified_bytes < $total_bytes)
        return min(99.99, $progress_percent);

    return min(100.0, $progress_percent);
}

function write_runtime_progress_line($stream, $message) {
    if(!is_resource($stream))
        throw new InvalidArgumentException("Runtime progress output requires an open stream.");

    if(!is_string($message) || str_contains($message, "\n") || str_contains($message, "\r"))
        throw new InvalidArgumentException("Runtime progress message must be a single line.");

    $stream_id = get_resource_id($stream);
    $previous_length = $GLOBALS["RUNTIME_PROGRESS_STATES"][$stream_id]["length"] ?? 0;
    $padding = max(0, $previous_length - strlen($message));

    if(fwrite($stream, "\r" . $message . str_repeat(" ", $padding)) === false)
        throw new RuntimeException("Runtime progress write failed.");

    fflush($stream);
    $GLOBALS["RUNTIME_PROGRESS_STATES"][$stream_id] = [
        "length" => strlen($message),
        "active" => true,
    ];
}

function finish_runtime_progress_line($stream) {
    if(!is_resource($stream))
        return false;

    $stream_id = get_resource_id($stream);
    $state = $GLOBALS["RUNTIME_PROGRESS_STATES"][$stream_id] ?? null;

    if(!is_array($state) || !($state["active"] ?? false))
        return false;

    fwrite($stream, "\n");
    fflush($stream);
    unset($GLOBALS["RUNTIME_PROGRESS_STATES"][$stream_id]);

    return true;
}

function runtime_process_cpu_usage_seconds() {
    if(!function_exists("getrusage"))
        return ["user" => null, "system" => null];

    $usage = @getrusage();

    if(!is_array($usage))
        return ["user" => null, "system" => null];

    $read_time = static function($seconds_key, $microseconds_key) use ($usage) {
        if(!isset($usage[$seconds_key], $usage[$microseconds_key]))
            return null;

        return floatval($usage[$seconds_key]) + (floatval($usage[$microseconds_key]) / 1000000.0);
    };

    return [
        "user" => $read_time("ru_utime.tv_sec", "ru_utime.tv_usec"),
        "system" => $read_time("ru_stime.tv_sec", "ru_stime.tv_usec"),
    ];
}

final class RuntimeProfiler {
    private bool $enabled;
    private float $started_at;
    private float $last_sample_at;
    private array $started_cpu;
    private array $last_cpu;
    private array $totals;
    private array $last_totals;

    public function __construct($now = null, $enabled = RUNTIME_PROFILING_ENABLED) {
        if(!is_bool($enabled))
            throw new InvalidArgumentException("Runtime profiler enabled state must be boolean.");

        $now = normalise_peer_time($now);
        $this->enabled = $enabled;
        $this->started_at = $now;
        $this->last_sample_at = $now;
        $this->started_cpu = runtime_process_cpu_usage_seconds();
        $this->last_cpu = $this->started_cpu;
        $this->totals = [
            "loop_iterations" => 0,
            "select_calls" => 0,
            "select_ready_sockets" => 0,
            "select_wait_seconds" => 0.0,
            "idle_wait_seconds" => 0.0,
            "read_callbacks" => 0,
            "write_callbacks" => 0,
            "messages" => 0,
            "piece_messages" => 0,
            "empty_connections_pruned" => 0,
            "terminal_connections_pruned" => 0,
        ];
        $this->last_totals = $this->totals;
    }

    public function is_enabled() {
        return $this->enabled;
    }

    public function record_loop_iteration() {
        if($this->enabled)
            $this->totals["loop_iterations"]++;
    }

    public function record_select($elapsed_seconds, $ready_sockets) {
        if(!$this->enabled)
            return;

        if((!is_int($elapsed_seconds) && !is_float($elapsed_seconds)) || $elapsed_seconds < 0)
            throw new InvalidArgumentException("Runtime profiler select duration must be non-negative.");

        if(!is_int($ready_sockets) || $ready_sockets < 0)
            throw new InvalidArgumentException("Runtime profiler ready-socket count must be non-negative.");

        $this->totals["select_calls"]++;
        $this->totals["select_ready_sockets"] += $ready_sockets;
        $this->totals["select_wait_seconds"] += floatval($elapsed_seconds);
    }

    public function record_idle_wait($elapsed_seconds) {
        if(!$this->enabled)
            return;

        if((!is_int($elapsed_seconds) && !is_float($elapsed_seconds)) || $elapsed_seconds < 0)
            throw new InvalidArgumentException("Runtime profiler idle duration must be non-negative.");

        $this->totals["idle_wait_seconds"] += floatval($elapsed_seconds);
    }

    public function record_socket_callbacks($read_callbacks, $write_callbacks) {
        if(!$this->enabled)
            return;

        if(!is_int($read_callbacks) || $read_callbacks < 0 || !is_int($write_callbacks) || $write_callbacks < 0)
            throw new InvalidArgumentException("Runtime profiler socket callback counts must be non-negative integers.");

        $this->totals["read_callbacks"] += $read_callbacks;
        $this->totals["write_callbacks"] += $write_callbacks;
    }

    public function record_messages($messages, $piece_messages = 0) {
        if(!$this->enabled)
            return;

        if(!is_int($messages) || $messages < 0 || !is_int($piece_messages) || $piece_messages < 0 || $piece_messages > $messages)
            throw new InvalidArgumentException("Runtime profiler message counts are invalid.");

        $this->totals["messages"] += $messages;
        $this->totals["piece_messages"] += $piece_messages;
    }

    public function record_pruned_connections($empty_connections, $terminal_connections) {
        if(!$this->enabled)
            return;

        if(!is_int($empty_connections) || $empty_connections < 0 || !is_int($terminal_connections) || $terminal_connections < 0)
            throw new InvalidArgumentException("Runtime profiler pruned connection counts must be non-negative integers.");

        $this->totals["empty_connections_pruned"] += $empty_connections;
        $this->totals["terminal_connections_pruned"] += $terminal_connections;
    }

    private function cpu_delta($current_cpu, $previous_cpu) {
        if($current_cpu["user"] === null || $current_cpu["system"] === null || $previous_cpu["user"] === null || $previous_cpu["system"] === null)
            return ["user" => null, "system" => null, "total" => null];

        $user = max(0.0, $current_cpu["user"] - $previous_cpu["user"]);
        $system = max(0.0, $current_cpu["system"] - $previous_cpu["system"]);

        return ["user" => $user, "system" => $system, "total" => $user + $system];
    }

    private function counter_delta($name) {
        return $this->totals[$name] - $this->last_totals[$name];
    }

    public function sample($now = null) {
        $now = normalise_peer_time($now);
        $interval = max(0.001, $now - $this->last_sample_at);
        $current_cpu = runtime_process_cpu_usage_seconds();
        $cpu = $this->cpu_delta($current_cpu, $this->last_cpu);
        $select_calls = $this->counter_delta("select_calls");
        $ready_sockets = $this->counter_delta("select_ready_sockets");
        $sample = [
            "enabled" => $this->enabled,
            "interval_seconds" => $interval,
            "cpu_user_seconds" => $cpu["user"],
            "cpu_system_seconds" => $cpu["system"],
            "cpu_percent" => $cpu["total"] === null ? null : ($cpu["total"] / $interval) * 100.0,
            "loop_hz" => $this->counter_delta("loop_iterations") / $interval,
            "select_calls_per_second" => $select_calls / $interval,
            "ready_sockets_per_select" => $select_calls > 0 ? $ready_sockets / $select_calls : 0.0,
            "select_wait_percent" => ($this->counter_delta("select_wait_seconds") / $interval) * 100.0,
            "idle_wait_percent" => ($this->counter_delta("idle_wait_seconds") / $interval) * 100.0,
            "read_callbacks_per_second" => $this->counter_delta("read_callbacks") / $interval,
            "write_callbacks_per_second" => $this->counter_delta("write_callbacks") / $interval,
            "messages_per_second" => $this->counter_delta("messages") / $interval,
            "piece_messages_per_second" => $this->counter_delta("piece_messages") / $interval,
            "empty_connections_pruned" => $this->counter_delta("empty_connections_pruned"),
            "terminal_connections_pruned" => $this->counter_delta("terminal_connections_pruned"),
            "memory_current_bytes" => memory_get_usage(true),
            "memory_peak_bytes" => memory_get_peak_usage(true),
        ];
        $this->last_sample_at = $now;
        $this->last_cpu = $current_cpu;
        $this->last_totals = $this->totals;

        return $sample;
    }

    public function summary($now = null) {
        $now = normalise_peer_time($now);
        $elapsed = max(0.001, $now - $this->started_at);
        $current_cpu = runtime_process_cpu_usage_seconds();
        $cpu = $this->cpu_delta($current_cpu, $this->started_cpu);

        return [
            "enabled" => $this->enabled,
            "elapsed_seconds" => $elapsed,
            "cpu_user_seconds" => $cpu["user"],
            "cpu_system_seconds" => $cpu["system"],
            "cpu_percent" => $cpu["total"] === null ? null : ($cpu["total"] / $elapsed) * 100.0,
            "loop_iterations" => $this->totals["loop_iterations"],
            "loop_hz" => $this->totals["loop_iterations"] / $elapsed,
            "select_calls" => $this->totals["select_calls"],
            "select_ready_sockets" => $this->totals["select_ready_sockets"],
            "select_wait_seconds" => $this->totals["select_wait_seconds"],
            "select_wait_percent" => ($this->totals["select_wait_seconds"] / $elapsed) * 100.0,
            "idle_wait_seconds" => $this->totals["idle_wait_seconds"],
            "idle_wait_percent" => ($this->totals["idle_wait_seconds"] / $elapsed) * 100.0,
            "read_callbacks" => $this->totals["read_callbacks"],
            "write_callbacks" => $this->totals["write_callbacks"],
            "messages" => $this->totals["messages"],
            "piece_messages" => $this->totals["piece_messages"],
            "empty_connections_pruned" => $this->totals["empty_connections_pruned"],
            "terminal_connections_pruned" => $this->totals["terminal_connections_pruned"],
            "memory_current_bytes" => memory_get_usage(true),
            "memory_peak_bytes" => memory_get_peak_usage(true),
        ];
    }
}

function log_runtime_profile_sample($sample, $piece_priority_generation, $connection_statistics, $log_stream) {
    if(!is_array($sample) || !is_array($connection_statistics))
        throw new InvalidArgumentException("Runtime profile log requires profile and connection statistics.");

    $cpu_percent = $sample["cpu_percent"] === null ? "n/a" : sprintf("%.1f", $sample["cpu_percent"]);
    log_message(
        sprintf(
            "PROFILE_STATUS cpu_percent=%s loop_hz=%.1f select_wait_percent=%.1f idle_wait_percent=%.1f select_calls_per_second=%.1f ready_sockets_per_select=%.2f read_callbacks_per_second=%.1f write_callbacks_per_second=%.1f messages_per_second=%.1f piece_messages_per_second=%.1f priority_generation=%d active_connections=%d established_connections=%d memory_current_bytes=%d memory_peak_bytes=%d empty_pruned=%d terminal_pruned=%d",
            $cpu_percent,
            $sample["loop_hz"],
            $sample["select_wait_percent"],
            $sample["idle_wait_percent"],
            $sample["select_calls_per_second"],
            $sample["ready_sockets_per_select"],
            $sample["read_callbacks_per_second"],
            $sample["write_callbacks_per_second"],
            $sample["messages_per_second"],
            $sample["piece_messages_per_second"],
            $piece_priority_generation,
            $connection_statistics["active"],
            $connection_statistics["established"],
            $sample["memory_current_bytes"],
            $sample["memory_peak_bytes"],
            $sample["empty_connections_pruned"],
            $sample["terminal_connections_pruned"]
        ),
        $log_stream
    );
}

function create_runtime_deferred_log_stream() {
    $stream = fopen("php://memory", "w+b");

    if($stream === false)
        throw new RuntimeException("Runtime startup log buffer could not be opened.");

    return $stream;
}

function runtime_resolve_log_stream($stream) {
    if(!is_resource($stream))
        throw new InvalidArgumentException("Runtime logging requires an open stream.");

    $stream_id = get_resource_id($stream);
    $redirect = $GLOBALS["RUNTIME_LOG_REDIRECTS"][$stream_id] ?? null;

    if(is_resource($redirect))
        return $redirect;

    return $stream;
}

function build_runtime_log_path($metadata, $base_path = null) {
    if(!($metadata instanceof TorrentMetadata))
        throw new InvalidArgumentException("Runtime log path requires verified torrent metadata.");

    $base_path = normalise_torrent_storage_base_path($base_path);

    if($metadata->is_multi_file) {
        $log_name = $metadata->name . ".log";
        validate_torrent_path_component($log_name);

        return build_torrent_storage_path($base_path, [$log_name]);
    }

    if(count($metadata->files) !== 1)
        throw new RuntimeException("Single-file torrent metadata must expose exactly one file.");

    $target_path = build_torrent_storage_path(
        $base_path,
        $metadata->files[0]->path_components
    );

    return $target_path . ".log";
}

function redirect_runtime_log_stream_to_file($stream, $path) {
    if(!is_resource($stream))
        throw new InvalidArgumentException("Runtime log redirection requires an open source stream.");

    if(!is_string($path) || $path === "" || str_contains($path, "\0"))
        throw new InvalidArgumentException("Runtime log redirection requires a valid path.");

    $target = @fopen($path, "ab");

    if($target === false)
        throw new RuntimeException("Runtime log file could not be opened for appending: {$path}");

    $source_position = ftell($stream);

    if($source_position === false || fseek($stream, 0) !== 0) {
        fclose($target);
        throw new RuntimeException("Runtime startup log buffer could not be rewound.");
    }

    if(fwrite($target, "\n=== BitTorrent session " . date("c") . " ===\n") === false) {
        fclose($target);
        throw new RuntimeException("Runtime log session header could not be written.");
    }

    if(stream_copy_to_stream($stream, $target) === false) {
        fclose($target);
        throw new RuntimeException("Runtime startup log could not be appended to the final log file.");
    }

    if(!fflush($target)) {
        fclose($target);
        throw new RuntimeException("Runtime log file could not be flushed after startup history append.");
    }

    if(fseek($stream, $source_position) !== 0) {
        fclose($target);
        throw new RuntimeException("Runtime startup log buffer position could not be restored.");
    }

    $GLOBALS["RUNTIME_LOG_REDIRECTS"][get_resource_id($stream)] = $target;

    return $target;
}

function close_runtime_log_stream($stream) {
    if(!is_resource($stream))
        return;

    $stream_id = get_resource_id($stream);
    $redirect = $GLOBALS["RUNTIME_LOG_REDIRECTS"][$stream_id] ?? null;

    if(is_resource($redirect)) {
        fflush($redirect);
        fclose($redirect);
    }

    unset($GLOBALS["RUNTIME_LOG_REDIRECTS"][$stream_id]);
}

function log_message($message, $stream) {
    $target = runtime_resolve_log_stream($stream);

    if(fwrite($target, "{$message}\n") === false)
        throw new RuntimeException("Runtime log write failed.");
}

// Bencode representation.
define("BENCODE_MAX_DEPTH", 512);

final class BencodeDictionary {
    public readonly array $values;

    public function __construct($values = []) {
        $this->values = $values;
    }
}

// Bencode decoding.
function bencode_decode_integer($data, &$offset) {
    $terminator = strpos($data, "e", $offset + 1);

    if($terminator === false)
        throw new InvalidArgumentException("Unterminated bencode integer.");

    $encoded_integer = substr($data, $offset + 1, $terminator - $offset - 1);

    if(preg_match("/\A(?:0|-?[1-9][0-9]*)\z/", $encoded_integer) !== 1)
        throw new InvalidArgumentException("Invalid bencode integer.");

    $integer = filter_var($encoded_integer, FILTER_VALIDATE_INT);

    if($integer === false)
        throw new InvalidArgumentException("Bencode integer is outside the supported range.");

    $offset = $terminator + 1;

    return $integer;
}

function bencode_decode_byte_string($data, &$offset) {
    $separator = strpos($data, ":", $offset);

    if($separator === false)
        throw new InvalidArgumentException("Bencode byte string has no length separator.");

    $encoded_length = substr($data, $offset, $separator - $offset);

    if(preg_match("/\A(?:0|[1-9][0-9]*)\z/", $encoded_length) !== 1)
        throw new InvalidArgumentException("Invalid bencode byte-string length.");

    $length = filter_var(
        $encoded_length,
        FILTER_VALIDATE_INT,
        ["options" => ["min_range" => 0]]
    );

    if($length === false)
        throw new InvalidArgumentException("Bencode byte-string length is outside the supported range.");

    $offset = $separator + 1;

    if($length > strlen($data) - $offset)
        throw new InvalidArgumentException("Bencode byte string is shorter than its declared length.");

    $value = substr($data, $offset, $length);
    $offset += $length;

    return $value;
}

function bencode_decode_list($data, &$offset, $depth) {
    $values = [];
    $offset++;

    while(true) {
        if($offset >= strlen($data))
            throw new InvalidArgumentException("Unterminated bencode list.");

        if($data[$offset] === "e") {
            $offset++;

            return $values;
        }

        $values[] = bencode_decode_value($data, $offset, $depth + 1);
    }
}

function bencode_decode_dictionary($data, &$offset, $depth) {
    $values = [];
    $previous_key = null;
    $offset++;

    while(true) {
        if($offset >= strlen($data))
            throw new InvalidArgumentException("Unterminated bencode dictionary.");

        if($data[$offset] === "e") {
            $offset++;

            return $values;
        }

        if($data[$offset] < "0" || $data[$offset] > "9")
            throw new InvalidArgumentException("Bencode dictionary keys must be byte strings.");

        $key = bencode_decode_byte_string($data, $offset);

        if($previous_key !== null && strcmp($previous_key, $key) >= 0)
            throw new InvalidArgumentException("Bencode dictionary keys must be unique and sorted.");

        $previous_key = $key;
        $values[$key] = bencode_decode_value($data, $offset, $depth + 1);
    }
}

function bencode_decode_value($data, &$offset, $depth) {
    if($depth > BENCODE_MAX_DEPTH)
        throw new InvalidArgumentException("Bencode nesting exceeds the supported depth.");

    if($offset >= strlen($data))
        throw new InvalidArgumentException("Unexpected end of bencode data.");

    if($data[$offset] === "i")
        return bencode_decode_integer($data, $offset);

    if($data[$offset] === "l")
        return bencode_decode_list($data, $offset, $depth);

    if($data[$offset] === "d")
        return bencode_decode_dictionary($data, $offset, $depth);

    if($data[$offset] >= "0" && $data[$offset] <= "9")
        return bencode_decode_byte_string($data, $offset);

    throw new InvalidArgumentException("Invalid bencode value marker.");
}

function bencode_decode($data) {
    $offset = 0;
    $value = bencode_decode_value($data, $offset, 0);

    if($offset !== strlen($data))
        throw new InvalidArgumentException("Trailing data follows the bencode value.");

    return $value;
}

// Bencode encoding.
function bencode_encode_dictionary($dictionary, $depth) {
    $entries = [];

    foreach($dictionary as $key => $value) {
        $entries[] = [
            "key" => is_int($key) ? strval($key) : $key,
            "value" => $value,
        ];
    }

    usort(
        $entries,
        static function($left, $right) {
            return strcmp($left["key"], $right["key"]);
        }
    );

    $encoded = "d";

    foreach($entries as $entry) {
        $encoded .= strlen($entry["key"]) . ":" . $entry["key"];
        $encoded .= bencode_encode_value($entry["value"], $depth + 1);
    }

    return "{$encoded}e";
}

function bencode_encode_value($value, $depth) {
    if($depth > BENCODE_MAX_DEPTH)
        throw new InvalidArgumentException("Bencode nesting exceeds the supported depth.");

    if(is_int($value))
        return "i{$value}e";

    if(is_string($value))
        return strlen($value) . ":" . $value;

    if($value instanceof BencodeDictionary)
        return bencode_encode_dictionary($value->values, $depth);

    if(is_array($value) && !array_is_list($value))
        return bencode_encode_dictionary($value, $depth);

    if(is_array($value)) {
        $encoded = "l";

        foreach($value as $item)
            $encoded .= bencode_encode_value($item, $depth + 1);

        return "{$encoded}e";
    }

    throw new InvalidArgumentException("Unsupported PHP value for bencode encoding.");
}

function bencode_encode($value) {
    return bencode_encode_value($value, 0);
}

// Magnet representation.
final class MagnetUri {
    public readonly string $original_uri;
    public readonly string $info_hash;
    public readonly string $info_hash_hex;
    public readonly ?string $display_name;
    public readonly array $trackers;
    public readonly array $explicit_peers;

    public function __construct(
        $original_uri,
        $info_hash,
        $info_hash_hex,
        $display_name,
        $trackers,
        $explicit_peers
    ) {
        $this->original_uri = $original_uri;
        $this->info_hash = $info_hash;
        $this->info_hash_hex = $info_hash_hex;
        $this->display_name = $display_name;
        $this->trackers = $trackers;
        $this->explicit_peers = $explicit_peers;
    }
}

// Magnet parsing.
function decode_magnet_component($component) {
    if(preg_match("/%(?![0-9A-Fa-f]{2})/", $component) === 1)
        throw new InvalidArgumentException("Magnet link contains malformed percent encoding.");

    return urldecode($component);
}

function parse_magnet_parameters($query) {
    $parameters = [
        "xt" => [],
        "dn" => [],
        "tr" => [],
        "x.pe" => [],
    ];

    foreach(explode("&", $query) as $component) {
        if($component === "")
            continue;

        $pair = explode("=", $component, 2);
        $name = decode_magnet_component($pair[0]);
        $value = decode_magnet_component($pair[1] ?? "");

        if(array_key_exists($name, $parameters))
            $parameters[$name][] = $value;
    }

    return $parameters;
}

function decode_base32_info_hash($encoded_hash) {
    if(preg_match("/\A[A-Z2-7]{32}\z/i", $encoded_hash) !== 1)
        throw new InvalidArgumentException("Invalid Base32 BitTorrent v1 info hash.");

    $buffer = 0;
    $buffer_bits = 0;
    $info_hash = "";

    foreach(str_split(strtoupper($encoded_hash)) as $character) {
        $value = strpos("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", $character);
        $buffer = ($buffer << 5) | $value;
        $buffer_bits += 5;

        if($buffer_bits < 8)
            continue;

        $buffer_bits -= 8;
        $info_hash .= chr(($buffer >> $buffer_bits) & 0xff);
        $buffer = $buffer_bits === 0 ? 0 : $buffer & ((1 << $buffer_bits) - 1);
    }

    return $info_hash;
}

function decode_v1_exact_topic($exact_topic) {
    if(preg_match("/\Aurn:btih:([0-9A-Fa-f]{40})\z/i", $exact_topic, $matches) === 1)
        return pack("H*", $matches[1]);

    if(preg_match("/\Aurn:btih:([A-Z2-7]{32})\z/i", $exact_topic, $matches) === 1)
        return decode_base32_info_hash($matches[1]);

    return null;
}

function select_v1_info_hash($exact_topics) {
    $info_hashes = [];

    foreach($exact_topics as $exact_topic) {
        $info_hash = decode_v1_exact_topic($exact_topic);

        if($info_hash === null)
            continue;

        $info_hashes[bin2hex($info_hash)] = $info_hash;
    }

    if(count($info_hashes) === 0)
        throw new InvalidArgumentException("Magnet link does not contain a valid BitTorrent v1 info hash.");

    if(count($info_hashes) > 1)
        throw new InvalidArgumentException("Magnet link contains conflicting BitTorrent v1 info hashes.");

    return array_values($info_hashes)[0];
}

function unique_nonempty_magnet_values($values) {
    $unique_values = [];
    $seen_values = [];

    foreach($values as $value) {
        if($value === "" || isset($seen_values[$value]))
            continue;

        $seen_values[$value] = true;
        $unique_values[] = $value;
    }

    return $unique_values;
}

function parse_magnet_uri($magnet_uri) {
    if(preg_match("/\Amagnet:\?/i", $magnet_uri) !== 1)
        throw new InvalidArgumentException("Invalid magnet-link URI.");

    $query = substr($magnet_uri, strlen("magnet:?"));
    $fragment_position = strpos($query, "#");

    if($fragment_position !== false)
        $query = substr($query, 0, $fragment_position);

    if($query === "")
        throw new InvalidArgumentException("Magnet link has no query parameters.");

    $parameters = parse_magnet_parameters($query);
    $info_hash = select_v1_info_hash($parameters["xt"]);

    return new MagnetUri(
        $magnet_uri,
        $info_hash,
        bin2hex($info_hash),
        $parameters["dn"][0] ?? null,
        unique_nonempty_magnet_values($parameters["tr"]),
        unique_nonempty_magnet_values($parameters["x.pe"])
    );
}

function is_magnet_uri($magnet_uri) {
    try {
        parse_magnet_uri($magnet_uri);
    } catch(InvalidArgumentException) {
        return false;
    }

    return true;
}

function parse_torrent_input($input) {
    if(!is_string($input))
        throw new InvalidArgumentException("Torrent input must be a magnet URI or 40-character hexadecimal info hash.");

    $input = trim($input);

    if(preg_match("/\A[0-9A-Fa-f]{40}\z/", $input) === 1)
        return parse_magnet_uri("magnet:?xt=urn:btih:" . $input);

    return parse_magnet_uri($input);
}

// Basic peer structures.
function normalise_peer_time($time = null) {
    if($time === null)
        return microtime(true);

    if((!is_int($time) && !is_float($time)) || !is_finite(floatval($time)) || $time < 0)
        throw new InvalidArgumentException("Peer timestamp must be a finite non-negative number.");

    return floatval($time);
}

function normalise_peer_host($host) {
    if(!is_string($host))
        throw new InvalidArgumentException("Peer host must be a string.");

    $host = trim($host);

    if($host === "" || preg_match("/[\\x00-\\x20\\x7f]/", $host) === 1)
        throw new InvalidArgumentException("Peer host is empty or contains control characters.");

    if(str_starts_with($host, "[") || str_ends_with($host, "]")) {
        if(!str_starts_with($host, "[") || !str_ends_with($host, "]"))
            throw new InvalidArgumentException("Peer IPv6 host has mismatched brackets.");

        $host = substr($host, 1, -1);
    }

    if(filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false)
        return inet_ntop(inet_pton($host));

    if(filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false)
        return strtolower(inet_ntop(inet_pton($host)));

    if(!is_valid_tracker_host($host))
        throw new InvalidArgumentException("Peer host is not a valid IP address or hostname.");

    return strtolower($host);
}

final class PeerEndpoint {
    public readonly string $host;
    public readonly int $port;
    public readonly bool $is_ipv6;
    public readonly string $key;

    public function __construct($host, $port) {
        if(!is_int($port) || $port < 1 || $port > 65535)
            throw new InvalidArgumentException("Peer port must be between 1 and 65535.");

        $was_bracketed = is_string($host) && str_starts_with($host, "[") && str_ends_with($host, "]");
        $this->host = normalise_peer_host($host);
        $this->port = $port;
        $this->is_ipv6 = str_contains($this->host, ":");

        if($was_bracketed && !$this->is_ipv6)
            throw new InvalidArgumentException("Only IPv6 peer hosts may use brackets.");

        $this->key = $this->is_ipv6
            ? "[{$this->host}]:{$this->port}"
            : "{$this->host}:{$this->port}";
    }

    public static function from_string($endpoint) {
        if(!is_string($endpoint))
            throw new InvalidArgumentException("Peer endpoint must be a string.");

        $endpoint = trim($endpoint);

        if(preg_match("/\A\[([^\[\]]+)\]:([0-9]+)\z/", $endpoint, $matches) === 1) {
            $host = "[{$matches[1]}]";
            $encoded_port = $matches[2];
        } elseif(preg_match("/\A([^:\[\]]+):([0-9]+)\z/", $endpoint, $matches) === 1) {
            $host = $matches[1];
            $encoded_port = $matches[2];
        } else {
            throw new InvalidArgumentException("Invalid peer endpoint.");
        }

        $port = filter_var(
            $encoded_port,
            FILTER_VALIDATE_INT,
            ["options" => ["min_range" => 1, "max_range" => 65535]]
        );

        if($port === false)
            throw new InvalidArgumentException("Peer port must be between 1 and 65535.");

        return new self($host, $port);
    }

    public function to_string() {
        return $this->key;
    }
}

final class Peer {
    public readonly PeerEndpoint $endpoint;
    public array $sources = [];
    public float $first_discovered_at;
    public float $last_discovered_at;
    public ?string $peer_id = null;
    public int $connection_attempts = 0;
    public int $connection_failures = 0;
    public int $consecutive_failures = 0;
    public ?string $last_connection_failure_reason = null;
    public ?string $last_connection_failure_category = null;
    public ?float $last_connection_failure_at = null;
    public array $connection_failure_categories = [];
    public ?float $last_connection_attempt_at = null;
    public ?float $last_successful_connection_at = null;
    public bool $is_connecting = false;
    public bool $is_connected = false;
    public float $cooldown_until = 0.0;
    public bool $is_seed = false;
    public bool $remote_choking = true;
    public string $policy_state = "CANDIDATE";
    public array $extension_ids = [];
    public $metadata_size = null;
    public int $downloaded_payload_bytes = 0;
    public int $useful_downloaded_payload_bytes = 0;
    public int $uploaded_payload_bytes = 0;
    public int $archived_downloaded_payload_bytes = 0;
    public int $archived_useful_downloaded_payload_bytes = 0;
    public int $archived_uploaded_payload_bytes = 0;
    public int $archived_successful_block_requests = 0;
    public int $archived_failed_block_requests = 0;
    public float $archived_request_latency_sum_seconds = 0.0;
    public int $archived_request_latency_count = 0;
    public float $recent_download_rate = 0.0;
    public float $recent_upload_rate = 0.0;
    public float $download_rate_short = 0.0;
    public float $download_rate_long = 0.0;
    public float $useful_download_rate_short = 0.0;
    public float $useful_download_rate_long = 0.0;
    public float $upload_rate_short = 0.0;
    public float $upload_rate_long = 0.0;
    public ?float $request_latency_short = null;
    public ?float $request_latency_long = null;
    public ?float $reliability_short = null;
    public ?float $reliability_long = null;
    public ?float $lifetime_reliability = null;
    public int $successful_block_requests = 0;
    public int $failed_block_requests = 0;
    public ?float $last_piece_received_at = null;
    public ?float $last_choked_at = null;
    public ?float $last_unchoked_at = null;
    public ?float $recent_download_per_upload = null;
    public ?float $lifetime_download_per_upload = null;
    public int $performance_sample_count = 0;
    public ?float $last_performance_sample_at = null;
    public ?float $greedy_upload_price = null;
    public ?float $greedy_marginal_return = null;
    public array $greedy_return_history = [];
    public array $greedy_marginal_return_history = [];
    public int $greedy_marginal_return_sample_count = 0;
    public ?float $last_greedy_marginal_return_sample_at = null;
    public float $greedy_allocator_rate = 0.0;
    public string $greedy_allocator_role = "IDLE";
    public string $greedy_price_state = "UNKNOWN";
    public float $greedy_upload_allocation = 0.0;
    public ?float $greedy_last_good_upload_allocation = null;
    public ?float $greedy_last_good_download_rate = null;
    public ?float $greedy_last_price_change_at = null;
    public array $greedy_price_history = [];
    public int $greedy_price_sample_count = 0;
    public ?float $last_greedy_price_sample_at = null;
    public float $connection_optimizer_priority_until = 0.0;
    public int $connection_optimizer_replacements = 0;
    public ?float $last_connection_optimizer_replacement_at = null;
    public int $fresh_service_connection_count = 0;
    public int $fresh_service_unchoke_count = 0;
    public int $fresh_service_useful_bytes = 0;
    public ?float $last_fresh_service_unchoke_at = null;
    public int $fresh_turnover_count = 0;
    public ?float $last_fresh_turnover_at = null;
    public int $request_pipeline_depth = DOWNLOAD_PIPELINE_COLD_START_REQUESTS;
    public int $request_pipeline_max_depth = DOWNLOAD_PIPELINE_COLD_START_REQUESTS;
    public int $request_pipeline_change_count = 0;
    public float $request_pipeline_estimated_rate = 0.0;
    public ?float $request_pipeline_latency = null;
    public float $request_pipeline_target_bytes = 0.0;
    public float $request_pipeline_target_seconds = 0.0;
    public ?float $last_request_pipeline_update_at = null;

    public function __construct($endpoint, $source, $discovered_at = null) {
        if(!($endpoint instanceof PeerEndpoint))
            throw new InvalidArgumentException("Peer requires a valid endpoint.");

        $discovered_at = normalise_peer_time($discovered_at);
        $this->endpoint = $endpoint;
        $this->first_discovered_at = $discovered_at;
        $this->last_discovered_at = $discovered_at;
        $this->add_source($source, $discovered_at);
    }

    public function add_source($source, $discovered_at = null) {
        if(!is_string($source) || trim($source) === "")
            throw new InvalidArgumentException("Peer discovery source must be a non-empty string.");

        $source = trim($source);
        $discovered_at = normalise_peer_time($discovered_at);

        if(!isset($this->sources[$source]) || $discovered_at < $this->sources[$source])
            $this->sources[$source] = $discovered_at;

        $this->first_discovered_at = min($this->first_discovered_at, $discovered_at);
        $this->last_discovered_at = max($this->last_discovered_at, $discovered_at);
    }

    public function get_sources() {
        return array_keys($this->sources);
    }

    public function has_source($source) {
        return isset($this->sources[$source]);
    }

    public function set_peer_id($peer_id) {
        if($peer_id !== null && (!is_string($peer_id) || strlen($peer_id) !== 20))
            throw new InvalidArgumentException("Peer ID must contain exactly 20 bytes.");

        $this->peer_id = $peer_id;
    }

    public function set_seed($is_seed) {
        if(!is_bool($is_seed))
            throw new InvalidArgumentException("Peer seed state must be boolean.");

        $this->is_seed = $is_seed;
    }

    public function update_extension_ids($extension_ids) {
        validate_peer_extension_ids($extension_ids);
        $updated_extension_ids = $this->extension_ids;

        foreach($extension_ids as $extension_name => $extension_id) {
            if($extension_id === 0) {
                unset($updated_extension_ids[$extension_name]);

                if($extension_name === UT_METADATA_EXTENSION_NAME)
                    $this->metadata_size = null;
            } else {
                $updated_extension_ids[$extension_name] = $extension_id;
            }
        }

        validate_peer_extension_ids($updated_extension_ids);
        $this->extension_ids = $updated_extension_ids;
    }

    public function get_extension_id($extension_name) {
        validate_peer_extension_name($extension_name);

        return $this->extension_ids[$extension_name] ?? null;
    }

    public function clear_extension_ids() {
        $this->extension_ids = [];
        $this->metadata_size = null;
    }

    public function set_metadata_size($metadata_size) {
        validate_ut_metadata_size($metadata_size);
        $this->metadata_size = $metadata_size;
    }

    public function record_connection_attempt($attempted_at = null) {
        $this->connection_attempts++;
        $this->last_connection_attempt_at = normalise_peer_time($attempted_at);
        $this->is_connecting = true;
        $this->is_connected = false;
    }

    public function record_connection_failure(
        $failed_at = null,
        $cooldown_seconds = null,
        $reason = null,
        $was_established = false
    ) {
        if(
            $cooldown_seconds !== null
            && (
                (!is_int($cooldown_seconds) && !is_float($cooldown_seconds))
                || !is_finite(floatval($cooldown_seconds))
                || $cooldown_seconds < 0
            )
        )
            throw new InvalidArgumentException("Peer cooldown must be null or a non-negative number of seconds.");

        if($reason !== null && (!is_string($reason) || trim($reason) === ""))
            throw new InvalidArgumentException("Peer connection failure reason must be null or a non-empty string.");

        if(!is_bool($was_established))
            throw new InvalidArgumentException("Peer established failure state must be boolean.");

        $failed_at = normalise_peer_time($failed_at);
        $this->connection_failures++;
        $this->consecutive_failures++;

        if($cooldown_seconds === null)
            $cooldown_seconds = runtime_peer_connection_retry_seconds($this, $this->consecutive_failures);

        $this->is_connecting = false;
        $this->is_connected = false;
        $this->remote_choking = true;
        $this->last_connection_failure_at = $failed_at;

        if($reason !== null) {
            $reason = trim($reason);
            $category = classify_runtime_peer_connection_failure($reason, $was_established);
            $this->last_connection_failure_reason = $reason;
            $this->last_connection_failure_category = $category;
            $this->connection_failure_categories[$category] = ($this->connection_failure_categories[$category] ?? 0) + 1;
        }

        $this->cooldown_until = max($this->cooldown_until, $failed_at + floatval($cooldown_seconds));
    }

    public function record_connection_success($connected_at = null) {
        $connected_at = normalise_peer_time($connected_at);
        $this->is_connecting = false;
        $this->is_connected = true;
        $this->last_successful_connection_at = $connected_at;
        $this->consecutive_failures = 0;
        $this->remote_choking = true;
        $this->cooldown_until = 0.0;
    }

    public function mark_disconnected() {
        $this->is_connecting = false;
        $this->is_connected = false;
        $this->remote_choking = true;
    }

    public function defer_connection_until($cooldown_until) {
        if(
            (!is_int($cooldown_until) && !is_float($cooldown_until))
            || !is_finite(floatval($cooldown_until))
            || $cooldown_until < 0
        )
            throw new InvalidArgumentException("Peer cooldown deadline must be a finite non-negative timestamp.");

        $this->cooldown_until = max($this->cooldown_until, floatval($cooldown_until));
    }

    public function is_in_cooldown($now = null) {
        return normalise_peer_time($now) < $this->cooldown_until;
    }

    public function is_available($now = null) {
        return !$this->is_connecting && !$this->is_connected && !$this->is_in_cooldown($now);
    }
}

function runtime_peer_connection_retry_seconds($peer, $consecutive_failures = null) {
    if(!($peer instanceof Peer))
        throw new InvalidArgumentException("Peer retry calculation requires a peer.");

    if($consecutive_failures === null)
        $consecutive_failures = max(1, $peer->consecutive_failures);

    if(!is_int($consecutive_failures) || $consecutive_failures < 1)
        throw new InvalidArgumentException("Peer retry calculation requires a positive failure count.");

    $exponent = min(10, $consecutive_failures - 1);
    $base_delay = min(
        floatval(PEER_CONNECT_RETRY_MAX_SECONDS),
        floatval(PEER_CONNECT_RETRY_BASE_SECONDS) * (2 ** $exponent)
    );
    $hash_prefix = substr(sha1($peer->endpoint->key), 0, 8);
    $unit = hexdec($hash_prefix) / 4294967295.0;
    $jitter = 1.0 + (((2.0 * $unit) - 1.0) * PEER_CONNECT_RETRY_JITTER_RATIO);

    return min(
        floatval(PEER_CONNECT_RETRY_MAX_SECONDS),
        max(0.1, $base_delay * $jitter)
    );
}

function classify_runtime_peer_connection_failure($reason, $was_established = false) {
    if(!is_string($reason) || trim($reason) === "")
        throw new InvalidArgumentException("Connection failure classification requires a reason.");

    if(!is_bool($was_established))
        throw new InvalidArgumentException("Connection failure classification requires a boolean established state.");

    $reason = strtolower($reason);

    if(str_contains($reason, "timed out"))
        return "TIMEOUT";

    if(str_contains($reason, "invalid bittorrent handshake") || str_contains($reason, "local peer id"))
        return "HANDSHAKE_INVALID";

    if(str_contains($reason, "invalid bittorrent peer message"))
        return "PROTOCOL_INVALID";

    if(str_contains($reason, "closed the tcp connection"))
        return $was_established ? "REMOTE_CLOSE_ESTABLISHED" : "REMOTE_CLOSE_HANDSHAKE";

    if(str_contains($reason, "write failed") || str_contains($reason, "read failed"))
        return "SOCKET_IO";

    if(
        str_contains($reason, "could not be started")
        || str_contains($reason, "connector returned")
        || str_contains($reason, "connection check failed")
        || str_contains($reason, "connection attempt failed")
    )
        return "TCP_CONNECT";

    return $was_established ? "ESTABLISHED_OTHER" : "CONNECT_OTHER";
}

// Peer pool.
final class PeerPool {
    private array $peers = [];

    private function resolve_endpoint($endpoint) {
        if(is_string($endpoint))
            return PeerEndpoint::from_string($endpoint);

        if($endpoint instanceof PeerEndpoint)
            return $endpoint;

        throw new InvalidArgumentException("Peer endpoint must be a string or PeerEndpoint.");
    }

    public function add_peer($endpoint, $source, $discovered_at = null) {
        $endpoint = $this->resolve_endpoint($endpoint);

        if(isset($this->peers[$endpoint->key])) {
            $this->peers[$endpoint->key]->add_source($source, $discovered_at);

            return $this->peers[$endpoint->key];
        }

        $peer = new Peer($endpoint, $source, $discovered_at);
        $this->peers[$endpoint->key] = $peer;

        return $peer;
    }

    public function find_peer($endpoint) {
        $endpoint = $this->resolve_endpoint($endpoint);

        return $this->peers[$endpoint->key] ?? null;
    }

    public function has_peer($endpoint) {
        return $this->find_peer($endpoint) !== null;
    }

    public function get_count() {
        return count($this->peers);
    }

    public function get_peers() {
        return array_values($this->peers);
    }

    public function get_connected_peers() {
        return array_values(array_filter(
            $this->peers,
            static function($peer) {
                return $peer->is_connected;
            }
        ));
    }

    public function get_connecting_peers() {
        return array_values(array_filter(
            $this->peers,
            static function($peer) {
                return $peer->is_connecting;
            }
        ));
    }

    public function get_available_peers($now = null) {
        $now = normalise_peer_time($now);

        return array_values(array_filter(
            $this->peers,
            static function($peer) use ($now) {
                return $peer->is_available($now);
            }
        ));
    }

    public function get_cooldown_peers($now = null) {
        $now = normalise_peer_time($now);

        return array_values(array_filter(
            $this->peers,
            static function($peer) use ($now) {
                return $peer->is_in_cooldown($now);
            }
        ));
    }

    public function get_peers_by_source($source) {
        return array_values(array_filter(
            $this->peers,
            static function($peer) use ($source) {
                return $peer->has_source($source);
            }
        ));
    }
}

// Public tracker-list retrieval.
function split_tracker_list_lines($contents) {
    if($contents === "")
        return [];

    $lines = preg_split("/\r\n|\n|\r/", $contents);

    if(preg_match("/(?:\r\n|\n|\r)\z/", $contents) === 1)
        array_pop($lines);

    return $lines;
}

function is_valid_tracker_host($host) {
    $host = trim($host, "[]");

    if($host === "")
        return false;

    if(filter_var($host, FILTER_VALIDATE_IP) !== false)
        return true;

    if(strlen($host) > 253)
        return false;

    return preg_match(
        "/\A(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)*[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\z/i",
        $host
    ) === 1;
}

function is_supported_tracker_url($tracker) {
    if($tracker === "" || preg_match("/[\\x00-\\x20\\x7f]/", $tracker) === 1)
        return false;

    $parts = @parse_url($tracker);

    if(!is_array($parts) || !isset($parts["scheme"], $parts["host"]))
        return false;

    $scheme = strtolower($parts["scheme"]);

    if(!in_array($scheme, ["udp", "http", "https"], true))
        return false;

    if(!is_valid_tracker_host($parts["host"]))
        return false;

    if(isset($parts["port"]) && ($parts["port"] < 1 || $parts["port"] > 65535))
        return false;

    if($scheme === "udp" && !isset($parts["port"]))
        return false;

    if(isset($parts["fragment"]))
        return false;

    return true;
}

function tracker_deduplication_key($tracker) {
    if(!is_string($tracker) || !is_supported_tracker_url($tracker))
        return null;

    $parts = parse_url($tracker);
    $scheme = strtolower($parts["scheme"]);
    $host = strtolower(trim($parts["host"], "[]"));

    if(filter_var($host, FILTER_VALIDATE_IP) !== false)
        $host = strtolower(inet_ntop(inet_pton($host)));

    $effective_port = $parts["port"] ?? match($scheme) {
        "http" => 80,
        "https" => 443,
        default => null,
    };

    if($scheme === "udp") {
        // BEP 15 uses only the UDP host and port; URL path/query text has no wire meaning.
        return "udp://{$host}:{$effective_port}";
    }

    $path = $parts["path"] ?? "/";

    if($path === "")
        $path = "/";

    $query = isset($parts["query"]) ? "?" . $parts["query"] : "";
    $user_info = "";

    if(isset($parts["user"])) {
        $user_info = $parts["user"];

        if(isset($parts["pass"]))
            $user_info .= ":" . $parts["pass"];

        $user_info .= "@";
    }

    return "{$scheme}://{$user_info}{$host}:{$effective_port}{$path}{$query}";
}

function parse_public_tracker_list($contents) {
    $lines = split_tracker_list_lines($contents);
    $trackers = [];
    $seen_trackers = [];
    $rejected_trackers = 0;
    $deduplicated_trackers = 0;

    foreach($lines as $line) {
        $tracker = trim($line);

        if($tracker === "")
            continue;

        if(!is_supported_tracker_url($tracker)) {
            $rejected_trackers++;

            continue;
        }

        $tracker_key = tracker_deduplication_key($tracker);

        if($tracker_key === null) {
            $rejected_trackers++;

            continue;
        }

        if(isset($seen_trackers[$tracker_key])) {
            $deduplicated_trackers++;

            continue;
        }

        $seen_trackers[$tracker_key] = true;
        $trackers[] = $tracker;
    }

    return [
        "trackers" => $trackers,
        "lines_received" => count($lines),
        "valid_trackers" => count($trackers),
        "rejected_trackers" => $rejected_trackers,
        "deduplicated_trackers" => $deduplicated_trackers,
    ];
}

function download_public_tracker_list($url) {
    $context = stream_context_create([
        "http" => [
            "follow_location" => 1,
            "header" => "User-Agent: GreedyBitTorrentClient/1.0\r\n",
            "max_redirects" => 3,
            "timeout" => PUBLIC_TRACKER_LIST_TIMEOUT,
        ],
        "ssl" => [
            "verify_peer" => true,
            "verify_peer_name" => true,
        ],
    ]);

    return @file_get_contents($url, false, $context);
}

function refresh_public_tracker_list($fetcher, $log_stream) {
    try {
        $contents = $fetcher(PUBLIC_TRACKER_LIST_URL);
    } catch(Throwable) {
        log_message("Public tracker list download failed; continuing without public trackers.", $log_stream);

        return [];
    }

    if(!is_string($contents)) {
        log_message("Public tracker list download failed; continuing without public trackers.", $log_stream);

        return [];
    }

    $result = parse_public_tracker_list($contents);
    log_message(
        sprintf(
            "Public tracker list: %d lines received, %d valid, %d rejected, %d deduplicated.",
            $result["lines_received"],
            $result["valid_trackers"],
            $result["rejected_trackers"],
            $result["deduplicated_trackers"]
        ),
        $log_stream
    );

    return $result["trackers"];
}

// HTTP and HTTPS tracker discovery.
function percent_encode_bytes($bytes) {
    if(!is_string($bytes))
        throw new InvalidArgumentException("Binary tracker parameter must be a string.");

    $encoded = "";

    for($index = 0; $index < strlen($bytes); $index++)
        $encoded .= sprintf("%%%02X", ord($bytes[$index]));

    return $encoded;
}

function validate_tracker_counter($name, $value) {
    if(!is_int($value) || $value < 0)
        throw new InvalidArgumentException("Tracker {$name} must be a non-negative integer.");
}

function build_http_tracker_announce_url(
    $tracker_url,
    $info_hash,
    $peer_id,
    $port,
    $uploaded,
    $downloaded,
    $left,
    $event = "started",
    $numwant = DESIRED_KNOWN_PEERS
) {
    if(!is_string($tracker_url) || !is_supported_tracker_url($tracker_url))
        throw new InvalidArgumentException("HTTP tracker URL is invalid.");

    $parts = parse_url($tracker_url);
    $scheme = strtolower($parts["scheme"]);

    if($scheme !== "http" && $scheme !== "https")
        throw new InvalidArgumentException("Tracker URL must use HTTP or HTTPS.");

    if(!is_string($info_hash) || strlen($info_hash) !== 20)
        throw new InvalidArgumentException("Tracker info hash must contain exactly 20 bytes.");

    if(!is_string($peer_id) || strlen($peer_id) !== 20)
        throw new InvalidArgumentException("Tracker peer ID must contain exactly 20 bytes.");

    if(!is_int($port) || $port < 1 || $port > 65535)
        throw new InvalidArgumentException("Tracker port must be between 1 and 65535.");

    validate_tracker_counter("uploaded byte count", $uploaded);
    validate_tracker_counter("downloaded byte count", $downloaded);
    validate_tracker_counter("remaining byte count", $left);
    validate_tracker_counter("requested peer count", $numwant);

    if(!is_string($event) || !in_array($event, ["", "started", "completed", "stopped"], true))
        throw new InvalidArgumentException("Tracker event is invalid.");

    $parameters = [
        "info_hash=" . percent_encode_bytes($info_hash),
        "peer_id=" . percent_encode_bytes($peer_id),
        "port={$port}",
        "uploaded={$uploaded}",
        "downloaded={$downloaded}",
        "left={$left}",
        "compact=1",
        "no_peer_id=1",
        "numwant={$numwant}",
    ];

    if($event !== "")
        $parameters[] = "event={$event}";

    if(str_contains($tracker_url, "?"))
        $separator = str_ends_with($tracker_url, "?") || str_ends_with($tracker_url, "&") ? "" : "&";
    else
        $separator = "?";

    return $tracker_url . $separator . implode("&", $parameters);
}

function deduplicate_peer_endpoints($endpoints) {
    if(!is_array($endpoints))
        throw new InvalidArgumentException("Peer endpoints must be an array.");

    $unique_endpoints = [];
    $seen_endpoints = [];

    foreach($endpoints as $endpoint) {
        if(!($endpoint instanceof PeerEndpoint))
            throw new InvalidArgumentException("Tracker peer endpoint is invalid.");

        if(isset($seen_endpoints[$endpoint->key]))
            continue;

        $seen_endpoints[$endpoint->key] = true;
        $unique_endpoints[] = $endpoint;
    }

    return $unique_endpoints;
}

function parse_compact_peer_endpoints($compact_peers, $address_family = 4) {
    if(!is_string($compact_peers))
        throw new InvalidArgumentException("Compact tracker peers must be a byte string.");

    if($address_family === 4)
        $record_length = 6;
    elseif($address_family === 6)
        $record_length = 18;
    else
        throw new InvalidArgumentException("Compact tracker address family must be IPv4 or IPv6.");

    if(strlen($compact_peers) % $record_length !== 0)
        throw new InvalidArgumentException("Compact tracker peer data has an incomplete record.");

    $address_length = $record_length - 2;
    $endpoints = [];

    for($offset = 0; $offset < strlen($compact_peers); $offset += $record_length) {
        $host = inet_ntop(substr($compact_peers, $offset, $address_length));
        $port_data = unpack("nport", substr($compact_peers, $offset + $address_length, 2));

        if($host === false || !is_array($port_data) || $port_data["port"] === 0)
            throw new InvalidArgumentException("Compact tracker peer record is invalid.");

        $endpoints[] = new PeerEndpoint($host, $port_data["port"]);
    }

    return deduplicate_peer_endpoints($endpoints);
}

function parse_non_compact_peer_endpoints($peers) {
    if(!is_array($peers) || !array_is_list($peers))
        throw new InvalidArgumentException("Non-compact tracker peers must be a list.");

    $endpoints = [];

    foreach($peers as $peer) {
        if(
            !is_array($peer)
            || array_is_list($peer)
            || !isset($peer["ip"], $peer["port"])
            || !is_string($peer["ip"])
            || !is_int($peer["port"])
        )
            throw new InvalidArgumentException("Non-compact tracker peer record is invalid.");

        $endpoints[] = new PeerEndpoint($peer["ip"], $peer["port"]);
    }

    return deduplicate_peer_endpoints($endpoints);
}

function read_optional_tracker_integer($response, $key, $minimum) {
    if(!array_key_exists($key, $response))
        return null;

    if(!is_int($response[$key]) || $response[$key] < $minimum)
        throw new RuntimeException("HTTP tracker response contains an invalid {$key} value.");

    return $response[$key];
}

function read_optional_tracker_string($response, $key) {
    if(!array_key_exists($key, $response))
        return null;

    if(!is_string($response[$key]))
        throw new RuntimeException("HTTP tracker response contains an invalid {$key} value.");

    return $response[$key];
}

function parse_http_tracker_response($body) {
    if(!is_string($body))
        throw new InvalidArgumentException("HTTP tracker response body must be a string.");

    try {
        $response = bencode_decode($body);
    } catch(InvalidArgumentException $exception) {
        throw new RuntimeException("HTTP tracker response is not valid bencode.", 0, $exception);
    }

    if(!is_array($response) || array_is_list($response))
        throw new RuntimeException("HTTP tracker response must be a dictionary.");

    if(array_key_exists("failure reason", $response)) {
        if(!is_string($response["failure reason"]))
            throw new RuntimeException("HTTP tracker response contains an invalid failure reason.");

        throw new RuntimeException("HTTP tracker failure: " . $response["failure reason"]);
    }

    if(!isset($response["interval"]) || !is_int($response["interval"]) || $response["interval"] < 1)
        throw new RuntimeException("HTTP tracker response contains an invalid interval.");

    $endpoints = [];

    if(array_key_exists("peers", $response)) {
        try {
            if(is_string($response["peers"]))
                $endpoints = parse_compact_peer_endpoints($response["peers"], 4);
            elseif(is_array($response["peers"]))
                $endpoints = parse_non_compact_peer_endpoints($response["peers"]);
            else
                throw new InvalidArgumentException("Tracker peers have an invalid representation.");
        } catch(InvalidArgumentException $exception) {
            throw new RuntimeException("HTTP tracker response contains invalid peers.", 0, $exception);
        }
    }

    if(array_key_exists("peers6", $response)) {
        try {
            $endpoints = array_merge(
                $endpoints,
                parse_compact_peer_endpoints($response["peers6"], 6)
            );
        } catch(InvalidArgumentException $exception) {
            throw new RuntimeException("HTTP tracker response contains invalid IPv6 peers.", 0, $exception);
        }
    }

    return [
        "interval" => $response["interval"],
        "min_interval" => read_optional_tracker_integer($response, "min interval", 1),
        "tracker_id" => read_optional_tracker_string($response, "tracker id"),
        "warning_message" => read_optional_tracker_string($response, "warning message"),
        "complete" => read_optional_tracker_integer($response, "complete", 0),
        "incomplete" => read_optional_tracker_integer($response, "incomplete", 0),
        "peers" => deduplicate_peer_endpoints($endpoints),
    ];
}

function download_http_tracker_announce($announce_url) {
    $context = stream_context_create([
        "http" => [
            "follow_location" => 1,
            "header" => "User-Agent: GreedyBitTorrentClient/1.0\r\n",
            "max_redirects" => 3,
            "timeout" => HTTP_TRACKER_TIMEOUT,
        ],
        "ssl" => [
            "verify_peer" => true,
            "verify_peer_name" => true,
        ],
    ]);

    return @file_get_contents($announce_url, false, $context);
}

function announce_http_tracker(
    $tracker_url,
    $info_hash,
    $peer_id,
    $port,
    $uploaded,
    $downloaded,
    $left,
    $peer_pool,
    $event = "started",
    $numwant = DESIRED_KNOWN_PEERS,
    $fetcher = null,
    $announced_at = null
) {
    if(!($peer_pool instanceof PeerPool))
        throw new InvalidArgumentException("HTTP tracker announce requires a peer pool.");

    $announced_at = normalise_peer_time($announced_at);
    $announce_url = build_http_tracker_announce_url(
        $tracker_url,
        $info_hash,
        $peer_id,
        $port,
        $uploaded,
        $downloaded,
        $left,
        $event,
        $numwant
    );

    if($fetcher === null)
        $fetcher = "download_http_tracker_announce";

    try {
        $body = $fetcher($announce_url);
    } catch(Throwable $exception) {
        throw new RuntimeException("HTTP tracker announce failed.", 0, $exception);
    }

    if(!is_string($body))
        throw new RuntimeException("HTTP tracker announce failed.");

    $result = parse_http_tracker_response($body);
    $source = "tracker:{$tracker_url}";
    $previous_peer_count = $peer_pool->get_count();

    foreach($result["peers"] as $endpoint)
        $peer_pool->add_peer($endpoint, $source, $announced_at);

    $result["announce_url"] = $announce_url;
    $result["new_peers"] = $peer_pool->get_count() - $previous_peer_count;
    $result["known_peers"] = $peer_pool->get_count();

    return $result;
}

// UDP tracker discovery.
function validate_udp_tracker_uint32($value, $name) {
    if(!is_int($value) || $value < 0 || $value > 4294967295)
        throw new InvalidArgumentException("UDP tracker {$name} must be an unsigned 32-bit integer.");
}

function pack_udp_tracker_uint32($value, $name) {
    validate_udp_tracker_uint32($value, $name);

    return pack("N", $value);
}

function unpack_udp_tracker_uint32($data, $offset, $name) {
    if(!is_string($data) || !is_int($offset) || $offset < 0 || strlen($data) - $offset < 4)
        throw new InvalidArgumentException("UDP tracker {$name} is missing or incomplete.");

    $value = unpack("Nvalue", substr($data, $offset, 4));

    if(!is_array($value))
        throw new InvalidArgumentException("UDP tracker {$name} could not be decoded.");

    return $value["value"];
}

function pack_udp_tracker_uint64($value, $name) {
    validate_tracker_counter($name, $value);

    return pack("N2", intdiv($value, 4294967296), $value % 4294967296);
}

function generate_udp_tracker_transaction_id() {
    return random_int(0, 4294967295);
}

function generate_udp_tracker_key() {
    return random_int(0, 4294967295);
}

function build_udp_tracker_connect_request($transaction_id) {
    return UDP_TRACKER_PROTOCOL_ID
        . pack_udp_tracker_uint32(0, "connect action")
        . pack_udp_tracker_uint32($transaction_id, "transaction ID");
}

function parse_udp_tracker_response_header($response, $expected_transaction_id) {
    validate_udp_tracker_uint32($expected_transaction_id, "expected transaction ID");

    if(!is_string($response) || strlen($response) < 8)
        throw new RuntimeException("UDP tracker response is shorter than its header.");

    $action = unpack_udp_tracker_uint32($response, 0, "response action");
    $transaction_id = unpack_udp_tracker_uint32($response, 4, "response transaction ID");

    if($transaction_id !== $expected_transaction_id)
        throw new RuntimeException("UDP tracker response transaction ID does not match the request.");

    if($action === 3) {
        $message = substr($response, 8);

        if($message === "")
            $message = "unspecified tracker error";

        throw new RuntimeException("UDP tracker failure: {$message}");
    }

    return [
        "action" => $action,
        "transaction_id" => $transaction_id,
    ];
}

function parse_udp_tracker_connect_response($response, $expected_transaction_id) {
    $header = parse_udp_tracker_response_header($response, $expected_transaction_id);

    if($header["action"] !== 0)
        throw new RuntimeException("UDP tracker connect response has an unexpected action.");

    if(strlen($response) < 16)
        throw new RuntimeException("UDP tracker connect response has an invalid length.");

    return [
        "transaction_id" => $header["transaction_id"],
        "connection_id" => substr($response, 8, 8),
    ];
}

function udp_tracker_event_code($event) {
    $event_codes = [
        "" => 0,
        "completed" => 1,
        "started" => 2,
        "stopped" => 3,
    ];

    if(!is_string($event) || !array_key_exists($event, $event_codes))
        throw new InvalidArgumentException("UDP tracker event is invalid.");

    return $event_codes[$event];
}

function validate_udp_tracker_announce_parameters(
    $connection_id,
    $info_hash,
    $peer_id,
    $port,
    $uploaded,
    $downloaded,
    $left,
    $event,
    $numwant,
    $transaction_id,
    $key
) {
    if(!is_string($connection_id) || strlen($connection_id) !== 8)
        throw new InvalidArgumentException("UDP tracker connection ID must contain exactly 8 bytes.");

    if(!is_string($info_hash) || strlen($info_hash) !== 20)
        throw new InvalidArgumentException("UDP tracker info hash must contain exactly 20 bytes.");

    if(!is_string($peer_id) || strlen($peer_id) !== 20)
        throw new InvalidArgumentException("UDP tracker peer ID must contain exactly 20 bytes.");

    if(!is_int($port) || $port < 1 || $port > 65535)
        throw new InvalidArgumentException("UDP tracker port must be between 1 and 65535.");

    validate_tracker_counter("uploaded byte count", $uploaded);
    validate_tracker_counter("downloaded byte count", $downloaded);
    validate_tracker_counter("remaining byte count", $left);
    udp_tracker_event_code($event);

    if(!is_int($numwant) || $numwant < -1 || $numwant > 2147483647)
        throw new InvalidArgumentException("UDP tracker requested peer count must fit a signed 32-bit integer.");

    validate_udp_tracker_uint32($transaction_id, "transaction ID");
    validate_udp_tracker_uint32($key, "key");
}

function build_udp_tracker_announce_request(
    $connection_id,
    $info_hash,
    $peer_id,
    $port,
    $uploaded,
    $downloaded,
    $left,
    $event,
    $numwant,
    $transaction_id,
    $key
) {
    validate_udp_tracker_announce_parameters(
        $connection_id,
        $info_hash,
        $peer_id,
        $port,
        $uploaded,
        $downloaded,
        $left,
        $event,
        $numwant,
        $transaction_id,
        $key
    );

    return $connection_id
        . pack_udp_tracker_uint32(1, "announce action")
        . pack_udp_tracker_uint32($transaction_id, "transaction ID")
        . $info_hash
        . $peer_id
        . pack_udp_tracker_uint64($downloaded, "downloaded byte count")
        . pack_udp_tracker_uint64($left, "remaining byte count")
        . pack_udp_tracker_uint64($uploaded, "uploaded byte count")
        . pack_udp_tracker_uint32(udp_tracker_event_code($event), "event")
        . pack_udp_tracker_uint32(0, "announced IP address")
        . pack_udp_tracker_uint32($key, "key")
        . pack_udp_tracker_uint32($numwant === -1 ? 4294967295 : $numwant, "requested peer count")
        . pack("n", $port);
}

function parse_udp_tracker_announce_response($response, $expected_transaction_id, $address_family = 4) {
    $header = parse_udp_tracker_response_header($response, $expected_transaction_id);

    if($header["action"] !== 1)
        throw new RuntimeException("UDP tracker announce response has an unexpected action.");

    if(strlen($response) < 20)
        throw new RuntimeException("UDP tracker announce response has an invalid length.");

    $interval = unpack_udp_tracker_uint32($response, 8, "announce interval");

    if($interval < 1)
        throw new RuntimeException("UDP tracker announce interval must be positive.");

    try {
        $peers = parse_compact_peer_endpoints(substr($response, 20), $address_family);
    } catch(InvalidArgumentException $exception) {
        throw new RuntimeException("UDP tracker announce response contains invalid peers.", 0, $exception);
    }

    return [
        "transaction_id" => $header["transaction_id"],
        "interval" => $interval,
        "incomplete" => unpack_udp_tracker_uint32($response, 12, "leecher count"),
        "complete" => unpack_udp_tracker_uint32($response, 16, "seeder count"),
        "peers" => $peers,
    ];
}

function parse_udp_tracker_url($tracker_url) {
    if(!is_string($tracker_url) || !is_supported_tracker_url($tracker_url))
        throw new InvalidArgumentException("UDP tracker URL is invalid.");

    $parts = parse_url($tracker_url);

    if(strtolower($parts["scheme"]) !== "udp")
        throw new InvalidArgumentException("Tracker URL must use UDP.");

    $host = trim($parts["host"], "[]");
    $port = $parts["port"];
    $is_ipv6 = filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
    $socket_host = $is_ipv6 ? "[{$host}]" : $host;

    return [
        "host" => $host,
        "port" => $port,
        "socket_address" => "udp://{$socket_host}:{$port}",
        "address_family" => $is_ipv6 ? 6 : 4,
    ];
}

function exchange_udp_tracker_packet($tracker_url, $request, $timeout) {
    $tracker = parse_udp_tracker_url($tracker_url);

    if(!is_string($request) || $request === "")
        throw new InvalidArgumentException("UDP tracker request must be a non-empty byte string.");

    if((!is_int($timeout) && !is_float($timeout)) || !is_finite(floatval($timeout)) || $timeout <= 0)
        throw new InvalidArgumentException("UDP tracker timeout must be a finite positive number.");

    $error_number = 0;
    $error_message = "";
    $socket = @stream_socket_client(
        $tracker["socket_address"],
        $error_number,
        $error_message,
        floatval($timeout),
        STREAM_CLIENT_CONNECT
    );

    if($socket === false)
        throw new RuntimeException("UDP tracker socket could not be opened: {$error_message}");

    try {
        if(!stream_set_blocking($socket, false))
            throw new RuntimeException("UDP tracker socket could not be made non-blocking.");

        $bytes_written = @fwrite($socket, $request);

        if($bytes_written !== strlen($request))
            throw new RuntimeException("UDP tracker request datagram could not be sent.");

        $read_sockets = [$socket];
        $write_sockets = null;
        $except_sockets = null;
        $timeout_seconds = intval(floor($timeout));
        $timeout_microseconds = intval(floor(($timeout - $timeout_seconds) * 1000000));

        if($timeout_seconds === 0 && $timeout_microseconds === 0)
            $timeout_microseconds = 1;

        $selected = @stream_select(
            $read_sockets,
            $write_sockets,
            $except_sockets,
            $timeout_seconds,
            $timeout_microseconds
        );

        if($selected === false)
            throw new RuntimeException("UDP tracker socket wait failed.");

        if($selected === 0)
            return null;

        $response = @fread($socket, 65535);

        if($response === false)
            throw new RuntimeException("UDP tracker response datagram could not be read.");

        return $response;
    } finally {
        fclose($socket);
    }
}

function request_udp_tracker_with_retry(
    $tracker_url,
    $request,
    $parser,
    $exchanger = null,
    $initial_timeout = UDP_TRACKER_INITIAL_TIMEOUT,
    $max_attempts = UDP_TRACKER_MAX_ATTEMPTS,
    $backoff_factor = UDP_TRACKER_BACKOFF_FACTOR
) {
    parse_udp_tracker_url($tracker_url);

    if(!is_string($request) || $request === "")
        throw new InvalidArgumentException("UDP tracker request must be a non-empty byte string.");

    if(!is_callable($parser))
        throw new InvalidArgumentException("UDP tracker response parser must be callable.");

    if($exchanger === null)
        $exchanger = "exchange_udp_tracker_packet";

    if(!is_callable($exchanger))
        throw new InvalidArgumentException("UDP tracker packet exchanger must be callable.");

    if(
        (!is_int($initial_timeout) && !is_float($initial_timeout))
        || !is_finite(floatval($initial_timeout))
        || $initial_timeout <= 0
    )
        throw new InvalidArgumentException("UDP tracker initial timeout must be a finite positive number.");

    if(!is_int($max_attempts) || $max_attempts < 1)
        throw new InvalidArgumentException("UDP tracker maximum attempts must be a positive integer.");

    if(
        (!is_int($backoff_factor) && !is_float($backoff_factor))
        || !is_finite(floatval($backoff_factor))
        || $backoff_factor <= 1
    )
        throw new InvalidArgumentException("UDP tracker backoff factor must be greater than one.");

    $timeout = floatval($initial_timeout);

    for($attempt = 1; $attempt <= $max_attempts; $attempt++) {
        try {
            $response = $exchanger($tracker_url, $request, $timeout);
        } catch(Throwable $exception) {
            throw new RuntimeException("UDP tracker packet exchange failed.", 0, $exception);
        }

        if($response !== null) {
            if(!is_string($response))
                throw new RuntimeException("UDP tracker packet exchanger returned an invalid response.");

            return [
                "result" => $parser($response),
                "attempts" => $attempt,
            ];
        }

        $timeout *= $backoff_factor;
    }

    throw new RuntimeException("UDP tracker request timed out after {$max_attempts} attempts.");
}

function announce_udp_tracker(
    $tracker_url,
    $info_hash,
    $peer_id,
    $port,
    $uploaded,
    $downloaded,
    $left,
    $peer_pool,
    $event = "started",
    $numwant = DESIRED_KNOWN_PEERS,
    $options = []
) {
    if(!($peer_pool instanceof PeerPool))
        throw new InvalidArgumentException("UDP tracker announce requires a peer pool.");

    if(!is_array($options))
        throw new InvalidArgumentException("UDP tracker options must be an array.");

    $tracker = parse_udp_tracker_url($tracker_url);
    $connect_transaction_id = $options["connect_transaction_id"] ?? generate_udp_tracker_transaction_id();
    $announce_transaction_id = $options["announce_transaction_id"] ?? generate_udp_tracker_transaction_id();
    $key = $options["key"] ?? generate_udp_tracker_key();
    $announced_at = normalise_peer_time($options["announced_at"] ?? null);
    $exchanger = $options["exchanger"] ?? null;
    $initial_timeout = $options["initial_timeout"] ?? UDP_TRACKER_INITIAL_TIMEOUT;
    $max_attempts = $options["max_attempts"] ?? UDP_TRACKER_MAX_ATTEMPTS;
    $backoff_factor = $options["backoff_factor"] ?? UDP_TRACKER_BACKOFF_FACTOR;

    validate_udp_tracker_announce_parameters(
        str_repeat("\x00", 8),
        $info_hash,
        $peer_id,
        $port,
        $uploaded,
        $downloaded,
        $left,
        $event,
        $numwant,
        $announce_transaction_id,
        $key
    );
    validate_udp_tracker_uint32($connect_transaction_id, "connect transaction ID");

    $connect_exchange = request_udp_tracker_with_retry(
        $tracker_url,
        build_udp_tracker_connect_request($connect_transaction_id),
        static function($response) use ($connect_transaction_id) {
            return parse_udp_tracker_connect_response($response, $connect_transaction_id);
        },
        $exchanger,
        $initial_timeout,
        $max_attempts,
        $backoff_factor
    );
    $connection_id = $connect_exchange["result"]["connection_id"];
    $announce_exchange = request_udp_tracker_with_retry(
        $tracker_url,
        build_udp_tracker_announce_request(
            $connection_id,
            $info_hash,
            $peer_id,
            $port,
            $uploaded,
            $downloaded,
            $left,
            $event,
            $numwant,
            $announce_transaction_id,
            $key
        ),
        static function($response) use ($announce_transaction_id, $tracker) {
            return parse_udp_tracker_announce_response(
                $response,
                $announce_transaction_id,
                $tracker["address_family"]
            );
        },
        $exchanger,
        $initial_timeout,
        $max_attempts,
        $backoff_factor
    );
    $result = $announce_exchange["result"];
    $source = "tracker:{$tracker_url}";
    $previous_peer_count = $peer_pool->get_count();

    foreach($result["peers"] as $endpoint)
        $peer_pool->add_peer($endpoint, $source, $announced_at);

    $result["connection_id"] = $connection_id;
    $result["connect_transaction_id"] = $connect_transaction_id;
    $result["announce_transaction_id"] = $announce_transaction_id;
    $result["key"] = $key;
    $result["connect_attempts"] = $connect_exchange["attempts"];
    $result["announce_attempts"] = $announce_exchange["attempts"];
    $result["new_peers"] = $peer_pool->get_count() - $previous_peer_count;
    $result["known_peers"] = $peer_pool->get_count();

    return $result;
}

// BitTorrent peer handshake.
function validate_peer_handshake_fields($info_hash, $peer_id, $reserved_bytes) {
    if(!is_string($info_hash) || strlen($info_hash) !== 20)
        throw new InvalidArgumentException("Peer handshake info hash must contain exactly 20 bytes.");

    if(!is_string($peer_id) || strlen($peer_id) !== 20)
        throw new InvalidArgumentException("Peer handshake peer ID must contain exactly 20 bytes.");

    if(!is_string($reserved_bytes) || strlen($reserved_bytes) !== 8)
        throw new InvalidArgumentException("Peer handshake reserved field must contain exactly 8 bytes.");
}

function build_peer_handshake($info_hash, $peer_id, $reserved_bytes = null) {
    if($reserved_bytes === null)
        $reserved_bytes = str_repeat("\x00", 8);

    validate_peer_handshake_fields($info_hash, $peer_id, $reserved_bytes);

    return chr(strlen(BITTORRENT_PROTOCOL_NAME))
        . BITTORRENT_PROTOCOL_NAME
        . $reserved_bytes
        . $info_hash
        . $peer_id;
}

function parse_peer_handshake($handshake, $expected_info_hash = null, $expected_peer_id = null) {
    if(!is_string($handshake))
        throw new InvalidArgumentException("Peer handshake must be a byte string.");

    if($expected_info_hash !== null && (!is_string($expected_info_hash) || strlen($expected_info_hash) !== 20))
        throw new InvalidArgumentException("Expected peer handshake info hash must contain exactly 20 bytes.");

    if($expected_peer_id !== null && (!is_string($expected_peer_id) || strlen($expected_peer_id) !== 20))
        throw new InvalidArgumentException("Expected peer handshake peer ID must contain exactly 20 bytes.");

    if(strlen($handshake) !== BITTORRENT_HANDSHAKE_LENGTH)
        throw new RuntimeException("Peer handshake has an invalid length.");

    if(ord($handshake[0]) !== strlen(BITTORRENT_PROTOCOL_NAME))
        throw new RuntimeException("Peer handshake has an invalid protocol-name length.");

    $protocol_name = substr($handshake, 1, strlen(BITTORRENT_PROTOCOL_NAME));

    if($protocol_name !== BITTORRENT_PROTOCOL_NAME)
        throw new RuntimeException("Peer handshake has an unexpected protocol name.");

    $reserved_bytes = substr($handshake, 20, 8);
    $info_hash = substr($handshake, 28, 20);
    $peer_id = substr($handshake, 48, 20);

    if($expected_info_hash !== null && !hash_equals($expected_info_hash, $info_hash))
        throw new RuntimeException("Peer handshake info hash does not match the torrent.");

    if($expected_peer_id !== null && !hash_equals($expected_peer_id, $peer_id))
        throw new RuntimeException("Peer handshake peer ID does not match the expected peer.");

    return [
        "protocol_name" => $protocol_name,
        "reserved_bytes" => $reserved_bytes,
        "info_hash" => $info_hash,
        "peer_id" => $peer_id,
    ];
}

function consume_peer_handshake(&$buffer, $expected_info_hash = null, $expected_peer_id = null) {
    if(!is_string($buffer))
        throw new InvalidArgumentException("Peer input buffer must be a byte string.");

    if($buffer !== "" && ord($buffer[0]) !== strlen(BITTORRENT_PROTOCOL_NAME))
        throw new RuntimeException("Peer handshake has an invalid protocol-name length.");

    $available_protocol_length = min(
        max(strlen($buffer) - 1, 0),
        strlen(BITTORRENT_PROTOCOL_NAME)
    );

    if(
        $available_protocol_length > 0
        && substr($buffer, 1, $available_protocol_length)
            !== substr(BITTORRENT_PROTOCOL_NAME, 0, $available_protocol_length)
    )
        throw new RuntimeException("Peer handshake has an unexpected protocol name.");

    if(strlen($buffer) < BITTORRENT_HANDSHAKE_LENGTH)
        return null;

    $handshake = parse_peer_handshake(
        substr($buffer, 0, BITTORRENT_HANDSHAKE_LENGTH),
        $expected_info_hash,
        $expected_peer_id
    );
    $buffer = substr($buffer, BITTORRENT_HANDSHAKE_LENGTH);

    return $handshake;
}

// Peer-wire message encoding.
function validate_peer_message_uint32($value, $name) {
    if(!is_int($value) || $value < 0 || $value > 4294967295)
        throw new InvalidArgumentException("Peer message {$name} must be an unsigned 32-bit integer.");
}

function pack_peer_message_uint32($value, $name) {
    validate_peer_message_uint32($value, $name);

    return pack("N", $value);
}

function build_peer_message_frame($message_id, $payload = "") {
    if(!is_int($message_id) || $message_id < 0 || $message_id > 255)
        throw new InvalidArgumentException("Peer message ID must be an unsigned byte.");

    if(!is_string($payload))
        throw new InvalidArgumentException("Peer message payload must be a byte string.");

    $message_length = strlen($payload) + 1;

    if($message_length > PEER_MESSAGE_MAX_LENGTH)
        throw new InvalidArgumentException("Peer message exceeds the configured maximum length.");

    return pack("N", $message_length) . chr($message_id) . $payload;
}

function encode_peer_keep_alive() {
    return pack("N", 0);
}

function encode_peer_choke() {
    return build_peer_message_frame(PEER_MESSAGE_CHOKE);
}

function encode_peer_unchoke() {
    return build_peer_message_frame(PEER_MESSAGE_UNCHOKE);
}

function encode_peer_interested() {
    return build_peer_message_frame(PEER_MESSAGE_INTERESTED);
}

function encode_peer_not_interested() {
    return build_peer_message_frame(PEER_MESSAGE_NOT_INTERESTED);
}

function encode_peer_have($piece_index) {
    return build_peer_message_frame(
        PEER_MESSAGE_HAVE,
        pack_peer_message_uint32($piece_index, "piece index")
    );
}

function encode_peer_bitfield($bitfield) {
    if(!is_string($bitfield))
        throw new InvalidArgumentException("Peer bitfield must be a byte string.");

    return build_peer_message_frame(PEER_MESSAGE_BITFIELD, $bitfield);
}

function validate_peer_block_request($piece_index, $begin, $length) {
    validate_peer_message_uint32($piece_index, "piece index");
    validate_peer_message_uint32($begin, "block offset");

    if(!is_int($length) || $length < 1 || $length > PEER_BLOCK_MAX_LENGTH)
        throw new InvalidArgumentException("Peer block length must be between 1 and PEER_BLOCK_MAX_LENGTH bytes.");
}

function encode_peer_request($piece_index, $begin, $length) {
    validate_peer_block_request($piece_index, $begin, $length);

    return build_peer_message_frame(
        PEER_MESSAGE_REQUEST,
        pack("N3", $piece_index, $begin, $length)
    );
}

function encode_peer_piece($piece_index, $begin, $block) {
    validate_peer_message_uint32($piece_index, "piece index");
    validate_peer_message_uint32($begin, "block offset");

    if(!is_string($block) || strlen($block) < 1 || strlen($block) > PEER_BLOCK_MAX_LENGTH)
        throw new InvalidArgumentException("Peer piece block must contain between 1 and PEER_BLOCK_MAX_LENGTH bytes.");

    return build_peer_message_frame(
        PEER_MESSAGE_PIECE,
        pack("N2", $piece_index, $begin) . $block
    );
}

function encode_peer_cancel($piece_index, $begin, $length) {
    validate_peer_block_request($piece_index, $begin, $length);

    return build_peer_message_frame(
        PEER_MESSAGE_CANCEL,
        pack("N3", $piece_index, $begin, $length)
    );
}

function encode_peer_port($port) {
    if(!is_int($port) || $port < 1 || $port > 65535)
        throw new InvalidArgumentException("Peer DHT port must be between 1 and 65535.");

    return build_peer_message_frame(PEER_MESSAGE_PORT, pack("n", $port));
}

// BEP 10 extension protocol.
function validate_peer_extension_name($extension_name) {
    if(!is_string($extension_name) || strlen($extension_name) < 3)
        throw new InvalidArgumentException("Peer extension names must contain at least three bytes.");
}

function validate_peer_extension_ids($extension_ids) {
    if(!is_array($extension_ids) || ($extension_ids !== [] && array_is_list($extension_ids)))
        throw new InvalidArgumentException("Peer extension IDs must be a name-to-ID dictionary.");

    $used_extension_ids = [];

    foreach($extension_ids as $extension_name => $extension_id) {
        validate_peer_extension_name($extension_name);

        if(!is_int($extension_id) || $extension_id < 0 || $extension_id > 255)
            throw new InvalidArgumentException("Peer extension IDs must be unsigned bytes.");

        if($extension_id === 0)
            continue;

        if(isset($used_extension_ids[$extension_id]))
            throw new InvalidArgumentException("Peer extensions cannot share a non-zero message ID.");

        $used_extension_ids[$extension_id] = true;
    }
}

function peer_extension_name_for_id($extension_ids, $extension_id) {
    validate_peer_extension_ids($extension_ids);

    if(!is_int($extension_id) || $extension_id < 1 || $extension_id > 255)
        throw new InvalidArgumentException("Peer extension message ID must be between 1 and 255.");

    foreach($extension_ids as $extension_name => $candidate_id) {
        if($candidate_id === $extension_id)
            return $extension_name;
    }

    return null;
}

function build_peer_extension_reserved_bytes($reserved_bytes = null) {
    if($reserved_bytes === null)
        $reserved_bytes = str_repeat("\x00", 8);

    if(!is_string($reserved_bytes) || strlen($reserved_bytes) !== 8)
        throw new InvalidArgumentException("Peer handshake reserved field must contain exactly 8 bytes.");

    $reserved_bytes[PEER_EXTENSION_RESERVED_BYTE_INDEX] = chr(
        ord($reserved_bytes[PEER_EXTENSION_RESERVED_BYTE_INDEX]) | PEER_EXTENSION_RESERVED_MASK
    );

    return $reserved_bytes;
}

function peer_supports_extension_protocol($reserved_bytes) {
    if(!is_string($reserved_bytes) || strlen($reserved_bytes) !== 8)
        throw new InvalidArgumentException("Peer handshake reserved field must contain exactly 8 bytes.");

    return (ord($reserved_bytes[PEER_EXTENSION_RESERVED_BYTE_INDEX]) & PEER_EXTENSION_RESERVED_MASK) !== 0;
}

function build_peer_dht_reserved_bytes($reserved_bytes = null) {
    if($reserved_bytes === null)
        $reserved_bytes = str_repeat("\x00", 8);

    if(!is_string($reserved_bytes) || strlen($reserved_bytes) !== 8)
        throw new InvalidArgumentException("Peer handshake reserved field must contain exactly 8 bytes.");

    $reserved_bytes[PEER_DHT_RESERVED_BYTE_INDEX] = chr(
        ord($reserved_bytes[PEER_DHT_RESERVED_BYTE_INDEX]) | PEER_DHT_RESERVED_MASK
    );

    return $reserved_bytes;
}

function peer_supports_dht($reserved_bytes) {
    if(!is_string($reserved_bytes) || strlen($reserved_bytes) !== 8)
        throw new InvalidArgumentException("Peer handshake reserved field must contain exactly 8 bytes.");

    return (ord($reserved_bytes[PEER_DHT_RESERVED_BYTE_INDEX]) & PEER_DHT_RESERVED_MASK) !== 0;
}

function encode_peer_extended_message($extension_id, $payload) {
    if(!is_int($extension_id) || $extension_id < 0 || $extension_id > 255)
        throw new InvalidArgumentException("Peer extension message ID must be an unsigned byte.");

    if(!is_string($payload))
        throw new InvalidArgumentException("Peer extension payload must be a byte string.");

    return build_peer_message_frame(
        PEER_MESSAGE_EXTENDED,
        chr($extension_id) . $payload
    );
}

function validate_peer_extension_handshake_properties($properties) {
    if(!is_array($properties) || ($properties !== [] && array_is_list($properties)))
        throw new InvalidArgumentException("Peer extension handshake properties must be a dictionary.");

    foreach($properties as $property_name => $property_value) {
        if(!is_string($property_name) || $property_name === "")
            throw new InvalidArgumentException("Peer extension handshake property names must be strings.");
    }

    if(array_key_exists("m", $properties))
        throw new InvalidArgumentException("Peer extension handshake properties cannot replace the m dictionary.");

    if(array_key_exists("metadata_size", $properties))
        validate_ut_metadata_size($properties["metadata_size"]);
}

function encode_peer_extension_handshake($extension_ids = [], $properties = []) {
    validate_peer_extension_ids($extension_ids);
    validate_peer_extension_handshake_properties($properties);

    $handshake = $properties;
    $handshake["m"] = new BencodeDictionary($extension_ids);

    return encode_peer_extended_message(
        PEER_EXTENSION_HANDSHAKE,
        bencode_encode(new BencodeDictionary($handshake))
    );
}

function parse_peer_extension_handshake($payload) {
    if(!is_string($payload))
        throw new InvalidArgumentException("Peer extension handshake must be a byte string.");

    try {
        $handshake = bencode_decode($payload);
    } catch(InvalidArgumentException $exception) {
        throw new RuntimeException("Peer extension handshake is not valid bencode.", 0, $exception);
    }

    if(!is_array($handshake) || ($handshake !== [] && array_is_list($handshake)))
        throw new RuntimeException("Peer extension handshake must be a dictionary.");

    $extension_ids = [];

    if(array_key_exists("m", $handshake)) {
        $extension_ids = $handshake["m"];

        if(!is_array($extension_ids) || ($extension_ids !== [] && array_is_list($extension_ids)))
            throw new RuntimeException("Peer extension handshake m value must be a dictionary.");

        try {
            validate_peer_extension_ids($extension_ids);
        } catch(InvalidArgumentException $exception) {
            throw new RuntimeException("Peer extension handshake contains invalid extension IDs.", 0, $exception);
        }
    }

    if(array_key_exists("metadata_size", $handshake)) {
        try {
            validate_ut_metadata_size($handshake["metadata_size"]);
        } catch(InvalidArgumentException $exception) {
            throw new RuntimeException("Peer extension handshake contains an invalid metadata size.", 0, $exception);
        }
    }

    return [
        "extension_ids" => $extension_ids,
        "handshake" => $handshake,
    ];
}

// BEP 11 peer exchange.
function pex_endpoint_address_family($endpoint) {
    if(!($endpoint instanceof PeerEndpoint))
        throw new InvalidArgumentException("PEX contact requires a peer endpoint.");

    if(filter_var($endpoint->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false)
        return 4;

    if(filter_var($endpoint->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false)
        return 6;

    return null;
}

function encode_compact_pex_endpoint($endpoint) {
    $address_family = pex_endpoint_address_family($endpoint);

    if($address_family === null)
        throw new InvalidArgumentException("PEX compact contacts require an IP address.");

    $packed_host = inet_pton($endpoint->host);

    if($packed_host === false)
        throw new InvalidArgumentException("PEX contact IP address could not be encoded.");

    return $packed_host . pack("n", $endpoint->port);
}

function parse_ut_pex_contact_field($compact_contacts, $flags, $address_family, $field_name) {
    if(!is_string($compact_contacts))
        throw new RuntimeException("PEX {$field_name} field must be a byte string.");

    $record_length = $address_family === 4 ? 6 : 18;

    if(strlen($compact_contacts) % $record_length !== 0)
        throw new RuntimeException("PEX {$field_name} field contains an incomplete compact contact.");

    $contact_count = intdiv(strlen($compact_contacts), $record_length);

    if($flags !== null) {
        if(!is_string($flags) || strlen($flags) !== $contact_count)
            throw new RuntimeException("PEX {$field_name}.f flags must contain one byte per contact.");
    }

    try {
        $endpoints = parse_compact_peer_endpoints($compact_contacts, $address_family);
    } catch(InvalidArgumentException $exception) {
        throw new RuntimeException("PEX {$field_name} field contains an invalid compact contact.", 0, $exception);
    }

    if(count($endpoints) !== $contact_count)
        throw new RuntimeException("PEX {$field_name} field contains duplicate endpoints.");

    $contacts = [];

    foreach($endpoints as $index => $endpoint) {
        $contacts[] = [
            "endpoint" => $endpoint,
            "flags" => $flags === null ? 0 : ord($flags[$index]),
        ];
    }

    return $contacts;
}

function parse_ut_pex_message($payload, $initial_message = false) {
    if(!is_string($payload))
        throw new InvalidArgumentException("ut_pex payload must be a byte string.");

    if(!is_bool($initial_message))
        throw new InvalidArgumentException("ut_pex initial-message state must be boolean.");

    try {
        $pex = bencode_decode($payload);
    } catch(InvalidArgumentException $exception) {
        throw new RuntimeException("ut_pex payload is not valid bencode.", 0, $exception);
    }

    if(!is_array($pex) || ($pex !== [] && array_is_list($pex)))
        throw new RuntimeException("ut_pex payload must be a dictionary.");

    $contact_fields = ["added", "added6", "dropped", "dropped6"];
    $has_contact_field = false;

    foreach($contact_fields as $field_name) {
        if(array_key_exists($field_name, $pex)) {
            $has_contact_field = true;

            if(!is_string($pex[$field_name]))
                throw new RuntimeException("ut_pex {$field_name} field must be a byte string.");
        }
    }

    if(!$has_contact_field)
        throw new RuntimeException("ut_pex message must contain at least one contact field.");

    if(array_key_exists("added.f", $pex) && !array_key_exists("added", $pex))
        throw new RuntimeException("ut_pex added.f cannot appear without added contacts.");

    if(array_key_exists("added6.f", $pex) && !array_key_exists("added6", $pex))
        throw new RuntimeException("ut_pex added6.f cannot appear without added6 contacts.");

    $added = [];
    $dropped = [];

    if(array_key_exists("added", $pex)) {
        $added = array_merge(
            $added,
            parse_ut_pex_contact_field(
                $pex["added"],
                $pex["added.f"] ?? null,
                4,
                "added"
            )
        );
    }

    if(array_key_exists("added6", $pex)) {
        $added = array_merge(
            $added,
            parse_ut_pex_contact_field(
                $pex["added6"],
                $pex["added6.f"] ?? null,
                6,
                "added6"
            )
        );
    }

    if(array_key_exists("dropped", $pex)) {
        foreach(parse_ut_pex_contact_field($pex["dropped"], null, 4, "dropped") as $contact)
            $dropped[] = $contact["endpoint"];
    }

    if(array_key_exists("dropped6", $pex)) {
        foreach(parse_ut_pex_contact_field($pex["dropped6"], null, 6, "dropped6") as $contact)
            $dropped[] = $contact["endpoint"];
    }

    $maximum_added = $initial_message ? PEX_MAX_INITIAL_CONTACTS : PEX_MAX_ADDED_PER_MESSAGE;
    $maximum_dropped = $initial_message ? PEX_MAX_INITIAL_CONTACTS : PEX_MAX_DROPPED_PER_MESSAGE;

    if(count($added) > $maximum_added)
        throw new RuntimeException("ut_pex message contains too many added contacts.");

    if(count($dropped) > $maximum_dropped)
        throw new RuntimeException("ut_pex message contains too many dropped contacts.");

    $added_keys = [];

    foreach($added as $contact)
        $added_keys[$contact["endpoint"]->key] = true;

    foreach($dropped as $endpoint) {
        if(isset($added_keys[$endpoint->key]))
            throw new RuntimeException("ut_pex cannot add and drop the same endpoint in one message.");
    }

    return [
        "added" => $added,
        "dropped" => $dropped,
    ];
}

function encode_ut_pex_message($added_contacts, $dropped_endpoints = []) {
    if(!is_array($added_contacts) || !is_array($dropped_endpoints))
        throw new InvalidArgumentException("ut_pex contacts must be arrays.");

    if(count($added_contacts) > PEX_MAX_ADDED_PER_MESSAGE)
        throw new InvalidArgumentException("ut_pex cannot add more than PEX_MAX_ADDED_PER_MESSAGE contacts.");

    if(count($dropped_endpoints) > PEX_MAX_DROPPED_PER_MESSAGE)
        throw new InvalidArgumentException("ut_pex cannot drop more than PEX_MAX_DROPPED_PER_MESSAGE contacts.");

    $added_by_family = [4 => [], 6 => []];
    $flags_by_family = [4 => "", 6 => ""];
    $dropped_by_family = [4 => [], 6 => []];
    $seen_added = [];
    $seen_dropped = [];

    foreach($added_contacts as $contact) {
        if(
            !is_array($contact)
            || !isset($contact["endpoint"])
            || !($contact["endpoint"] instanceof PeerEndpoint)
        )
            throw new InvalidArgumentException("ut_pex added contact is invalid.");

        $flags = $contact["flags"] ?? 0;

        if(!is_int($flags) || $flags < 0 || $flags > 255)
            throw new InvalidArgumentException("ut_pex contact flags must be an unsigned byte.");

        $endpoint = $contact["endpoint"];
        $address_family = pex_endpoint_address_family($endpoint);

        if($address_family === null)
            continue;

        if(isset($seen_added[$endpoint->key]))
            continue;

        $seen_added[$endpoint->key] = true;
        $added_by_family[$address_family][] = encode_compact_pex_endpoint($endpoint);
        $flags_by_family[$address_family] .= chr($flags);
    }

    foreach($dropped_endpoints as $endpoint) {
        if(!($endpoint instanceof PeerEndpoint))
            throw new InvalidArgumentException("ut_pex dropped contact is invalid.");

        $address_family = pex_endpoint_address_family($endpoint);

        if($address_family === null)
            continue;

        if(isset($seen_added[$endpoint->key]))
            throw new InvalidArgumentException("ut_pex cannot add and drop the same endpoint.");

        if(isset($seen_dropped[$endpoint->key]))
            continue;

        $seen_dropped[$endpoint->key] = true;
        $dropped_by_family[$address_family][] = encode_compact_pex_endpoint($endpoint);
    }

    $message = [];

    if($added_by_family[4] !== []) {
        $message["added"] = implode("", $added_by_family[4]);
        $message["added.f"] = $flags_by_family[4];
    }

    if($added_by_family[6] !== []) {
        $message["added6"] = implode("", $added_by_family[6]);
        $message["added6.f"] = $flags_by_family[6];
    }

    if($dropped_by_family[4] !== [])
        $message["dropped"] = implode("", $dropped_by_family[4]);

    if($dropped_by_family[6] !== [])
        $message["dropped6"] = implode("", $dropped_by_family[6]);

    if($message === [])
        throw new InvalidArgumentException("ut_pex message must contain at least one encodable contact.");

    return bencode_encode(new BencodeDictionary($message));
}

// BEP 9 metadata exchange.
function validate_ut_metadata_size($metadata_size) {
    if(!is_int($metadata_size) || $metadata_size < 1 || $metadata_size > UT_METADATA_MAX_SIZE)
        throw new InvalidArgumentException("Metadata size must be between 1 and UT_METADATA_MAX_SIZE bytes.");
}

function validate_ut_metadata_piece_index($piece) {
    $maximum_piece = intdiv(UT_METADATA_MAX_SIZE - 1, UT_METADATA_BLOCK_LENGTH);

    if(!is_int($piece) || $piece < 0 || $piece > $maximum_piece)
        throw new InvalidArgumentException("Metadata piece index is outside the supported range.");
}

function ut_metadata_piece_count($metadata_size) {
    validate_ut_metadata_size($metadata_size);

    return intdiv($metadata_size + UT_METADATA_BLOCK_LENGTH - 1, UT_METADATA_BLOCK_LENGTH);
}

function ut_metadata_piece_length($metadata_size, $piece) {
    validate_ut_metadata_size($metadata_size);
    validate_ut_metadata_piece_index($piece);
    $piece_count = ut_metadata_piece_count($metadata_size);

    if($piece >= $piece_count)
        throw new InvalidArgumentException("Metadata piece index is outside the advertised metadata size.");

    if($piece < $piece_count - 1)
        return UT_METADATA_BLOCK_LENGTH;

    return $metadata_size - ($piece * UT_METADATA_BLOCK_LENGTH);
}

function encode_ut_metadata_request($piece) {
    validate_ut_metadata_piece_index($piece);

    return bencode_encode(new BencodeDictionary([
        "msg_type" => UT_METADATA_REQUEST,
        "piece" => $piece,
    ]));
}

function encode_ut_metadata_data($piece, $total_size, $data) {
    if(!is_string($data))
        throw new InvalidArgumentException("Metadata piece data must be a byte string.");

    $expected_length = ut_metadata_piece_length($total_size, $piece);

    if(strlen($data) !== $expected_length)
        throw new InvalidArgumentException("Metadata piece data does not match its required block length.");

    return bencode_encode(new BencodeDictionary([
        "msg_type" => UT_METADATA_DATA,
        "piece" => $piece,
        "total_size" => $total_size,
    ])) . $data;
}

function encode_ut_metadata_reject($piece) {
    validate_ut_metadata_piece_index($piece);

    return bencode_encode(new BencodeDictionary([
        "msg_type" => UT_METADATA_REJECT,
        "piece" => $piece,
    ]));
}

function parse_ut_metadata_message($payload) {
    if(!is_string($payload))
        throw new InvalidArgumentException("Metadata extension payload must be a byte string.");

    if($payload === "" || $payload[0] !== "d")
        throw new RuntimeException("Metadata extension message must begin with a bencoded dictionary.");

    $offset = 0;

    try {
        $header = bencode_decode_value($payload, $offset, 0);
    } catch(InvalidArgumentException $exception) {
        throw new RuntimeException("Metadata extension message has an invalid bencoded header.", 0, $exception);
    }

    if(!is_array($header) || ($header !== [] && array_is_list($header)))
        throw new RuntimeException("Metadata extension message header must be a dictionary.");

    if(!array_key_exists("msg_type", $header) || !is_int($header["msg_type"]))
        throw new RuntimeException("Metadata extension message has no integer message type.");

    if(!array_key_exists("piece", $header))
        throw new RuntimeException("Metadata extension message has no piece index.");

    try {
        validate_ut_metadata_piece_index($header["piece"]);
    } catch(InvalidArgumentException $exception) {
        throw new RuntimeException("Metadata extension message has an invalid piece index.", 0, $exception);
    }

    $msg_type = $header["msg_type"];
    $piece = $header["piece"];
    $trailing_data = substr($payload, $offset);

    if($msg_type !== UT_METADATA_REQUEST && $msg_type !== UT_METADATA_DATA && $msg_type !== UT_METADATA_REJECT) {
        return [
            "type" => "unknown",
            "msg_type" => $msg_type,
            "piece" => $piece,
            "header" => $header,
            "payload" => $trailing_data,
        ];
    }

    if($msg_type === UT_METADATA_REQUEST || $msg_type === UT_METADATA_REJECT) {
        if($trailing_data !== "")
            throw new RuntimeException("Metadata request and reject messages cannot contain trailing data.");

        return [
            "type" => $msg_type === UT_METADATA_REQUEST ? "request" : "reject",
            "msg_type" => $msg_type,
            "piece" => $piece,
        ];
    }

    if(!array_key_exists("total_size", $header))
        throw new RuntimeException("Metadata data message has no total size.");

    try {
        $expected_length = ut_metadata_piece_length($header["total_size"], $piece);
    } catch(InvalidArgumentException $exception) {
        throw new RuntimeException("Metadata data message has an invalid total size or piece index.", 0, $exception);
    }

    if(strlen($trailing_data) !== $expected_length)
        throw new RuntimeException("Metadata data message has an invalid block length.");

    return [
        "type" => "data",
        "msg_type" => $msg_type,
        "piece" => $piece,
        "total_size" => $header["total_size"],
        "data" => $trailing_data,
    ];
}

final class MetadataExchange {
    public $expected_info_hash;
    public $max_size;
    public $metadata_size = null;
    public $piece_count = 0;
    public $pieces = [];
    public $requested_pieces = [];
    public $rejection_counts = [];
    public $metadata = null;
    public $info_dictionary = null;
    public $torrent_metadata = null;
    public $failure_reason = null;

    public function __construct($expected_info_hash, $max_size = UT_METADATA_MAX_SIZE) {
        if(!is_string($expected_info_hash) || strlen($expected_info_hash) !== 20)
            throw new InvalidArgumentException("Metadata exchange info hash must contain exactly 20 bytes.");

        validate_ut_metadata_size($max_size);
        $this->expected_info_hash = $expected_info_hash;
        $this->max_size = $max_size;
    }

    public static function from_magnet($magnet, $max_size = UT_METADATA_MAX_SIZE) {
        if(!($magnet instanceof MagnetUri))
            throw new InvalidArgumentException("Metadata exchange requires a parsed magnet URI.");

        return new self($magnet->info_hash, $max_size);
    }

    public function set_metadata_size($metadata_size) {
        validate_ut_metadata_size($metadata_size);

        if($metadata_size > $this->max_size)
            throw new RuntimeException("Peer metadata size exceeds the configured safety limit.");

        if($this->metadata_size !== null && $this->metadata_size !== $metadata_size)
            throw new RuntimeException("Peer metadata size conflicts with the active metadata exchange.");

        if($this->metadata_size === null) {
            $this->metadata_size = $metadata_size;
            $this->piece_count = ut_metadata_piece_count($metadata_size);
        }

        return $this->metadata_size;
    }

    public function get_missing_pieces() {
        if($this->metadata_size === null)
            return [];

        $missing_pieces = [];

        for($piece = 0; $piece < $this->piece_count; $piece++) {
            if(!array_key_exists($piece, $this->pieces))
                $missing_pieces[] = $piece;
        }

        return $missing_pieces;
    }

    public function reserve_pieces($limit = 1) {
        if(!is_int($limit) || $limit < 1)
            throw new InvalidArgumentException("Metadata request limit must be a positive integer.");

        if($this->metadata_size === null)
            throw new LogicException("Metadata pieces cannot be requested before metadata_size is known.");

        if($this->is_complete())
            return [];

        $reserved_pieces = [];

        for($piece = 0; $piece < $this->piece_count && count($reserved_pieces) < $limit; $piece++) {
            if(array_key_exists($piece, $this->pieces) || isset($this->requested_pieces[$piece]))
                continue;

            $this->requested_pieces[$piece] = true;
            $reserved_pieces[] = $piece;
        }

        return $reserved_pieces;
    }

    public function release_requests($pieces) {
        if(!is_array($pieces))
            throw new InvalidArgumentException("Released metadata requests must be an array of piece indexes.");

        foreach($pieces as $piece) {
            validate_ut_metadata_piece_index($piece);
            unset($this->requested_pieces[$piece]);
        }
    }

    public function record_rejection($piece) {
        validate_ut_metadata_piece_index($piece);

        if($this->metadata_size !== null)
            ut_metadata_piece_length($this->metadata_size, $piece);

        unset($this->requested_pieces[$piece]);
        $this->rejection_counts[$piece] = ($this->rejection_counts[$piece] ?? 0) + 1;
    }

    private function reject_assembly($reason) {
        $this->pieces = [];
        $this->requested_pieces = [];
        $this->metadata = null;
        $this->info_dictionary = null;
        $this->torrent_metadata = null;
        $this->failure_reason = $reason;

        throw new RuntimeException($reason);
    }

    private function verify_assembly() {
        $metadata = "";

        for($piece = 0; $piece < $this->piece_count; $piece++) {
            if(!array_key_exists($piece, $this->pieces))
                return false;

            $metadata .= $this->pieces[$piece];
        }

        if(strlen($metadata) !== $this->metadata_size)
            $this->reject_assembly("Assembled metadata does not match its advertised size.");

        if(!hash_equals($this->expected_info_hash, sha1($metadata, true)))
            $this->reject_assembly("Assembled metadata does not match the magnet info hash.");

        if($metadata === "" || $metadata[0] !== "d")
            $this->reject_assembly("Verified metadata is not a bencoded info dictionary.");

        try {
            $info_dictionary = bencode_decode($metadata);
        } catch(InvalidArgumentException $exception) {
            $this->reject_assembly("Verified metadata is not valid bencode.");
        }

        if(!is_array($info_dictionary) || ($info_dictionary !== [] && array_is_list($info_dictionary)))
            $this->reject_assembly("Verified metadata is not a bencoded info dictionary.");

        try {
            $torrent_metadata = TorrentMetadata::from_info_bytes(
                $metadata,
                $this->expected_info_hash
            );
        } catch(Throwable $exception) {
            $this->reject_assembly("Verified metadata is not valid torrent metadata: {$exception->getMessage()}");
        }

        $this->metadata = $metadata;
        $this->info_dictionary = $info_dictionary;
        $this->torrent_metadata = $torrent_metadata;
        $this->failure_reason = null;

        return true;
    }

    public function add_piece($piece, $total_size, $data) {
        if(!is_string($data))
            throw new InvalidArgumentException("Metadata piece data must be a byte string.");

        $this->set_metadata_size($total_size);
        $expected_length = ut_metadata_piece_length($this->metadata_size, $piece);

        if(strlen($data) !== $expected_length)
            throw new InvalidArgumentException("Metadata piece data does not match its required block length.");

        if(array_key_exists($piece, $this->pieces)) {
            if(!hash_equals($this->pieces[$piece], $data))
                throw new RuntimeException("Peer supplied conflicting data for a metadata piece.");

            unset($this->requested_pieces[$piece]);

            return $this->is_complete();
        }

        $this->pieces[$piece] = $data;
        unset($this->requested_pieces[$piece]);

        if(count($this->pieces) < $this->piece_count)
            return false;

        return $this->verify_assembly();
    }

    public function is_complete() {
        return $this->metadata !== null;
    }

    public function get_metadata() {
        if(!$this->is_complete())
            throw new LogicException("Metadata is not complete and verified.");

        return $this->metadata;
    }

    public function get_info_dictionary() {
        if(!$this->is_complete())
            throw new LogicException("Metadata is not complete and verified.");

        return $this->info_dictionary;
    }

    public function get_torrent_metadata() {
        if(!$this->is_complete())
            throw new LogicException("Torrent metadata is not complete and verified.");

        return $this->torrent_metadata;
    }

    public function get_piece($piece) {
        if(!$this->is_complete())
            throw new LogicException("Unverified metadata cannot be served to peers.");

        $piece_length = ut_metadata_piece_length($this->metadata_size, $piece);

        return substr($this->metadata, $piece * UT_METADATA_BLOCK_LENGTH, $piece_length);
    }
}

// Torrent metadata and storage.
function is_torrent_dictionary($value) {
    return is_array($value) && ($value === [] || !array_is_list($value));
}

function validate_torrent_path_component($component) {
    if(!is_string($component) || $component === "")
        throw new RuntimeException("Torrent path components must be non-empty byte strings.");

    if(strlen($component) > TORRENT_PATH_COMPONENT_MAX_LENGTH)
        throw new RuntimeException("Torrent path component exceeds the supported byte length.");

    if(str_contains($component, "\0"))
        throw new RuntimeException("Torrent path components cannot contain NUL bytes.");

    if(str_contains($component, "/") || str_contains($component, "\\"))
        throw new RuntimeException("Torrent path components cannot contain directory separators.");

    if($component === "." || $component === "..")
        throw new RuntimeException("Torrent path components cannot traverse directories.");

    if(preg_match("/[\x01-\x1f\x7f]/", $component) === 1)
        throw new RuntimeException("Torrent path components cannot contain control bytes.");

    if(str_contains($component, ":"))
        throw new RuntimeException("Torrent path components cannot contain drive or stream separators.");

    if(str_ends_with($component, ".") || str_ends_with($component, " "))
        throw new RuntimeException("Torrent path components cannot end with a dot or space.");

    $device_name = strtoupper(explode(".", $component, 2)[0]);

    if(
        in_array($device_name, ["CON", "PRN", "AUX", "NUL", "CLOCK\$"], true)
        || preg_match("/\A(?:COM|LPT)[1-9]\z/", $device_name) === 1
    )
        throw new RuntimeException("Torrent path component uses a reserved device name.");

    return $component;
}

function read_torrent_info_integer($dictionary, $key, $minimum, $description) {
    if(!array_key_exists($key, $dictionary) || !is_int($dictionary[$key]) || $dictionary[$key] < $minimum)
        throw new RuntimeException("Torrent metadata {$description} is missing or invalid.");

    return $dictionary[$key];
}

function read_torrent_info_string($dictionary, $key, $description) {
    if(!array_key_exists($key, $dictionary) || !is_string($dictionary[$key]))
        throw new RuntimeException("Torrent metadata {$description} is missing or invalid.");

    return $dictionary[$key];
}

function add_torrent_lengths($left, $right) {
    if(!is_int($left) || !is_int($right) || $left < 0 || $right < 0)
        throw new InvalidArgumentException("Torrent lengths must be non-negative integers.");

    if($right > PHP_INT_MAX - $left)
        throw new RuntimeException("Torrent total length exceeds the supported integer range.");

    return $left + $right;
}

function torrent_path_key($components) {
    $key = implode("\0", $components);

    if(PHP_OS_FAMILY === "Windows")
        $key = strtolower($key);

    return $key;
}

function register_torrent_file_path(&$file_paths, &$directory_paths, $components) {
    $file_key = torrent_path_key($components);

    if(isset($file_paths[$file_key]) || isset($directory_paths[$file_key]))
        throw new RuntimeException("Torrent metadata contains colliding file paths.");

    $component_count = count($components);

    for($length = 1; $length < $component_count; $length++) {
        $directory_key = torrent_path_key(array_slice($components, 0, $length));

        if(isset($file_paths[$directory_key]))
            throw new RuntimeException("Torrent metadata uses one path as both a file and directory.");
    }

    $file_paths[$file_key] = true;

    for($length = 1; $length < $component_count; $length++)
        $directory_paths[torrent_path_key(array_slice($components, 0, $length))] = true;
}

final class TorrentFileEntry {
    public $path_components;
    public $relative_path;
    public $length;
    public $torrent_offset;
    public $torrent_end_offset;

    public function __construct($path_components, $length, $torrent_offset) {
        if(!is_array($path_components) || !array_is_list($path_components) || $path_components === [])
            throw new InvalidArgumentException("Torrent file paths must be non-empty component lists.");

        foreach($path_components as $component)
            validate_torrent_path_component($component);

        if(!is_int($length) || $length < 0)
            throw new InvalidArgumentException("Torrent file length must be a non-negative integer.");

        if(!is_int($torrent_offset) || $torrent_offset < 0)
            throw new InvalidArgumentException("Torrent file offset must be a non-negative integer.");

        $this->path_components = array_values($path_components);
        $this->relative_path = implode("/", $this->path_components);
        $this->length = $length;
        $this->torrent_offset = $torrent_offset;
        $this->torrent_end_offset = add_torrent_lengths($torrent_offset, $length);
    }
}

final class TorrentMetadata {
    public $raw_info;
    public $info_hash;
    public $name;
    public $piece_length;
    public $piece_hashes;
    public $piece_count;
    public $files;
    public $total_length;
    public $is_multi_file;

    private function __construct() {
    }

    public static function from_info_bytes($raw_info, $expected_info_hash) {
        if(!is_string($raw_info) || $raw_info === "")
            throw new InvalidArgumentException("Torrent info bytes must be a non-empty byte string.");

        if(!is_string($expected_info_hash) || strlen($expected_info_hash) !== 20)
            throw new InvalidArgumentException("Torrent info hash must contain exactly 20 bytes.");

        if(!hash_equals($expected_info_hash, sha1($raw_info, true)))
            throw new RuntimeException("Torrent info bytes do not match the expected info hash.");

        if($raw_info[0] !== "d")
            throw new RuntimeException("Torrent info bytes must encode a dictionary.");

        try {
            $info = bencode_decode($raw_info);
        } catch(InvalidArgumentException $exception) {
            throw new RuntimeException("Torrent info bytes are not valid bencode.", 0, $exception);
        }

        if(!is_torrent_dictionary($info))
            throw new RuntimeException("Torrent info value must be a dictionary.");

        $name = read_torrent_info_string($info, "name", "name");
        validate_torrent_path_component($name);
        $piece_length = read_torrent_info_integer($info, "piece length", 1, "piece length");
        $pieces = read_torrent_info_string($info, "pieces", "piece hashes");
        $has_length = array_key_exists("length", $info);
        $has_files = array_key_exists("files", $info);

        if($has_length === $has_files)
            throw new RuntimeException("Torrent metadata must contain exactly one of length or files.");

        $files = [];
        $file_paths = [];
        $directory_paths = [];
        $total_length = 0;

        if($has_length) {
            $length = read_torrent_info_integer($info, "length", 0, "file length");
            $path_components = [$name];
            register_torrent_file_path($file_paths, $directory_paths, $path_components);
            $files[] = new TorrentFileEntry($path_components, $length, 0);
            $total_length = $length;
        } else {
            if(!is_array($info["files"]) || !array_is_list($info["files"]) || $info["files"] === [])
                throw new RuntimeException("Multi-file torrent metadata requires a non-empty files list.");

            foreach($info["files"] as $file) {
                if(!is_torrent_dictionary($file))
                    throw new RuntimeException("Torrent file entries must be dictionaries.");

                $length = read_torrent_info_integer($file, "length", 0, "file length");

                if(!array_key_exists("path", $file) || !is_array($file["path"]))
                    throw new RuntimeException("Torrent file path is missing or invalid.");

                if(!array_is_list($file["path"]) || $file["path"] === [])
                    throw new RuntimeException("Torrent file path must be a non-empty component list.");

                $path_components = [$name];

                foreach($file["path"] as $component) {
                    validate_torrent_path_component($component);
                    $path_components[] = $component;
                }

                register_torrent_file_path($file_paths, $directory_paths, $path_components);
                $files[] = new TorrentFileEntry($path_components, $length, $total_length);
                $total_length = add_torrent_lengths($total_length, $length);
            }
        }

        if(strlen($pieces) % 20 !== 0)
            throw new RuntimeException("Torrent piece-hash string length must be a multiple of 20 bytes.");

        $expected_piece_count = 0;

        if($total_length > 0)
            $expected_piece_count = intdiv($total_length - 1, $piece_length) + 1;
        $piece_count = intdiv(strlen($pieces), 20);

        if($piece_count !== $expected_piece_count)
            throw new RuntimeException("Torrent piece-hash count does not match the total length.");

        $piece_hashes = [];

        for($offset = 0; $offset < strlen($pieces); $offset += 20)
            $piece_hashes[] = substr($pieces, $offset, 20);

        $metadata = new self();
        $metadata->raw_info = $raw_info;
        $metadata->info_hash = $expected_info_hash;
        $metadata->name = $name;
        $metadata->piece_length = $piece_length;
        $metadata->piece_hashes = $piece_hashes;
        $metadata->piece_count = $piece_count;
        $metadata->files = $files;
        $metadata->total_length = $total_length;
        $metadata->is_multi_file = $has_files;

        return $metadata;
    }

    public static function from_metadata_exchange($metadata_exchange) {
        if(!($metadata_exchange instanceof MetadataExchange))
            throw new InvalidArgumentException("Torrent metadata requires a metadata exchange.");

        return $metadata_exchange->get_torrent_metadata();
    }
}

function normalise_torrent_storage_base_path($base_path = null) {
    if($base_path === null)
        $base_path = getcwd();

    if(!is_string($base_path) || $base_path === "")
        throw new RuntimeException("Torrent storage requires a current working directory.");

    if(str_contains($base_path, "\0"))
        throw new RuntimeException("Torrent storage base path cannot contain NUL bytes.");

    $base_path = realpath($base_path);

    if($base_path === false || !is_dir($base_path))
        throw new RuntimeException("Torrent storage base path must be an existing directory.");

    return $base_path;
}

function torrent_storage_path_is_within_base($base_path, $path) {
    $comparison_base = $base_path;
    $comparison_path = $path;

    if(PHP_OS_FAMILY === "Windows") {
        $comparison_base = strtolower($comparison_base);
        $comparison_path = strtolower($comparison_path);
    }

    if($comparison_path === $comparison_base)
        return true;

    $prefix = rtrim($comparison_base, "/\\") . DIRECTORY_SEPARATOR;

    return str_starts_with($comparison_path, $prefix);
}

function build_torrent_storage_path($base_path, $components) {
    if(!is_array($components) || !array_is_list($components))
        throw new InvalidArgumentException("Torrent storage paths require an ordered component list.");

    $path = $base_path;

    foreach($components as $component) {
        validate_torrent_path_component($component);
        $path .= DIRECTORY_SEPARATOR . $component;
    }

    if(!torrent_storage_path_is_within_base($base_path, $path))
        throw new RuntimeException("Torrent storage path escapes the current working directory.");

    return $path;
}

function ensure_torrent_storage_directory($base_path, $components) {
    if(!is_array($components) || !array_is_list($components))
        throw new InvalidArgumentException("Torrent storage directories require an ordered component list.");

    $path = $base_path;

    foreach($components as $component) {
        $path = build_torrent_storage_path($path, [$component]);
        clearstatcache(true, $path);

        if(is_link($path))
            throw new RuntimeException("Torrent storage refuses symbolic-link directories.");

        if(!file_exists($path) && !@mkdir($path, 0777) && !is_dir($path))
            throw new RuntimeException("Torrent storage directory could not be created.");

        clearstatcache(true, $path);

        if(is_link($path) || !is_dir($path))
            throw new RuntimeException("Torrent storage path is not a safe directory.");

        $resolved_path = realpath($path);

        if($resolved_path === false || !torrent_storage_path_is_within_base($base_path, $resolved_path))
            throw new RuntimeException("Torrent storage directory escapes the current working directory.");
    }

    return $path;
}

function validate_torrent_storage_file($base_path, $path, $must_exist = true) {
    if(!torrent_storage_path_is_within_base($base_path, $path))
        throw new RuntimeException("Torrent storage file escapes the current working directory.");

    clearstatcache(true, $path);

    if(is_link($path))
        throw new RuntimeException("Torrent storage refuses symbolic-link files.");

    if(!file_exists($path)) {
        if($must_exist)
            throw new RuntimeException("Torrent storage file does not exist.");

        return true;
    }

    if(!is_file($path))
        throw new RuntimeException("Torrent storage path is not a regular file.");

    $resolved_path = realpath($path);

    if($resolved_path === false || !torrent_storage_path_is_within_base($base_path, $resolved_path))
        throw new RuntimeException("Torrent storage file escapes the current working directory.");

    return true;
}

function initialise_torrent_storage_file($base_path, $path) {
    validate_torrent_storage_file($base_path, $path, false);

    if(file_exists($path))
        return true;

    $handle = @fopen($path, "x+b");

    if($handle === false)
        throw new RuntimeException("Torrent storage file could not be created.");

    fclose($handle);
    validate_torrent_storage_file($base_path, $path);

    return true;
}

function write_torrent_storage_file($base_path, $path, $offset, $data) {
    if(!is_int($offset) || $offset < 0 || !is_string($data))
        throw new InvalidArgumentException("Torrent storage file write arguments are invalid.");

    validate_torrent_storage_file($base_path, $path);

    if($data === "")
        return true;

    $handle = @fopen($path, "r+b");

    if($handle === false)
        throw new RuntimeException("Torrent storage file could not be opened for writing.");

    try {
        validate_torrent_storage_file($base_path, $path);

        if(fseek($handle, $offset) !== 0)
            throw new RuntimeException("Torrent storage file offset could not be selected.");

        $written = 0;

        while($written < strlen($data)) {
            $bytes_written = fwrite($handle, substr($data, $written));

            if($bytes_written === false || $bytes_written === 0)
                throw new RuntimeException("Torrent storage file write did not complete.");

            $written += $bytes_written;
        }

        if(!fflush($handle))
            throw new RuntimeException("Torrent storage file could not be flushed.");
    } finally {
        fclose($handle);
    }

    return true;
}

function read_torrent_storage_file($base_path, $path, $offset, $length) {
    if(!is_int($offset) || $offset < 0 || !is_int($length) || $length < 0)
        throw new InvalidArgumentException("Torrent storage file read arguments are invalid.");

    validate_torrent_storage_file($base_path, $path);

    if($length === 0)
        return "";

    $handle = @fopen($path, "rb");

    if($handle === false)
        throw new RuntimeException("Torrent storage file could not be opened for reading.");

    try {
        validate_torrent_storage_file($base_path, $path);

        if(fseek($handle, $offset) !== 0)
            throw new RuntimeException("Torrent storage file offset could not be selected.");

        $data = "";

        while(strlen($data) < $length) {
            $chunk = fread($handle, $length - strlen($data));

            if($chunk === false || $chunk === "")
                throw new RuntimeException("Torrent storage file is shorter than the requested range.");

            $data .= $chunk;
        }
    } finally {
        fclose($handle);
    }

    return $data;
}

final class TorrentStorage {
    public $metadata;
    public $base_path;
    public $total_length;
    public $files = [];
    public $initialised = false;
    private array $handles = [];

    public function __construct($metadata, $base_path = null) {
        if(!($metadata instanceof TorrentMetadata))
            throw new InvalidArgumentException("Torrent storage requires verified torrent metadata.");

        $this->metadata = $metadata;
        $this->base_path = normalise_torrent_storage_base_path($base_path);
        $this->total_length = $metadata->total_length;

        foreach($metadata->files as $index => $file) {
            $this->files[] = [
                "file_index" => $index,
                "relative_path" => $file->relative_path,
                "path" => build_torrent_storage_path($this->base_path, $file->path_components),
                "length" => $file->length,
                "torrent_offset" => $file->torrent_offset,
                "torrent_end_offset" => $file->torrent_end_offset,
                "path_components" => $file->path_components,
            ];
        }
    }

    public function __destruct() {
        $this->close_handles();
    }

    public static function from_metadata_exchange($metadata_exchange, $base_path = null) {
        return new self(
            TorrentMetadata::from_metadata_exchange($metadata_exchange),
            $base_path
        );
    }

    private function get_file_handle($file_index) {
        if(!isset($this->handles[$file_index]) || !is_resource($this->handles[$file_index]))
            throw new LogicException("Torrent storage file handle is not open.");

        return $this->handles[$file_index];
    }

    private function close_handles() {
        foreach($this->handles as $handle) {
            if(is_resource($handle))
                @fclose($handle);
        }

        $this->handles = [];
    }

    public function initialise() {
        if($this->initialised)
            return true;

        try {
            foreach($this->files as $file) {
                ensure_torrent_storage_directory(
                    $this->base_path,
                    array_slice($file["path_components"], 0, -1)
                );
                initialise_torrent_storage_file($this->base_path, $file["path"]);
                validate_torrent_storage_file($this->base_path, $file["path"]);
                $handle = @fopen($file["path"], "r+b");

                if($handle === false)
                    throw new RuntimeException("Torrent storage file could not be opened.");

                $this->handles[$file["file_index"]] = $handle;
            }
        } catch(Throwable $exception) {
            $this->close_handles();

            throw $exception;
        }

        $this->initialised = true;

        return true;
    }

    public function map_range($torrent_offset, $length) {
        if(!is_int($torrent_offset) || $torrent_offset < 0)
            throw new InvalidArgumentException("Torrent storage offset must be a non-negative integer.");

        if(!is_int($length) || $length < 0)
            throw new InvalidArgumentException("Torrent storage length must be a non-negative integer.");

        if(
            $torrent_offset > $this->total_length
            || $length > $this->total_length - $torrent_offset
        )
            throw new RuntimeException("Torrent storage range exceeds the torrent byte stream.");

        if($length === 0)
            return [];

        $range_end = $torrent_offset + $length;
        $segments = [];
        $mapped_length = 0;

        foreach($this->files as $file) {
            $segment_start = max($torrent_offset, $file["torrent_offset"]);
            $segment_end = min($range_end, $file["torrent_end_offset"]);

            if($segment_end <= $segment_start)
                continue;

            $segment_length = $segment_end - $segment_start;
            $segments[] = [
                "file_index" => $file["file_index"],
                "relative_path" => $file["relative_path"],
                "path" => $file["path"],
                "torrent_offset" => $segment_start,
                "file_offset" => $segment_start - $file["torrent_offset"],
                "data_offset" => $segment_start - $torrent_offset,
                "length" => $segment_length,
            ];
            $mapped_length += $segment_length;
        }

        if($mapped_length !== $length)
            throw new LogicException("Torrent storage range could not be mapped completely.");

        return $segments;
    }

    public function write($torrent_offset, $data) {
        if(!$this->initialised)
            throw new LogicException("Torrent storage must be initialised before writing.");

        if(!is_string($data))
            throw new InvalidArgumentException("Torrent storage writes require a byte string.");

        $data_length = strlen($data);

        foreach($this->map_range($torrent_offset, $data_length) as $segment) {
            $handle = $this->get_file_handle($segment["file_index"]);

            if(fseek($handle, $segment["file_offset"]) !== 0)
                throw new RuntimeException("Torrent storage file offset could not be selected.");

            $segment_data = $segment["data_offset"] === 0 && $segment["length"] === $data_length
                ? $data
                : substr($data, $segment["data_offset"], $segment["length"]);
            $written = 0;
            $segment_length = strlen($segment_data);

            while($written < $segment_length) {
                $bytes_written = fwrite(
                    $handle,
                    $written === 0 ? $segment_data : substr($segment_data, $written)
                );

                if($bytes_written === false || $bytes_written === 0)
                    throw new RuntimeException("Torrent storage file write did not complete.");

                $written += $bytes_written;
            }
        }

        return true;
    }

    public function read($torrent_offset, $length) {
        if(!$this->initialised)
            throw new LogicException("Torrent storage must be initialised before reading.");

        $data = "";

        foreach($this->map_range($torrent_offset, $length) as $segment) {
            $handle = $this->get_file_handle($segment["file_index"]);

            if(fseek($handle, $segment["file_offset"]) !== 0)
                throw new RuntimeException("Torrent storage file offset could not be selected.");

            $remaining = $segment["length"];

            while($remaining > 0) {
                $chunk = fread($handle, $remaining);

                if($chunk === false || $chunk === "")
                    throw new RuntimeException("Torrent storage file is shorter than the requested range.");

                $data .= $chunk;
                $remaining -= strlen($chunk);
            }
        }

        return $data;
    }

    public function sha1_range($torrent_offset, $length) {
        if(!$this->initialised)
            throw new LogicException("Torrent storage must be initialised before hashing.");

        $context = hash_init("sha1");

        foreach($this->map_range($torrent_offset, $length) as $segment) {
            $handle = $this->get_file_handle($segment["file_index"]);

            if(!fflush($handle))
                throw new RuntimeException("Torrent storage file could not be flushed before verification.");

            if(fseek($handle, $segment["file_offset"]) !== 0)
                throw new RuntimeException("Torrent storage file offset could not be selected for verification.");

            $hashed = hash_update_stream($context, $handle, $segment["length"]);

            if($hashed !== $segment["length"])
                throw new RuntimeException("Torrent storage file is shorter than the piece verification range.");
        }

        return hash_final($context, true);
    }

    public function get_paths() {
        return array_column($this->files, "path");
    }

    public function flush() {
        if(!$this->initialised)
            throw new LogicException("Torrent storage must be initialised before flushing.");

        $flushed_files = 0;

        foreach($this->files as $file) {
            $handle = $this->get_file_handle($file["file_index"]);

            if(!fflush($handle))
                throw new RuntimeException("Torrent storage file could not be flushed at completion.");

            if(function_exists("fsync") && !@fsync($handle))
                throw new RuntimeException("Torrent storage file could not be synchronised at completion.");

            $flushed_files++;
        }

        return $flushed_files;
    }
}

// Piece and block state.
final class PieceManager {
    public $metadata;
    public $storage;
    public $block_length;
    public $piece_count;
    public $expected_hashes;
    public $completed_blocks = [];
    public $outstanding_requests = [];
    public $verified_pieces = [];
    public $verified_piece_count = 0;
    public int $verified_byte_count = 0;
    public int $state_version = 0;
    public int $priority_state_version = 0;
    public $verification_failures = [];
    public $total_verification_failures = 0;

    public function __construct($metadata, $storage, $block_length = PEER_BLOCK_MAX_LENGTH) {
        if(!($metadata instanceof TorrentMetadata))
            throw new InvalidArgumentException("Piece management requires verified torrent metadata.");

        if(!($storage instanceof TorrentStorage))
            throw new InvalidArgumentException("Piece management requires torrent storage.");

        if(
            $metadata->raw_info !== $storage->metadata->raw_info
            || !hash_equals($metadata->info_hash, $storage->metadata->info_hash)
        )
            throw new InvalidArgumentException("Piece metadata and torrent storage do not describe the same torrent.");

        if(!is_int($block_length) || $block_length < 1 || $block_length > PEER_BLOCK_MAX_LENGTH)
            throw new InvalidArgumentException("Piece block length must be within the peer protocol limit.");

        $this->metadata = $metadata;
        $this->storage = $storage;
        $this->block_length = $block_length;
        $this->piece_count = $metadata->piece_count;
        $this->expected_hashes = $metadata->piece_hashes;
    }

    private function validate_piece_index($piece_index) {
        if(!is_int($piece_index) || $piece_index < 0 || $piece_index >= $this->piece_count)
            throw new InvalidArgumentException("Piece index is outside the torrent piece range.");

        return $piece_index;
    }

    private function validate_request_owner($request_owner) {
        if(!is_string($request_owner) || $request_owner === "")
            throw new InvalidArgumentException("Block request owner must be a non-empty string.");

        return $request_owner;
    }

    public function get_piece_offset($piece_index) {
        $this->validate_piece_index($piece_index);

        return $piece_index * $this->metadata->piece_length;
    }

    public function get_piece_length($piece_index) {
        $piece_offset = $this->get_piece_offset($piece_index);

        return min(
            $this->metadata->piece_length,
            $this->metadata->total_length - $piece_offset
        );
    }

    public function get_expected_hash($piece_index) {
        $this->validate_piece_index($piece_index);

        return $this->expected_hashes[$piece_index];
    }

    public function get_block_count($piece_index) {
        $piece_length = $this->get_piece_length($piece_index);

        return intdiv($piece_length - 1, $this->block_length) + 1;
    }

    public function get_block_length($piece_index, $begin) {
        $piece_length = $this->get_piece_length($piece_index);

        if(
            !is_int($begin)
            || $begin < 0
            || $begin >= $piece_length
            || $begin % $this->block_length !== 0
        )
            throw new InvalidArgumentException("Piece block offset does not identify a canonical block.");

        return min($this->block_length, $piece_length - $begin);
    }

    public function is_block_complete($piece_index, $begin) {
        $this->get_block_length($piece_index, $begin);

        return isset($this->verified_pieces[$piece_index])
            || isset($this->completed_blocks[$piece_index][$begin]);
    }

    public function is_piece_complete($piece_index) {
        $this->validate_piece_index($piece_index);

        return isset($this->verified_pieces[$piece_index]);
    }

    public function get_piece_failure_count($piece_index) {
        $this->validate_piece_index($piece_index);

        return $this->verification_failures[$piece_index] ?? 0;
    }

    public function get_piece_state($piece_index) {
        $piece_length = $this->get_piece_length($piece_index);
        $block_count = $this->get_block_count($piece_index);
        $verified = isset($this->verified_pieces[$piece_index]);

        return [
            "piece_index" => $piece_index,
            "torrent_offset" => $this->get_piece_offset($piece_index),
            "length" => $piece_length,
            "expected_hash" => $this->get_expected_hash($piece_index),
            "block_count" => $block_count,
            "complete_block_count" => $verified
                ? $block_count
                : count($this->completed_blocks[$piece_index] ?? []),
            "verified" => $verified,
            "verification_failures" => $this->get_piece_failure_count($piece_index),
        ];
    }

    public function can_serve_range($piece_index, $begin, $length) {
        $piece_length = $this->get_piece_length($piece_index);

        if(!is_int($begin) || $begin < 0)
            throw new InvalidArgumentException("Piece upload offset must be a non-negative integer.");

        if(!is_int($length) || $length < 1 || $length > PEER_BLOCK_MAX_LENGTH)
            throw new InvalidArgumentException("Piece upload length must be between 1 and PEER_BLOCK_MAX_LENGTH bytes.");

        if($begin > $piece_length || $length > $piece_length - $begin)
            throw new InvalidArgumentException("Piece upload range exceeds the piece boundary.");

        return $this->is_piece_complete($piece_index);
    }

    public function read_verified_range($piece_index, $begin, $length) {
        if(!$this->can_serve_range($piece_index, $begin, $length))
            throw new LogicException("Only verified piece data may be uploaded.");

        return $this->storage->read(
            $this->get_piece_offset($piece_index) + $begin,
            $length
        );
    }

    public function get_missing_blocks($piece_index, $limit = null) {
        $piece_length = $this->get_piece_length($piece_index);

        if($limit !== null && (!is_int($limit) || $limit < 1))
            throw new InvalidArgumentException("Missing-block limit must be a positive integer or null.");

        if(isset($this->verified_pieces[$piece_index]))
            return [];

        $blocks = [];

        for($begin = 0; $begin < $piece_length; $begin += $this->block_length) {
            if(isset($this->completed_blocks[$piece_index][$begin]))
                continue;

            $blocks[] = [
                "piece_index" => $piece_index,
                "begin" => $begin,
                "length" => min($this->block_length, $piece_length - $begin),
                "torrent_offset" => $this->get_piece_offset($piece_index) + $begin,
            ];

            if($limit !== null && count($blocks) >= $limit)
                break;
        }

        return $blocks;
    }

    public function get_unfinished_block_count($stop_after = null) {
        if($stop_after !== null && (!is_int($stop_after) || $stop_after < 0))
            throw new InvalidArgumentException("Unfinished-block stop threshold must be a non-negative integer or null.");

        $unfinished = 0;

        for($piece_index = 0; $piece_index < $this->piece_count; $piece_index++) {
            if(isset($this->verified_pieces[$piece_index]))
                continue;

            $unfinished += $this->get_block_count($piece_index)
                - count($this->completed_blocks[$piece_index] ?? []);

            if($stop_after !== null && $unfinished > $stop_after)
                return $unfinished;
        }

        return $unfinished;
    }

    public function get_all_outstanding_requests() {
        $requests = [];

        foreach($this->outstanding_requests as $piece_requests) {
            foreach($piece_requests as $request)
                $requests[] = $request;
        }

        return $requests;
    }

    public function reserve_blocks($piece_index, $limit, $request_owner, $requested_at = null) {
        $piece_length = $this->get_piece_length($piece_index);

        if(!is_int($limit) || $limit < 1)
            throw new InvalidArgumentException("Block request limit must be a positive integer.");

        $this->validate_request_owner($request_owner);

        if(!$this->storage->initialised)
            throw new LogicException("Block requests require initialised torrent storage.");

        $requested_at = normalise_peer_time($requested_at);

        if(isset($this->verified_pieces[$piece_index]))
            return [];

        $requests = [];

        for($begin = 0; $begin < $piece_length; $begin += $this->block_length) {
            if(
                isset($this->completed_blocks[$piece_index][$begin])
                || isset($this->outstanding_requests[$piece_index][$begin])
            )
                continue;

            $request = [
                "piece_index" => $piece_index,
                "begin" => $begin,
                "length" => min($this->block_length, $piece_length - $begin),
                "torrent_offset" => $this->get_piece_offset($piece_index) + $begin,
                "request_owner" => $request_owner,
                "requested_at" => $requested_at,
            ];
            $this->outstanding_requests[$piece_index][$begin] = $request;
            $requests[] = $request;

            if(count($requests) >= $limit)
                break;
        }

        return $requests;
    }

    public function get_outstanding_request($piece_index, $begin) {
        $this->get_block_length($piece_index, $begin);

        return $this->outstanding_requests[$piece_index][$begin] ?? null;
    }

    public function release_block_request($piece_index, $begin, $request_owner = null) {
        $this->get_block_length($piece_index, $begin);

        if($request_owner !== null)
            $this->validate_request_owner($request_owner);

        if(!isset($this->outstanding_requests[$piece_index][$begin]))
            return false;

        if(
            $request_owner !== null
            && $this->outstanding_requests[$piece_index][$begin]["request_owner"] !== $request_owner
        )
            return false;

        unset($this->outstanding_requests[$piece_index][$begin]);

        if($this->outstanding_requests[$piece_index] === [])
            unset($this->outstanding_requests[$piece_index]);

        return true;
    }

    public function get_verified_piece_indexes() {
        $piece_indexes = array_keys($this->verified_pieces);
        sort($piece_indexes, SORT_NUMERIC);

        return $piece_indexes;
    }

    public function reset_piece($piece_index) {
        $this->validate_piece_index($piece_index);
        $had_state = isset($this->completed_blocks[$piece_index])
            || isset($this->outstanding_requests[$piece_index])
            || isset($this->verified_pieces[$piece_index]);

        unset($this->completed_blocks[$piece_index]);
        unset($this->outstanding_requests[$piece_index]);

        if(isset($this->verified_pieces[$piece_index])) {
            unset($this->verified_pieces[$piece_index]);
            $this->verified_piece_count--;
            $this->verified_byte_count = max(
                0,
                $this->verified_byte_count - $this->get_piece_length($piece_index)
            );
        }

        if($had_state) {
            $this->state_version++;
            $this->priority_state_version++;
        }

        return true;
    }

    private function record_verification_failure($piece_index) {
        $this->reset_piece($piece_index);
        $this->verification_failures[$piece_index] = $this->get_piece_failure_count($piece_index) + 1;
        $this->total_verification_failures++;
    }

    private function verify_piece($piece_index) {
        $piece_hash = $this->storage->sha1_range(
            $this->get_piece_offset($piece_index),
            $this->get_piece_length($piece_index)
        );

        if(!hash_equals($this->get_expected_hash($piece_index), $piece_hash)) {
            $this->record_verification_failure($piece_index);

            return false;
        }

        unset($this->completed_blocks[$piece_index]);
        $this->verified_pieces[$piece_index] = true;
        $this->verified_piece_count++;
        $this->verified_byte_count += $this->get_piece_length($piece_index);
        $this->state_version++;
        $this->priority_state_version++;

        return true;
    }

    public function add_block($piece_index, $begin, $data) {
        if(!is_string($data))
            throw new InvalidArgumentException("Piece blocks must be byte strings.");

        $block_length = $this->get_block_length($piece_index, $begin);

        if(strlen($data) !== $block_length)
            throw new InvalidArgumentException("Piece block data does not match its canonical block length.");

        $this->release_block_request($piece_index, $begin);
        $torrent_offset = $this->get_piece_offset($piece_index) + $begin;

        if($this->is_block_complete($piece_index, $begin)) {
            if($this->storage->read($torrent_offset, $block_length) !== $data)
                throw new RuntimeException("Piece block conflicts with data already stored for its range.");

            if($this->is_piece_complete($piece_index))
                return true;

            if(count($this->completed_blocks[$piece_index]) === $this->get_block_count($piece_index))
                return $this->verify_piece($piece_index);

            return null;
        }

        $this->storage->write($torrent_offset, $data);
        $this->completed_blocks[$piece_index][$begin] = true;
        $this->state_version++;

        if(count($this->completed_blocks[$piece_index]) < $this->get_block_count($piece_index))
            return null;

        return $this->verify_piece($piece_index);
    }

    public function get_verified_byte_count() {
        return $this->verified_byte_count;
    }

    public function is_complete() {
        return $this->verified_piece_count === $this->piece_count;
    }
}

function build_verified_piece_bitfield($piece_manager) {
    if(!($piece_manager instanceof PieceManager))
        throw new InvalidArgumentException("Verified-piece bitfield requires a piece manager.");

    $byte_count = intdiv($piece_manager->piece_count + 7, 8);
    $bitfield = str_repeat("\x00", $byte_count);

    foreach($piece_manager->get_verified_piece_indexes() as $piece_index) {
        $byte_index = intdiv($piece_index, 8);
        $bit_index = 7 - ($piece_index % 8);
        $bitfield[$byte_index] = chr(ord($bitfield[$byte_index]) | (1 << $bit_index));
    }

    return $bitfield;
}

// Peer-wire message decoding.
function peer_message_type_for_id($message_id) {
    return match($message_id) {
        PEER_MESSAGE_CHOKE => "choke",
        PEER_MESSAGE_UNCHOKE => "unchoke",
        PEER_MESSAGE_INTERESTED => "interested",
        PEER_MESSAGE_NOT_INTERESTED => "not_interested",
        PEER_MESSAGE_HAVE => "have",
        PEER_MESSAGE_BITFIELD => "bitfield",
        PEER_MESSAGE_REQUEST => "request",
        PEER_MESSAGE_PIECE => "piece",
        PEER_MESSAGE_CANCEL => "cancel",
        PEER_MESSAGE_PORT => "port",
        PEER_MESSAGE_EXTENDED => "extended",
        default => throw new RuntimeException("Peer message has an unsupported ID."),
    };
}

function unpack_peer_message_uint32($payload, $offset, $name) {
    if(strlen($payload) - $offset < 4)
        throw new RuntimeException("Peer message {$name} is missing or incomplete.");

    $value = unpack("Nvalue", substr($payload, $offset, 4));

    if(!is_array($value))
        throw new RuntimeException("Peer message {$name} could not be decoded.");

    return $value["value"];
}

function parse_peer_message_payload($payload) {
    if(!is_string($payload))
        throw new InvalidArgumentException("Peer message payload must be a byte string.");

    if($payload === "")
        throw new RuntimeException("Peer message payload does not contain an ID.");

    if(strlen($payload) > PEER_MESSAGE_MAX_LENGTH)
        throw new RuntimeException("Peer message exceeds the configured maximum length.");

    $message_id = ord($payload[0]);
    $message_type = peer_message_type_for_id($message_id);

    if(in_array($message_id, [
        PEER_MESSAGE_CHOKE,
        PEER_MESSAGE_UNCHOKE,
        PEER_MESSAGE_INTERESTED,
        PEER_MESSAGE_NOT_INTERESTED,
    ], true)) {
        if(strlen($payload) !== 1)
            throw new RuntimeException("Peer {$message_type} message has an invalid payload length.");

        return [
            "type" => $message_type,
            "id" => $message_id,
        ];
    }

    if($message_id === PEER_MESSAGE_HAVE) {
        if(strlen($payload) !== 5)
            throw new RuntimeException("Peer have message has an invalid payload length.");

        return [
            "type" => $message_type,
            "id" => $message_id,
            "piece_index" => unpack_peer_message_uint32($payload, 1, "piece index"),
        ];
    }

    if($message_id === PEER_MESSAGE_BITFIELD) {
        return [
            "type" => $message_type,
            "id" => $message_id,
            "bitfield" => substr($payload, 1),
        ];
    }

    if($message_id === PEER_MESSAGE_REQUEST || $message_id === PEER_MESSAGE_CANCEL) {
        if(strlen($payload) !== 13)
            throw new RuntimeException("Peer {$message_type} message has an invalid payload length.");

        $block_length = unpack_peer_message_uint32($payload, 9, "block length");

        if($block_length < 1 || $block_length > PEER_BLOCK_MAX_LENGTH)
            throw new RuntimeException("Peer {$message_type} message has an invalid block length.");

        return [
            "type" => $message_type,
            "id" => $message_id,
            "piece_index" => unpack_peer_message_uint32($payload, 1, "piece index"),
            "begin" => unpack_peer_message_uint32($payload, 5, "block offset"),
            "length" => $block_length,
        ];
    }

    if($message_id === PEER_MESSAGE_PIECE) {
        if(strlen($payload) < 10)
            throw new RuntimeException("Peer piece message has an invalid payload length.");

        $block = substr($payload, 9);

        if(strlen($block) > PEER_BLOCK_MAX_LENGTH)
            throw new RuntimeException("Peer piece message exceeds the maximum block length.");

        return [
            "type" => $message_type,
            "id" => $message_id,
            "piece_index" => unpack_peer_message_uint32($payload, 1, "piece index"),
            "begin" => unpack_peer_message_uint32($payload, 5, "block offset"),
            "block" => $block,
        ];
    }

    if($message_id === PEER_MESSAGE_PORT) {
        if(strlen($payload) !== 3)
            throw new RuntimeException("Peer port message has an invalid payload length.");

        $decoded = unpack("nport", substr($payload, 1, 2));

        if(!is_array($decoded) || $decoded["port"] < 1)
            throw new RuntimeException("Peer port message has an invalid DHT port.");

        return [
            "type" => $message_type,
            "id" => $message_id,
            "port" => $decoded["port"],
        ];
    }

    if($message_id === PEER_MESSAGE_EXTENDED) {
        if(strlen($payload) < 2)
            throw new RuntimeException("Peer extended message does not contain an extension ID.");

        $extension_id = ord($payload[1]);
        $extension_payload = substr($payload, 2);

        if($extension_id !== PEER_EXTENSION_HANDSHAKE) {
            return [
                "type" => $message_type,
                "id" => $message_id,
                "extension_id" => $extension_id,
                "payload" => $extension_payload,
            ];
        }

        $extension_handshake = parse_peer_extension_handshake($extension_payload);

        return [
            "type" => "extended_handshake",
            "id" => $message_id,
            "extension_id" => $extension_id,
            "extension_ids" => $extension_handshake["extension_ids"],
            "handshake" => $extension_handshake["handshake"],
        ];
    }

    throw new RuntimeException("Peer message could not be decoded.");
}

function consume_peer_message_at_offset($buffer, &$offset) {
    if(!is_string($buffer) || !is_int($offset) || $offset < 0 || $offset > strlen($buffer))
        throw new InvalidArgumentException("Peer message cursor is invalid.");

    $available = strlen($buffer) - $offset;

    if($available < 4)
        return null;

    $message_length = unpack_peer_message_uint32($buffer, $offset, "length prefix");

    if($message_length > PEER_MESSAGE_MAX_LENGTH)
        throw new RuntimeException("Peer message exceeds the configured maximum length.");

    if($available < 4 + $message_length)
        return null;

    $frame_offset = $offset;
    $offset += 4 + $message_length;

    if($message_length === 0) {
        return [
            "type" => "keep_alive",
            "id" => null,
        ];
    }

    $message_id = ord($buffer[$frame_offset + 4]);

    if($message_id === PEER_MESSAGE_PIECE) {
        if($message_length < 10)
            throw new RuntimeException("Peer piece message has an invalid payload length.");

        $block_length = $message_length - 9;

        if($block_length < 1 || $block_length > PEER_BLOCK_MAX_LENGTH)
            throw new RuntimeException("Peer piece message exceeds the maximum block length.");

        return [
            "type" => "piece",
            "id" => PEER_MESSAGE_PIECE,
            "piece_index" => unpack_peer_message_uint32($buffer, $frame_offset + 5, "piece index"),
            "begin" => unpack_peer_message_uint32($buffer, $frame_offset + 9, "block offset"),
            "block" => substr($buffer, $frame_offset + 13, $block_length),
        ];
    }

    return parse_peer_message_payload(substr($buffer, $frame_offset + 4, $message_length));
}

function consume_peer_message(&$buffer) {
    if(!is_string($buffer))
        throw new InvalidArgumentException("Peer message buffer must be a byte string.");

    if(strlen($buffer) < 4)
        return null;

    $message_length = unpack_peer_message_uint32($buffer, 0, "length prefix");

    if($message_length > PEER_MESSAGE_MAX_LENGTH)
        throw new RuntimeException("Peer message exceeds the configured maximum length.");

    if(strlen($buffer) < 4 + $message_length)
        return null;

    if($message_length === 0) {
        $buffer = substr($buffer, 4);

        return [
            "type" => "keep_alive",
            "id" => null,
        ];
    }

    $message = parse_peer_message_payload(substr($buffer, 4, $message_length));
    $buffer = substr($buffer, 4 + $message_length);

    return $message;
}

function consume_peer_messages(&$buffer) {
    $messages = [];

    while(true) {
        $message = consume_peer_message($buffer);

        if($message === null)
            return $messages;

        $messages[] = $message;
    }
}

function decode_peer_message($frame) {
    if(!is_string($frame))
        throw new InvalidArgumentException("Peer message frame must be a byte string.");

    $buffer = $frame;
    $message = consume_peer_message($buffer);

    if($message === null)
        throw new RuntimeException("Peer message frame is incomplete.");

    if($buffer !== "")
        throw new RuntimeException("Peer message frame contains trailing bytes.");

    return $message;
}

// Inbound peer listening and automatic router port mapping.
function parse_socket_host_port($socket_name) {
    if(!is_string($socket_name) || trim($socket_name) === "")
        throw new InvalidArgumentException("Socket endpoint must be a non-empty string.");

    return PeerEndpoint::from_string($socket_name);
}

function detect_outbound_local_ipv4() {
    $error_number = 0;
    $error_message = "";
    $socket = @stream_socket_client(
        "udp://1.1.1.1:53",
        $error_number,
        $error_message,
        PORT_MAPPING_UDP_TIMEOUT,
        STREAM_CLIENT_CONNECT
    );

    if($socket === false)
        return null;

    try {
        $local_name = @stream_socket_get_name($socket, false);

        if(!is_string($local_name))
            return null;

        $endpoint = parse_socket_host_port($local_name);

        if($endpoint->is_ipv6 || filter_var($endpoint->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false)
            return null;

        return $endpoint->host;
    } catch(Throwable) {
        return null;
    } finally {
        fclose($socket);
    }
}

function detect_local_ipv4_for_url($url) {
    if(!is_string($url) || preg_match('/\Ahttps?:\/\//i', $url) !== 1)
        return null;

    $parts = parse_url($url);

    if(!is_array($parts) || !isset($parts["host"]))
        return null;

    $host = strval($parts["host"]);

    if(filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false)
        return null;

    $port = isset($parts["port"])
        ? intval($parts["port"])
        : (strtolower(strval($parts["scheme"] ?? "http")) === "https" ? 443 : 80);

    if($port < 1 || $port > 65535)
        return null;

    $error_number = 0;
    $error_message = "";
    $socket = @stream_socket_client(
        "udp://{$host}:{$port}",
        $error_number,
        $error_message,
        PORT_MAPPING_UDP_TIMEOUT,
        STREAM_CLIENT_CONNECT
    );

    if($socket === false)
        return null;

    try {
        $local_name = @stream_socket_get_name($socket, false);

        if(!is_string($local_name))
            return null;

        $endpoint = parse_socket_host_port($local_name);

        if($endpoint->is_ipv6 || filter_var($endpoint->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false)
            return null;

        return $endpoint->host;
    } catch(Throwable) {
        return null;
    } finally {
        fclose($socket);
    }
}

function resolve_upnp_internal_client_ipv4($service, $fallback_local_ip = null, $route_detector = null) {
    if(!is_array($service) || !isset($service["control_url"]))
        throw new InvalidArgumentException("UPnP internal-client resolution requires a control service.");

    if($route_detector === null)
        $route_detector = "detect_local_ipv4_for_url";

    if(!is_callable($route_detector))
        throw new InvalidArgumentException("UPnP internal-client route detector must be callable.");

    $route_local_ip = $route_detector($service["control_url"]);

    if(is_string($route_local_ip) && filter_var($route_local_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false)
        return $route_local_ip;

    if(is_string($fallback_local_ip) && filter_var($fallback_local_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false)
        return $fallback_local_ip;

    return null;
}

function decode_linux_route_gateway($encoded_gateway) {
    if(!is_string($encoded_gateway) || preg_match('/\A[0-9A-Fa-f]{8}\z/', $encoded_gateway) !== 1)
        return null;

    $bytes = str_split($encoded_gateway, 2);
    $bytes = array_reverse($bytes);
    $packed = hex2bin(implode("", $bytes));

    if($packed === false)
        return null;

    $gateway = @inet_ntop($packed);

    return is_string($gateway) ? $gateway : null;
}

function detect_default_ipv4_gateway() {
    if(is_readable("/proc/net/route")) {
        $contents = @file_get_contents("/proc/net/route");

        if(is_string($contents)) {
            foreach(preg_split('/\r\n|\n|\r/', $contents) as $line) {
                $fields = preg_split('/\s+/', trim($line));

                if(count($fields) < 4 || $fields[1] !== "00000000")
                    continue;

                $flags = hexdec($fields[3]);

                if(($flags & 0x2) === 0)
                    continue;

                $gateway = decode_linux_route_gateway($fields[2]);

                if($gateway !== null && $gateway !== "0.0.0.0")
                    return $gateway;
            }
        }
    }

    if(PHP_OS_FAMILY === "Darwin" && function_exists("shell_exec")) {
        $output = @shell_exec("/sbin/route -n get default 2>/dev/null");

        if(is_string($output) && preg_match('/^\s*gateway:\s*([^\s]+)\s*$/mi', $output, $matches) === 1) {
            $gateway = trim($matches[1]);

            if(filter_var($gateway, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false)
                return $gateway;
        }
    }

    return null;
}

function ipv4_to_ipv4_mapped_ipv6($address) {
    if(!is_string($address) || filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false)
        throw new InvalidArgumentException("PCP requires a valid IPv4 address.");

    $packed = inet_pton($address);

    if($packed === false)
        throw new InvalidArgumentException("PCP IPv4 address could not be encoded.");

    return str_repeat("\0", 10) . "\xff\xff" . $packed;
}

function decode_pcp_ip_address($packed) {
    if(!is_string($packed) || strlen($packed) !== 16)
        return null;

    if(substr($packed, 0, 12) === str_repeat("\0", 10) . "\xff\xff") {
        $address = @inet_ntop(substr($packed, 12, 4));

        return is_string($address) ? $address : null;
    }

    $address = @inet_ntop($packed);

    return is_string($address) ? $address : null;
}

function exchange_port_mapping_udp($gateway, $packet, $timeout = PORT_MAPPING_UDP_TIMEOUT) {
    if(!is_string($gateway) || filter_var($gateway, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false)
        throw new InvalidArgumentException("Port mapping gateway must be a valid IPv4 address.");

    if(!is_string($packet) || $packet === "")
        throw new InvalidArgumentException("Port mapping UDP packet must be non-empty.");

    $error_number = 0;
    $error_message = "";
    $socket = @stream_socket_client(
        "udp://{$gateway}:5351",
        $error_number,
        $error_message,
        floatval($timeout),
        STREAM_CLIENT_CONNECT
    );

    if($socket === false)
        return null;

    try {
        stream_set_timeout($socket, 0, max(1, intval(round(floatval($timeout) * 1000000))));
        $written = @fwrite($socket, $packet);

        if($written !== strlen($packet))
            return null;

        $response = @fread($socket, 1100);

        return is_string($response) && $response !== "" ? $response : null;
    } finally {
        fclose($socket);
    }
}

function build_pcp_map_request($local_ip, $internal_port, $external_port, $lifetime, $nonce) {
    if(!is_int($internal_port) || $internal_port < 1 || $internal_port > 65535)
        throw new InvalidArgumentException("PCP internal TCP port is invalid.");

    if(!is_int($external_port) || $external_port < 0 || $external_port > 65535)
        throw new InvalidArgumentException("PCP external TCP port is invalid.");

    if(!is_int($lifetime) || $lifetime < 0)
        throw new InvalidArgumentException("PCP mapping lifetime is invalid.");

    if(!is_string($nonce) || strlen($nonce) !== 12)
        throw new InvalidArgumentException("PCP mapping nonce must contain 12 bytes.");

    return chr(2)
        . chr(1)
        . "\0\0"
        . pack("N", $lifetime)
        . ipv4_to_ipv4_mapped_ipv6($local_ip)
        . $nonce
        . chr(6)
        . "\0\0\0"
        . pack("n", $internal_port)
        . pack("n", $external_port)
        . str_repeat("\0", 16);
}

function parse_pcp_map_response($response, $internal_port, $nonce) {
    if(!is_string($response) || strlen($response) < 60)
        throw new RuntimeException("PCP MAP response is incomplete.");

    if(ord($response[0]) !== 2 || (ord($response[1]) & 0x80) === 0 || (ord($response[1]) & 0x7f) !== 1)
        throw new RuntimeException("PCP MAP response has an unexpected version or opcode.");

    $result_code = ord($response[3]);

    if($result_code !== 0)
        throw new RuntimeException("PCP MAP response returned result code {$result_code}.");

    $lifetime = unpack("Nvalue", substr($response, 4, 4))["value"];
    $response_nonce = substr($response, 24, 12);
    $protocol = ord($response[36]);
    $response_internal_port = unpack("nvalue", substr($response, 40, 2))["value"];
    $external_port = unpack("nvalue", substr($response, 42, 2))["value"];

    if(!hash_equals($nonce, $response_nonce) || $protocol !== 6 || $response_internal_port !== $internal_port)
        throw new RuntimeException("PCP MAP response does not match the request.");

    if($external_port < 1 || $external_port > 65535)
        throw new RuntimeException("PCP MAP response returned an invalid external port.");

    return [
        "external_port" => $external_port,
        "external_address" => decode_pcp_ip_address(substr($response, 44, 16)),
        "lifetime" => intval($lifetime),
    ];
}

function request_pcp_tcp_mapping($gateway, $local_ip, $internal_port, $external_port, $lifetime, $nonce = null) {
    if($nonce === null)
        $nonce = random_bytes(12);

    $request = build_pcp_map_request($local_ip, $internal_port, $external_port, $lifetime, $nonce);
    $response = exchange_port_mapping_udp($gateway, $request);

    if($response === null)
        return null;

    $parsed = parse_pcp_map_response($response, $internal_port, $nonce);
    $parsed["nonce"] = $nonce;

    return $parsed;
}

function build_nat_pmp_tcp_mapping_request($internal_port, $external_port, $lifetime) {
    if(!is_int($internal_port) || $internal_port < 1 || $internal_port > 65535)
        throw new InvalidArgumentException("NAT-PMP internal TCP port is invalid.");

    if(!is_int($external_port) || $external_port < 0 || $external_port > 65535)
        throw new InvalidArgumentException("NAT-PMP external TCP port is invalid.");

    if(!is_int($lifetime) || $lifetime < 0)
        throw new InvalidArgumentException("NAT-PMP mapping lifetime is invalid.");

    return "\0\x02\0\0"
        . pack("n", $internal_port)
        . pack("n", $external_port)
        . pack("N", $lifetime);
}

function parse_nat_pmp_tcp_mapping_response($response, $internal_port) {
    if(!is_string($response) || strlen($response) < 16)
        throw new RuntimeException("NAT-PMP TCP mapping response is incomplete.");

    if(ord($response[0]) !== 0 || ord($response[1]) !== 130)
        throw new RuntimeException("NAT-PMP TCP mapping response has an unexpected version or opcode.");

    $result_code = unpack("nvalue", substr($response, 2, 2))["value"];

    if($result_code !== 0)
        throw new RuntimeException("NAT-PMP TCP mapping response returned result code {$result_code}.");

    $response_internal_port = unpack("nvalue", substr($response, 8, 2))["value"];
    $external_port = unpack("nvalue", substr($response, 10, 2))["value"];
    $lifetime = unpack("Nvalue", substr($response, 12, 4))["value"];

    if($response_internal_port !== $internal_port || $external_port < 1 || $external_port > 65535)
        throw new RuntimeException("NAT-PMP TCP mapping response does not match the request.");

    return [
        "external_port" => intval($external_port),
        "external_address" => null,
        "lifetime" => intval($lifetime),
    ];
}

function request_nat_pmp_tcp_mapping($gateway, $internal_port, $external_port, $lifetime) {
    $response = exchange_port_mapping_udp(
        $gateway,
        build_nat_pmp_tcp_mapping_request($internal_port, $external_port, $lifetime)
    );

    if($response === null)
        return null;

    return parse_nat_pmp_tcp_mapping_response($response, $internal_port);
}

function parse_http_headers_block($response) {
    if(!is_string($response))
        return [];

    $headers = [];

    foreach(preg_split('/\r\n|\n|\r/', $response) as $line) {
        $separator = strpos($line, ":");

        if($separator === false)
            continue;

        $name = strtolower(trim(substr($line, 0, $separator)));
        $value = trim(substr($line, $separator + 1));

        if($name !== "")
            $headers[$name] = $value;
    }

    return $headers;
}

function get_upnp_ssdp_search_targets() {
    return [
        "urn:schemas-upnp-org:device:InternetGatewayDevice:2",
        "urn:schemas-upnp-org:device:InternetGatewayDevice:1",
        "urn:schemas-upnp-org:service:WANIPConnection:2",
        "urn:schemas-upnp-org:service:WANIPConnection:1",
        "urn:schemas-upnp-org:service:WANPPPConnection:1",
        "upnp:rootdevice",
        "ssdp:all",
    ];
}

function build_upnp_ssdp_search_request($search_target, $mx_seconds = PORT_MAPPING_UPNP_MX_SECONDS) {
    if(!is_string($search_target) || trim($search_target) === "")
        throw new InvalidArgumentException("UPnP SSDP search target must be non-empty.");

    if(!is_int($mx_seconds) || $mx_seconds < 1 || $mx_seconds > 5)
        throw new InvalidArgumentException("UPnP SSDP MX must be between 1 and 5 seconds.");

    return "M-SEARCH * HTTP/1.1\r\n"
        . "HOST: 239.255.255.250:1900\r\n"
        . "MAN: \"ssdp:discover\"\r\n"
        . "MX: {$mx_seconds}\r\n"
        . "ST: {$search_target}\r\n\r\n";
}

function discover_upnp_igd_devices(
    $timeout = PORT_MAPPING_UPNP_DISCOVERY_TIMEOUT,
    $local_ip = null,
    $search_targets = null,
    $repetitions = PORT_MAPPING_UPNP_SEARCH_REPETITIONS
) {
    if(
        (!is_int($timeout) && !is_float($timeout))
        || !is_finite(floatval($timeout))
        || $timeout <= 0
    )
        throw new InvalidArgumentException("UPnP discovery timeout must be finite and positive.");

    if($local_ip !== null && filter_var($local_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false)
        throw new InvalidArgumentException("UPnP discovery local IPv4 address is invalid.");

    if($search_targets === null)
        $search_targets = get_upnp_ssdp_search_targets();

    if(!is_array($search_targets) || $search_targets === [])
        throw new InvalidArgumentException("UPnP discovery requires at least one SSDP search target.");

    if(!is_int($repetitions) || $repetitions < 1 || $repetitions > 4)
        throw new InvalidArgumentException("UPnP SSDP search repetitions are invalid.");

    $error_number = 0;
    $error_message = "";
    $bind_host = $local_ip ?? "0.0.0.0";
    $socket = @stream_socket_server(
        "udp://{$bind_host}:0",
        $error_number,
        $error_message,
        STREAM_SERVER_BIND
    );

    if($socket === false)
        return [];

    try {
        stream_set_blocking($socket, false);

        for($repetition = 0; $repetition < $repetitions; $repetition++) {
            foreach($search_targets as $search_target) {
                if(!is_string($search_target) || trim($search_target) === "")
                    continue;

                $request = build_upnp_ssdp_search_request($search_target);
                @stream_socket_sendto($socket, $request, 0, "239.255.255.250:1900");
            }

            if($repetition + 1 < $repetitions)
                usleep(100000);
        }

        // MX is a maximum random response delay, so the receive window must extend
        // beyond MX itself rather than end before a compliant gateway may answer.
        $minimum_receive_window = PORT_MAPPING_UPNP_MX_SECONDS + 0.50;
        $deadline = microtime(true) + max(floatval($timeout), $minimum_receive_window);
        $devices = [];

        while(microtime(true) < $deadline) {
            $read = [$socket];
            $write = null;
            $except = null;
            $remaining = max(0.0, $deadline - microtime(true));
            $seconds = intval(floor($remaining));
            $microseconds = intval(round(($remaining - $seconds) * 1000000));
            $selected = @stream_select($read, $write, $except, $seconds, $microseconds);

            if($selected === false)
                break;

            if($selected === 0)
                continue;

            $source = null;
            $response = @stream_socket_recvfrom($socket, 65535, 0, $source);

            if(!is_string($response) || $response === "")
                continue;

            $headers = parse_http_headers_block($response);
            $location = trim(strval($headers["location"] ?? ""));

            if($location === "" || preg_match('/\Ahttps?:\/\//i', $location) !== 1)
                continue;

            $key = strtolower($location);

            if(!isset($devices[$key])) {
                $devices[$key] = [
                    "location_url" => $location,
                    "source" => is_string($source) ? $source : null,
                    "search_target" => trim(strval($headers["st"] ?? "")),
                    "usn" => trim(strval($headers["usn"] ?? "")),
                    "server" => trim(strval($headers["server"] ?? "")),
                ];
            }
        }

        return array_values($devices);
    } finally {
        fclose($socket);
    }
}

function discover_upnp_igd_locations(
    $timeout = PORT_MAPPING_UPNP_DISCOVERY_TIMEOUT,
    $local_ip = null,
    $search_targets = null,
    $repetitions = PORT_MAPPING_UPNP_SEARCH_REPETITIONS
) {
    return array_map(
        static fn($device) => $device["location_url"],
        discover_upnp_igd_devices($timeout, $local_ip, $search_targets, $repetitions)
    );
}

function discover_upnp_igd_location($timeout = PORT_MAPPING_UPNP_DISCOVERY_TIMEOUT, $local_ip = null) {
    $locations = discover_upnp_igd_locations($timeout, $local_ip);

    return $locations[0] ?? null;
}

function fetch_runtime_http_text($url, $timeout = PORT_MAPPING_HTTP_TIMEOUT) {
    if(!is_string($url) || preg_match('/\Ahttps?:\/\//i', $url) !== 1)
        throw new InvalidArgumentException("Runtime HTTP URL is invalid.");

    $context = stream_context_create([
        "http" => [
            "timeout" => floatval($timeout),
            "follow_location" => 1,
            "max_redirects" => 3,
            "header" => "User-Agent: GreedyBitTorrentClient/1.0\r\n",
        ],
    ]);

    $body = @file_get_contents($url, false, $context);

    return is_string($body) ? $body : null;
}

function normalise_runtime_url_path($path) {
    if(!is_string($path) || $path === "")
        return "/";

    $leading_slash = str_starts_with($path, "/");
    $trailing_slash = str_ends_with($path, "/") || preg_match("~/(?:\.|\.\.)$~", $path) === 1;
    $segments = [];

    foreach(explode("/", $path) as $segment) {
        if($segment === "" || $segment === ".")
            continue;

        if($segment === "..") {
            array_pop($segments);
            continue;
        }

        $segments[] = $segment;
    }

    $normalised = ($leading_slash ? "/" : "") . implode("/", $segments);

    if($normalised === "")
        $normalised = $leading_slash ? "/" : ".";

    if($trailing_slash && $normalised !== "/")
        $normalised .= "/";

    return $normalised;
}

function resolve_runtime_url($base_url, $reference) {
    if(!is_string($base_url) || !is_string($reference))
        throw new InvalidArgumentException("Runtime URL resolution requires valid URLs.");

    $reference = explode("#", trim($reference), 2)[0];

    if(preg_match('/\Ahttps?:\/\//i', $reference) === 1)
        return $reference;

    $base = parse_url($base_url);

    if(!is_array($base) || !isset($base["scheme"], $base["host"]))
        throw new InvalidArgumentException("Runtime base URL is invalid.");

    if(str_starts_with($reference, "//"))
        return $base["scheme"] . ":" . $reference;

    /*
     * UPnP controlURL values are URI references, not standalone URLs.  Paths
     * such as /WANIPConnection:1 are valid, but PHP parse_url() rejects them
     * because the colon is interpreted as URL syntax.  Parse the relative
     * reference ourselves so the router's advertised path is preserved.
     */
    $fragment_position = strpos($reference, "#");

    if($fragment_position !== false)
        $reference = substr($reference, 0, $fragment_position);

    $query = null;
    $query_position = strpos($reference, "?");

    if($query_position !== false) {
        $query = substr($reference, $query_position + 1);
        $reference_path = substr($reference, 0, $query_position);
    } else {
        $reference_path = $reference;
    }

    $authority = $base["scheme"] . "://" . $base["host"];

    if(isset($base["port"]))
        $authority .= ":" . $base["port"];

    if($reference_path === "") {
        $path = strval($base["path"] ?? "/");
        $query ??= $base["query"] ?? null;
    } elseif(str_starts_with($reference_path, "/")) {
        $path = normalise_runtime_url_path($reference_path);
    } else {
        $base_path = strval($base["path"] ?? "/");
        $directory = preg_replace('/[^\/]*$/', "", $base_path);
        $path = normalise_runtime_url_path($directory . $reference_path);
    }

    $resolved = $authority . $path;

    if($query !== null)
        $resolved .= "?" . $query;

    return $resolved;
}

function parse_upnp_igd_control_services($device_description, $location_url) {
    if(!is_string($device_description) || !is_string($location_url))
        return [];

    if(preg_match('/\Ahttps?:\/\//i', $location_url) !== 1)
        return [];

    $advertised_url_base = null;
    $resolution_base_url = $location_url;

    if(preg_match('/<URLBase\b[^>]*>\s*([^<]+)\s*<\/URLBase>/i', $device_description, $base_match) === 1) {
        $candidate_base = html_entity_decode(trim($base_match[1]), ENT_QUOTES | ENT_XML1, "UTF-8");

        $advertised_url_base = $candidate_base;

        try {
            $resolution_base_url = resolve_runtime_url($location_url, $candidate_base);
        } catch(Throwable) {
            $resolution_base_url = $location_url;
        }
    }

    $service_count = preg_match_all('/<service\b[^>]*>(.*?)<\/service>/is', $device_description, $service_matches);

    if($service_count === false || empty($service_matches[1]))
        return [];

    $preferred = [];
    $seen = [];

    foreach($service_matches[1] as $service_xml) {
        if(preg_match('/<serviceType\b[^>]*>\s*([^<]+)\s*<\/serviceType>/i', $service_xml, $type_match) !== 1)
            continue;

        if(preg_match('/<controlURL\b[^>]*>\s*([^<]+)\s*<\/controlURL>/i', $service_xml, $control_match) !== 1)
            continue;

        $service_type = html_entity_decode(trim($type_match[1]), ENT_QUOTES | ENT_XML1, "UTF-8");

        if(!preg_match('/:(WANIPConnection|WANPPPConnection):[12]\z/i', $service_type))
            continue;

        $advertised_control_url = html_entity_decode(trim($control_match[1]), ENT_QUOTES | ENT_XML1, "UTF-8");

        if($advertised_control_url === "")
            continue;

        try {
            $resolved_control_url = resolve_runtime_url(
                preg_match("~^(?:[a-z][a-z0-9+.-]*:)?//~i", $advertised_control_url) === 1
                    ? $location_url
                    : $resolution_base_url,
                $advertised_control_url
            );
        } catch(Throwable) {
            continue;
        }

        if(preg_match('/\Ahttps?:\/\//i', $resolved_control_url) !== 1)
            continue;

        $dedupe_key = strtolower($service_type . "\n" . $resolved_control_url);

        if(isset($seen[$dedupe_key]))
            continue;

        $seen[$dedupe_key] = true;
        $rank = stripos($service_type, "WANIPConnection:2") !== false ? 0
            : (stripos($service_type, "WANIPConnection") !== false ? 1 : 2);
        $preferred[] = [
            "service_type" => $service_type,
            "control_url" => $resolved_control_url,
            "advertised_control_url" => $advertised_control_url,
            "url_base" => $advertised_url_base,
            "resolution_base_url" => $resolution_base_url,
            "rank" => $rank,
            "location_url" => $location_url,
        ];
    }

    usort($preferred, static function($left, $right) {
        $rank_comparison = $left["rank"] <=> $right["rank"];

        if($rank_comparison !== 0)
            return $rank_comparison;

        return strcmp($left["control_url"], $right["control_url"]);
    });

    return $preferred;
}

function parse_upnp_igd_control_service($device_description, $location_url) {
    $services = parse_upnp_igd_control_services($device_description, $location_url);

    return $services[0] ?? null;
}

function build_upnp_soap_envelope($service_type, $action, $arguments) {
    $argument_xml = "";

    foreach($arguments as $name => $value) {
        $encoded_name = htmlspecialchars(strval($name), ENT_XML1 | ENT_QUOTES, "UTF-8");
        $encoded_value = htmlspecialchars(strval($value), ENT_XML1 | ENT_QUOTES, "UTF-8");
        $argument_xml .= "<{$encoded_name}>{$encoded_value}</{$encoded_name}>";
    }

    $encoded_service_type = htmlspecialchars($service_type, ENT_XML1 | ENT_QUOTES, "UTF-8");
    $encoded_action = htmlspecialchars($action, ENT_XML1 | ENT_QUOTES, "UTF-8");

    return <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
 s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body><u:{$encoded_action} xmlns:u="{$encoded_service_type}">{$argument_xml}</u:{$encoded_action}></s:Body>
</s:Envelope>
XML;
}

function build_upnp_raw_http_request($control_url, $service_type, $action, $body) {
    if(!is_string($control_url) || trim($control_url) === "")
        throw new InvalidArgumentException("UPnP SOAP control URL must be non-empty.");

    if(!is_string($service_type) || trim($service_type) === "")
        throw new InvalidArgumentException("UPnP SOAP service type must be non-empty.");

    if(!is_string($action) || trim($action) === "")
        throw new InvalidArgumentException("UPnP SOAP action must be non-empty.");

    if(!is_string($body) || $body === "")
        throw new InvalidArgumentException("UPnP SOAP body must be non-empty.");

    $parts = parse_url($control_url);

    if(
        !is_array($parts)
        || strtolower(strval($parts["scheme"] ?? "")) !== "http"
        || !isset($parts["host"])
    )
        throw new InvalidArgumentException("UPnP SOAP control URL must use HTTP.");

    $host = strval($parts["host"]);
    $port = intval($parts["port"] ?? 80);
    $path = strval($parts["path"] ?? "/");

    if($path === "")
        $path = "/";

    if(isset($parts["query"]) && strval($parts["query"]) !== "")
        $path .= "?" . strval($parts["query"]);

    $host_header = str_contains($host, ":") ? "[{$host}]" : $host;

    $host_header .= ":{$port}";
    $length = strlen($body);
    $headers = "POST {$path} HTTP/1.1\r\nHost: {$host_header}\r\nConnection: close\r\n";
    $headers .= "Content-Type: text/xml; charset=\"utf-8\"\r\n";
    $headers .= "SOAPAction: \"{$service_type}#{$action}\"\r\nContent-Length: {$length}\r\n\r\n";

    return [
        "host" => $host,
        "port" => $port,
        "path" => $path,
        "request" => $headers . $body,
        "body" => $body,
    ];
}

function decode_upnp_http_chunked_body($body) {
    if(!is_string($body))
        return null;

    $offset = 0;
    $decoded = "";
    $length = strlen($body);

    while($offset < $length) {
        $line_end = strpos($body, "\r\n", $offset);

        if($line_end === false)
            return null;

        $size_text = trim(substr($body, $offset, $line_end - $offset));
        $semicolon = strpos($size_text, ";");

        if($semicolon !== false)
            $size_text = substr($size_text, 0, $semicolon);

        if($size_text === "" || preg_match('/^[0-9a-fA-F]+$/', $size_text) !== 1)
            return null;

        $chunk_size = hexdec($size_text);
        $offset = $line_end + 2;

        if($chunk_size === 0)
            return $decoded;

        if($offset + $chunk_size + 2 > $length)
            return null;

        $decoded .= substr($body, $offset, $chunk_size);
        $offset += $chunk_size;

        if(substr($body, $offset, 2) !== "\r\n")
            return null;

        $offset += 2;
    }

    return null;
}

function parse_upnp_raw_http_response($response) {
    if(!is_string($response) || $response === "")
        return [
            "status_code" => null,
            "headers" => [],
            "body" => "",
            "complete" => false,
        ];

    $header_end = strpos($response, "\r\n\r\n");

    if($header_end === false)
        return [
            "status_code" => null,
            "headers" => [],
            "body" => $response,
            "complete" => false,
        ];

    $header_text = substr($response, 0, $header_end);
    $body = substr($response, $header_end + 4);
    $lines = explode("\r\n", $header_text);
    $status_line = array_shift($lines);
    $status_code = null;

    if(is_string($status_line) && preg_match('/^HTTP\/[0-9.]+\s+([0-9]{3})(?:\s|$)/i', $status_line, $matches) === 1)
        $status_code = intval($matches[1]);

    if($status_code !== null && $status_code >= 100 && $status_code < 200 && $status_code !== 101)
        return parse_upnp_raw_http_response($body);

    $headers = [];

    foreach($lines as $line) {
        $colon = strpos($line, ":");

        if($colon === false)
            continue;

        $name = strtolower(trim(substr($line, 0, $colon)));
        $value = trim(substr($line, $colon + 1));

        if($name === "")
            continue;

        if(isset($headers[$name]))
            $headers[$name] .= ", " . $value;
        else
            $headers[$name] = $value;
    }

    $complete = null;

    if(
        isset($headers["transfer-encoding"])
        && str_contains(strtolower($headers["transfer-encoding"]), "chunked")
    ) {
        $decoded = decode_upnp_http_chunked_body($body);
        $complete = $decoded !== null;

        if($decoded !== null)
            $body = $decoded;
    } elseif(isset($headers["content-length"])) {
        $content_length = intval($headers["content-length"]);
        $complete = ctype_digit($headers["content-length"])
            && strlen($body) >= $content_length;

        if($complete)
            $body = substr($body, 0, $content_length);
    }

    return [
        "status_code" => $status_code,
        "headers" => $headers,
        "body" => $body,
        "complete" => $complete,
    ];
}

function write_upnp_raw_http_request($socket, $request) {
    $length = strlen($request);
    $written = @fwrite($socket, $request);

    if($written === false || $written < 1)
        return false;

    $offset = $written;

    while($offset < $length) {
        $written = @fwrite($socket, substr($request, $offset));

        if($written === false || $written < 1)
            return false;

        $offset += $written;
    }

    return true;
}

function parse_upnp_soap_value($body, $name) {
    $name = preg_quote($name, "~");

    if(preg_match(
        "~<(?:[a-zA-Z_][a-zA-Z0-9_.-]*:)?{$name}\b[^>]*>\s*([^<]*)</(?:[a-zA-Z_][a-zA-Z0-9_.-]*:)?{$name}\s*>~i",
        $body,
        $match
    ) !== 1)
        return null;

    return trim(html_entity_decode($match[1], ENT_QUOTES | ENT_XML1, "UTF-8"));
}

function parse_upnp_soap_fault($body) {
    if(!is_string($body) || $body === "")
        return null;

    $code = parse_upnp_soap_value($body, "errorCode");
    $description = parse_upnp_soap_value($body, "errorDescription");

    if($code === null && $description === null)
        return null;

    return [
        "code" => $code,
        "description" => $description,
    ];
}

function send_upnp_soap_action_detailed($control_url, $service_type, $action, $arguments) {
    $body = build_upnp_soap_envelope($service_type, $action, $arguments);
    $request = build_upnp_raw_http_request($control_url, $service_type, $action, $body);
    $transport_error = null;
    $errno = 0;
    $errstr = "";

    set_error_handler(static function($severity, $message) use (&$transport_error) {
        $transport_error = strval($message);

        return true;
    });

    try {
        $socket_host = str_contains($request["host"], ":") ? "[{$request["host"]}]" : $request["host"];
        $socket = stream_socket_client(
            "tcp://{$socket_host}:{$request["port"]}",
            $errno,
            $errstr,
            floatval(PORT_MAPPING_UPNP_CONNECT_TIMEOUT),
            STREAM_CLIENT_CONNECT
        );
    } finally {
        restore_error_handler();
    }

    if(!is_resource($socket)) {
        if($transport_error === null || $transport_error === "")
            $transport_error = trim($errstr) !== "" ? trim($errstr) : "TCP connection failed";

        return [
            "ok" => false,
            "status_code" => null,
            "body" => "",
            "fault" => null,
            "transport_error" => $transport_error,
        ];
    }

    stream_set_timeout(
        $socket,
        intval(floor(floatval(PORT_MAPPING_SOAP_TIMEOUT))),
        intval((floatval(PORT_MAPPING_SOAP_TIMEOUT) - floor(floatval(PORT_MAPPING_SOAP_TIMEOUT))) * 1000000)
    );

    if(!write_upnp_raw_http_request($socket, $request["request"])) {
        fclose($socket);

        return [
            "ok" => false,
            "status_code" => null,
            "body" => "",
            "fault" => null,
            "transport_error" => "failed to write complete SOAP request",
        ];
    }

    $response = "";
    $parsed = null;

    while(!feof($socket)) {
        $chunk = @fread($socket, 8192);

        if($chunk === false) {
            $transport_error = "failed while reading SOAP response";
            break;
        }

        if($chunk !== "")
            $response .= $chunk;

        if(strlen($response) > PORT_MAPPING_UPNP_MAX_RESPONSE_BYTES) {
            $transport_error = "SOAP response exceeds the size limit";
            break;
        }

        $parsed = parse_upnp_raw_http_response($response);

        if($parsed["complete"] === true)
            break;

        $meta = stream_get_meta_data($socket);

        if(!empty($meta["timed_out"])) {
            $transport_error = "SOAP response timed out";
            break;
        }

        if($chunk === "" && !empty($meta["eof"]))
            break;
    }

    fclose($socket);

    $parsed ??= parse_upnp_raw_http_response($response);
    $status_code = $parsed["status_code"];
    $response_body = strval($parsed["body"] ?? "");
    $fault = parse_upnp_soap_fault($response_body);

    if($transport_error === null && $status_code === null)
        $transport_error = "router closed the connection; no HTTP status line was received";
    elseif($transport_error === null && $parsed["complete"] === false)
        $transport_error = "router response ended before the complete SOAP body was received";

    $response_action = preg_quote($action, "~");
    $has_response = preg_match(
        "~<(?:[a-zA-Z_][a-zA-Z0-9_.-]*:)?{$response_action}Response(?:\s|/?>)~",
        $response_body
    ) === 1;

    return [
        "ok" => $transport_error === null && $status_code === 200 && $fault === null && $has_response,
        "status_code" => $status_code,
        "body" => $response_body,
        "fault" => $fault,
        "transport_error" => $transport_error,
    ];
}

function send_upnp_soap_action($control_url, $service_type, $action, $arguments) {
    $result = send_upnp_soap_action_detailed($control_url, $service_type, $action, $arguments);

    return $result["ok"] ? $result["body"] : null;
}

function parse_upnp_external_ip_address($body) {
    if(!is_string($body) || $body === "")
        return null;

    $address = parse_upnp_soap_value($body, "NewExternalIPAddress");

    return filter_var($address, FILTER_VALIDATE_IP) !== false ? $address : null;
}

function probe_upnp_connected_service($service, $log_stream, $soap_sender) {
    // Select a connected, NAT-enabled WAN service before attempting to change a mapping.
    foreach([
        "GetStatusInfo" => ["NewConnectionStatus", ["connected"]],
        "GetNATRSIPStatus" => ["NewNATEnabled", ["1", "true"]],
    ] as $action => [$field, $accepted_values]) {
        $result = $soap_sender($service["control_url"], $service["service_type"], $action, []);
        $value = is_array($result) && !empty($result["ok"])
            ? parse_upnp_soap_value(strval($result["body"] ?? ""), $field)
            : null;

        if($value !== null && in_array(strtolower($value), $accepted_values, true))
            continue;

        if($log_stream !== null)
            log_message(
                "UPnP mapping: {$action} did not confirm a connected, NAT-enabled WAN at {$service["control_url"]}.",
                $log_stream
            );

        return false;
    }

    return true;
}

function probe_upnp_external_ip_address($service, $log_stream = null, $soap_sender = null) {
    if(!is_array($service) || !isset($service["control_url"], $service["service_type"]))
        throw new InvalidArgumentException("UPnP external-address probe requires a control service.");

    if($soap_sender === null)
        $soap_sender = "send_upnp_soap_action_detailed";

    if(!is_callable($soap_sender))
        throw new InvalidArgumentException("UPnP external-address probe SOAP sender must be callable.");

    $result = $soap_sender(
        $service["control_url"],
        $service["service_type"],
        "GetExternalIPAddress",
        []
    );
    $address = is_array($result) && !empty($result["ok"])
        ? parse_upnp_external_ip_address(strval($result["body"] ?? ""))
        : null;

    if($log_stream !== null) {
        if($address !== null) {
            log_message(
                "UPnP mapping: SOAP control probe succeeded at {$service["control_url"]}; external address {$address}.",
                $log_stream
            );
        } else {
            $status = is_array($result) && ($result["status_code"] ?? null) !== null
                ? "HTTP " . intval($result["status_code"])
                : "no HTTP status";
            $transport_error = is_array($result) ? trim(strval($result["transport_error"] ?? "")) : "";
            $transport_text = $transport_error !== "" ? "; transport {$transport_error}" : "";
            log_message(
                "UPnP mapping: SOAP control probe failed at {$service["control_url"]}: {$status}{$transport_text}.",
                $log_stream
            );
        }
    }

    return $address;
}

function parse_upnp_specific_port_mapping_entry($body) {
    if(!is_string($body) || $body === "")
        return null;

    $fields = [];

    foreach(["NewInternalClient", "NewInternalPort", "NewEnabled", "NewPortMappingDescription", "NewLeaseDuration"] as $name) {
        $value = parse_upnp_soap_value($body, $name);

        if($value !== null)
            $fields[$name] = $value;
    }

    if(!isset($fields["NewInternalClient"], $fields["NewInternalPort"]))
        return null;

    return [
        "internal_client" => $fields["NewInternalClient"],
        "internal_port" => intval($fields["NewInternalPort"]),
        "enabled" => isset($fields["NewEnabled"])
            ? intval(in_array(strtolower($fields["NewEnabled"]), ["1", "true"], true))
            : null,
        "description" => $fields["NewPortMappingDescription"] ?? null,
        "lease_duration" => isset($fields["NewLeaseDuration"]) ? intval($fields["NewLeaseDuration"]) : null,
    ];
}

function verify_upnp_tcp_mapping_service(
    $external_port,
    $internal_port,
    $local_ip,
    $service,
    $soap_sender,
    $log_stream = null,
    &$entry = null
) {
    $result = $soap_sender(
        $service["control_url"],
        $service["service_type"],
        "GetSpecificPortMappingEntry",
        [
            "NewRemoteHost" => "",
            "NewExternalPort" => $external_port,
            "NewProtocol" => "TCP",
        ]
    );

    $entry = is_array($result) && !empty($result["ok"])
        ? parse_upnp_specific_port_mapping_entry(strval($result["body"] ?? ""))
        : null;

    $verified = is_array($entry)
        && $entry["internal_port"] === intval($internal_port)
        && $entry["internal_client"] === strval($local_ip)
        && $entry["enabled"] === 1;

    if($log_stream !== null) {
        if($verified) {
            $lease_text = $entry["lease_duration"] !== null ? "; lease {$entry["lease_duration"]}s" : "";
            log_message(
                "UPnP mapping: GetSpecificPortMappingEntry verified external TCP {$external_port} -> {$local_ip}:{$internal_port}{$lease_text}.",
                $log_stream
            );
        } else {
            $status = is_array($result) && ($result["status_code"] ?? null) !== null
                ? "HTTP " . intval($result["status_code"])
                : "no HTTP status";
            $fault = is_array($result) ? ($result["fault"] ?? null) : null;
            $fault_text = "";
            $transport_error = is_array($result) ? trim(strval($result["transport_error"] ?? "")) : "";
            $transport_text = $transport_error !== "" ? "; transport {$transport_error}" : "";

            if(is_array($fault)) {
                $code = $fault["code"] ?? "unknown";
                $description = $fault["description"] ?? "unspecified";
                $fault_text = "; UPnP fault {$code} ({$description})";
            }

            if(is_array($entry))
                $fault_text .= "; router returned {$entry["internal_client"]}:{$entry["internal_port"]}";

            log_message(
                "UPnP mapping: GetSpecificPortMappingEntry verification failed for TCP {$external_port}: {$status}{$fault_text}{$transport_text}.",
                $log_stream
            );
        }
    }

    return $verified;
}

function attempt_upnp_tcp_mapping_service(
    $internal_port,
    $external_port,
    $local_ip,
    $lifetime,
    $service,
    $log_stream,
    $soap_sender,
    $external_address = null
) {
    // Reuse an enabled matching rule, and never overwrite a conflicting mapping.
    $result = $soap_sender(
        $service["control_url"],
        $service["service_type"],
        "GetSpecificPortMappingEntry",
        [
            "NewRemoteHost" => "",
            "NewExternalPort" => $external_port,
            "NewProtocol" => "TCP",
        ]
    );
    $entry = is_array($result) && !empty($result["ok"])
        ? parse_upnp_specific_port_mapping_entry(strval($result["body"] ?? ""))
        : null;

    if($entry !== null) {
        if($entry["internal_client"] !== strval($local_ip)
            || $entry["internal_port"] !== intval($internal_port)
            || $entry["enabled"] !== 1) {
            if($log_stream !== null)
                log_message(
                    "UPnP mapping: external TCP {$external_port} already has a different or disabled rule; it was not replaced.",
                    $log_stream
                );

            return null;
        }

        if($log_stream !== null)
            log_message(
                "UPnP mapping: enabled mapping already exists for external TCP {$external_port} -> {$local_ip}:{$internal_port}.",
                $log_stream
            );

        return [
            "external_port" => $external_port,
            "external_address" => $external_address,
            "lifetime" => max(0, intval($entry["lease_duration"] ?? 0)),
            "service" => $service,
        ];
    }

    if(!is_array($result)
        || !empty($result["transport_error"])
        || intval($result["fault"]["code"] ?? 0) !== 714) {
        $reason = is_array($result)
            ? trim(strval($result["transport_error"] ?? $result["fault"]["description"] ?? "unexpected SOAP response"))
            : "missing SOAP response";

        throw new RuntimeException("UPnP mapping preflight failed at {$service["control_url"]}: {$reason}; no mapping was changed.");
    }

    // Add once with a permanent lease, then read back even if the action reply was lost.
    $result = $soap_sender(
        $service["control_url"],
        $service["service_type"],
        "AddPortMapping",
        [
            "NewRemoteHost" => "",
            "NewExternalPort" => $external_port,
            "NewProtocol" => "TCP",
            "NewInternalPort" => $internal_port,
            "NewInternalClient" => $local_ip,
            "NewEnabled" => 1,
            "NewPortMappingDescription" => PORT_MAPPING_DESCRIPTION . " at {$internal_port}",
            "NewLeaseDuration" => 0,
        ]
    );

    if(!is_array($result) || empty($result["ok"])) {
        if($log_stream !== null) {
            $status = is_array($result) && ($result["status_code"] ?? null) !== null
                ? "HTTP " . intval($result["status_code"])
                : "no HTTP status";
            $fault = is_array($result) ? ($result["fault"] ?? null) : null;
            $fault_text = "";
            $transport_error = is_array($result) ? trim(strval($result["transport_error"] ?? "")) : "";
            $transport_text = $transport_error !== "" ? "; transport {$transport_error}" : "";

            if(is_array($fault)) {
                $code = $fault["code"] ?? "unknown";
                $fault_description = $fault["description"] ?? "unspecified";
                $fault_text = "; UPnP fault {$code} ({$fault_description})";
            }

            log_message(
                "UPnP mapping: {$service["service_type"]} at {$service["control_url"]} AddPortMapping reply was not confirmed: {$status}{$fault_text}{$transport_text}.",
                $log_stream
            );
        }

        if(is_array($result) && (is_array($result["fault"] ?? null) || intval($result["status_code"] ?? 0) >= 400))
            return null;
    }

    $entry = null;

    if(!verify_upnp_tcp_mapping_service(
        $external_port,
        $internal_port,
        $local_ip,
        $service,
        $soap_sender,
        $log_stream,
        $entry
    ))
        throw new RuntimeException(
            "UPnP mapping could not be confirmed after AddPortMapping at {$service["control_url"]}; no add retry was made."
        );

    return [
        "external_port" => $external_port,
        "external_address" => $external_address,
        "lifetime" => max(0, intval($entry["lease_duration"] ?? 0)),
        "service" => $service,
    ];
}

function request_upnp_tcp_mapping(
    $internal_port,
    $external_port,
    $fallback_local_ip,
    $lifetime,
    $service = null,
    $log_stream = null,
    $soap_sender = null,
    $route_detector = null
) {
    if($log_stream !== null && !is_resource($log_stream))
        throw new InvalidArgumentException("UPnP mapping log stream is invalid.");

    if($soap_sender === null)
        $soap_sender = "send_upnp_soap_action_detailed";

    if(!is_callable($soap_sender))
        throw new InvalidArgumentException("UPnP SOAP sender must be callable.");

    if($route_detector === null)
        $route_detector = "detect_local_ipv4_for_url";

    if(!is_callable($route_detector))
        throw new InvalidArgumentException("UPnP route detector must be callable.");

    if($service !== null) {
        $service_local_ip = resolve_upnp_internal_client_ipv4($service, $fallback_local_ip, $route_detector);

        if($service_local_ip === null) {
            if($log_stream !== null)
                log_message("UPnP mapping: could not determine the LAN IPv4 address used to reach {$service["control_url"]}.", $log_stream);

            return null;
        }

        $service["local_ip"] = $service_local_ip;

        if(!probe_upnp_connected_service($service, $log_stream, $soap_sender))
            return null;

        $external_address = probe_upnp_external_ip_address($service, $log_stream, $soap_sender);

        return attempt_upnp_tcp_mapping_service(
            $internal_port,
            $external_port,
            $service_local_ip,
            $lifetime,
            $service,
            $log_stream,
            $soap_sender,
            $external_address
        );
    }

    $devices = discover_upnp_igd_devices(PORT_MAPPING_UPNP_DISCOVERY_TIMEOUT);

    if($devices === []) {
        if($log_stream !== null)
            log_message("UPnP mapping: SSDP discovery found no Internet Gateway Device description.", $log_stream);

        return null;
    }

    if($log_stream !== null)
        log_message("UPnP mapping: SSDP discovered " . count($devices) . " unique device description location(s).", $log_stream);

    $services = [];
    $seen_services = [];

    foreach($devices as $device) {
        $location = strval($device["location_url"] ?? "");

        if($location === "")
            continue;

        if($log_stream !== null) {
            $source = trim(strval($device["source"] ?? ""));
            $target = trim(strval($device["search_target"] ?? ""));
            $source_text = $source !== "" ? " from {$source}" : "";
            $target_text = $target !== "" ? "; ST {$target}" : "";
            log_message("UPnP mapping: SSDP LOCATION {$location}{$source_text}{$target_text}.", $log_stream);
        }

        $description = fetch_runtime_http_text($location);

        if($description === null) {
            if($log_stream !== null)
                log_message("UPnP mapping: could not fetch advertised device description {$location}.", $log_stream);

            continue;
        }

        $location_services = parse_upnp_igd_control_services($description, $location);

        if($location_services === [] && $log_stream !== null)
            log_message("UPnP mapping: advertised device description {$location} exposes no WANIPConnection/WANPPPConnection service.", $log_stream);

        foreach($location_services as $candidate) {
            $key = strtolower($candidate["service_type"] . "\n" . $candidate["control_url"]);

            if(isset($seen_services[$key]))
                continue;

            $service_local_ip = resolve_upnp_internal_client_ipv4($candidate, $fallback_local_ip, $route_detector);

            if($service_local_ip === null) {
                if($log_stream !== null)
                    log_message("UPnP mapping: could not determine route-local IPv4 address for advertised control URL {$candidate["control_url"]}.", $log_stream);

                continue;
            }

            $candidate["local_ip"] = $service_local_ip;
            $seen_services[$key] = true;
            $services[] = $candidate;

            if($log_stream !== null) {
                $base_text = $candidate["url_base"] !== null
                    ? "URLBase {$candidate["url_base"]}"
                    : "LOCATION base {$candidate["location_url"]}";
                log_message(
                    "UPnP mapping: advertised {$candidate["service_type"]}; controlURL {$candidate["advertised_control_url"]}; {$base_text}; resolved {$candidate["control_url"]}; internal client {$service_local_ip}.",
                    $log_stream
                );
            }
        }
    }

    if($services === []) {
        if($log_stream !== null)
            log_message("UPnP mapping: device descriptions exposed no usable advertised WAN control service.", $log_stream);

        return null;
    }

    usort($services, static function($left, $right) {
        $rank_comparison = ($left["rank"] ?? 99) <=> ($right["rank"] ?? 99);

        if($rank_comparison !== 0)
            return $rank_comparison;

        return strcmp($left["control_url"], $right["control_url"]);
    });

    if($log_stream !== null)
        log_message("UPnP mapping: trying " . count($services) . " advertised WAN control service(s).", $log_stream);

    foreach($services as $candidate) {
        if($log_stream !== null)
            log_message("UPnP mapping: trying {$candidate["service_type"]} at advertised control endpoint {$candidate["control_url"]} using internal client {$candidate["local_ip"]}.", $log_stream);

        if(!probe_upnp_connected_service($candidate, $log_stream, $soap_sender))
            continue;

        $external_address = probe_upnp_external_ip_address($candidate, $log_stream, $soap_sender);
        $mapping = attempt_upnp_tcp_mapping_service(
            $internal_port,
            $external_port,
            $candidate["local_ip"],
            $lifetime,
            $candidate,
            $log_stream,
            $soap_sender,
            $external_address
        );

        if($mapping !== null)
            return $mapping;
    }

    return null;
}

function delete_upnp_tcp_mapping($external_port, $service) {
    if(!is_array($service) || !isset($service["control_url"], $service["service_type"]))
        return false;

    return send_upnp_soap_action(
        $service["control_url"],
        $service["service_type"],
        "DeletePortMapping",
        [
            "NewRemoteHost" => "",
            "NewExternalPort" => $external_port,
            "NewProtocol" => "TCP",
        ]
    ) !== null;
}

final class RuntimePortMapping {
    public readonly int $internal_port;
    public int $external_port;
    public ?string $external_address;
    public string $method;
    public int $lifetime;
    public bool $mapped;
    public ?float $mapped_at;
    public ?float $renew_at;
    public int $renewal_count = 0;
    public int $renewal_failures = 0;
    public bool $delete_attempted = false;
    public ?bool $delete_succeeded = null;
    private $renew_callback;
    private $delete_callback;
    private bool $closed = false;

    public function __construct(
        $internal_port,
        $external_port = null,
        $external_address = null,
        $method = "none",
        $lifetime = 0,
        $mapped = false,
        $mapped_at = null,
        $renew_callback = null,
        $delete_callback = null
    ) {
        if(!is_int($internal_port) || $internal_port < 1 || $internal_port > 65535)
            throw new InvalidArgumentException("Runtime port mapping internal port is invalid.");

        if($external_port === null)
            $external_port = $internal_port;

        if(!is_int($external_port) || $external_port < 1 || $external_port > 65535)
            throw new InvalidArgumentException("Runtime port mapping external port is invalid.");

        $this->internal_port = $internal_port;
        $this->external_port = $external_port;
        $this->external_address = is_string($external_address) && $external_address !== "" ? $external_address : null;
        $this->method = strval($method);
        $this->lifetime = max(0, intval($lifetime));
        $this->mapped = boolval($mapped);
        $this->mapped_at = $this->mapped ? normalise_peer_time($mapped_at) : null;
        $this->renew_callback = $renew_callback;
        $this->delete_callback = $delete_callback;
        $this->renew_at = $this->mapped && $this->lifetime > 0
            ? $this->mapped_at + max(30.0, $this->lifetime * PORT_MAPPING_RENEW_FRACTION)
            : null;
    }

    public function poll($now = null) {
        if($this->closed || !$this->mapped || $this->renew_at === null)
            return false;

        $now = normalise_peer_time($now);

        if($now < $this->renew_at)
            return false;

        if(!is_callable($this->renew_callback)) {
            $this->renew_at = null;

            return false;
        }

        try {
            $result = ($this->renew_callback)($this);
        } catch(Throwable) {
            $result = null;
        }

        if(!is_array($result) || !isset($result["external_port"])) {
            $this->renewal_failures++;
            $this->renew_at = $now + 60.0;

            return false;
        }

        $this->external_port = intval($result["external_port"]);
        $this->external_address = isset($result["external_address"]) && is_string($result["external_address"])
            ? $result["external_address"]
            : $this->external_address;
        $this->lifetime = max(0, intval($result["lifetime"] ?? $this->lifetime));
        $this->mapped_at = $now;
        $this->renew_at = $this->lifetime > 0
            ? $now + max(30.0, $this->lifetime * PORT_MAPPING_RENEW_FRACTION)
            : null;
        $this->renewal_count++;

        return true;
    }

    public function close() {
        if($this->closed)
            return false;

        $this->closed = true;

        if($this->mapped) {
            $this->delete_attempted = true;

            if(is_callable($this->delete_callback)) {
                try {
                    $this->delete_succeeded = ($this->delete_callback)($this) !== false;
                } catch(Throwable) {
                    $this->delete_succeeded = false;
                }
            } else {
                $this->delete_succeeded = false;
            }
        }

        $this->mapped = false;
        $this->renew_at = null;

        return true;
    }

    public function is_closed() {
        return $this->closed;
    }
}

function attempt_runtime_tcp_port_mapping($internal_port, $log_stream, $providers = null, $now = null) {
    if(!is_int($internal_port) || $internal_port < 1 || $internal_port > 65535)
        throw new InvalidArgumentException("Runtime TCP port mapping requires a valid listening port.");

    if(!is_resource($log_stream))
        throw new InvalidArgumentException("Runtime TCP port mapping requires a log stream.");

    $now = normalise_peer_time($now);

    if($providers === null) {
        $providers = [];
        $local_ip = detect_outbound_local_ipv4();
        $gateway = detect_default_ipv4_gateway();

        if($gateway !== null && $local_ip !== null) {
            $providers["PCP"] = static function() use ($gateway, $local_ip, $internal_port) {
                $nonce = random_bytes(12);
                $result = request_pcp_tcp_mapping(
                    $gateway,
                    $local_ip,
                    $internal_port,
                    $internal_port,
                    PORT_MAPPING_LIFETIME_SECONDS,
                    $nonce
                );

                if($result === null)
                    return null;

                $result["renew_callback"] = static function($mapping) use ($gateway, $local_ip, $internal_port, $nonce) {
                    return request_pcp_tcp_mapping(
                        $gateway,
                        $local_ip,
                        $internal_port,
                        $mapping->external_port,
                        PORT_MAPPING_LIFETIME_SECONDS,
                        $nonce
                    );
                };
                $result["delete_callback"] = static function($mapping) use ($gateway, $local_ip, $internal_port, $nonce) {
                    request_pcp_tcp_mapping($gateway, $local_ip, $internal_port, $mapping->external_port, 0, $nonce);

                    return true;
                };

                return $result;
            };
            $providers["NAT-PMP"] = static function() use ($gateway, $internal_port) {
                $result = request_nat_pmp_tcp_mapping(
                    $gateway,
                    $internal_port,
                    $internal_port,
                    PORT_MAPPING_LIFETIME_SECONDS
                );

                if($result === null)
                    return null;

                $result["renew_callback"] = static function($mapping) use ($gateway, $internal_port) {
                    return request_nat_pmp_tcp_mapping(
                        $gateway,
                        $internal_port,
                        $mapping->external_port,
                        PORT_MAPPING_LIFETIME_SECONDS
                    );
                };
                $result["delete_callback"] = static function($mapping) use ($gateway, $internal_port) {
                    request_nat_pmp_tcp_mapping($gateway, $internal_port, $mapping->external_port, 0);

                    return true;
                };

                return $result;
            };
        }

        $providers["UPnP"] = static function() use ($local_ip, $internal_port, $log_stream) {
            $result = request_upnp_tcp_mapping(
                $internal_port,
                $internal_port,
                $local_ip,
                PORT_MAPPING_LIFETIME_SECONDS,
                null,
                $log_stream
            );

            if($result === null)
                return null;

            $service = $result["service"];
            $service_local_ip = $service["local_ip"] ?? $local_ip;
            $result["renew_callback"] = static function($mapping) use ($service_local_ip, $internal_port, $service, $log_stream) {
                if($mapping->lifetime === 0)
                    return [
                        "external_port" => $mapping->external_port,
                        "external_address" => $mapping->external_address,
                        "lifetime" => 0,
                    ];

                return request_upnp_tcp_mapping(
                    $internal_port,
                    $mapping->external_port,
                    $service_local_ip,
                    PORT_MAPPING_LIFETIME_SECONDS,
                    $service,
                    $log_stream
                );
            };
            $result["delete_callback"] = static function($mapping) use ($service) {
                return delete_upnp_tcp_mapping($mapping->external_port, $service);
            };

            return $result;
        };
    }

    foreach($providers as $method => $provider) {
        if(!is_callable($provider))
            continue;

        try {
            $result = $provider();
        } catch(Throwable $exception) {
            log_message(
                "Automatic router mapping via {$method} failed: {$exception->getMessage()}",
                $log_stream
            );
            $result = null;
        }

        if(!is_array($result) || !isset($result["external_port"]))
            continue;

        $mapping = new RuntimePortMapping(
            $internal_port,
            intval($result["external_port"]),
            $result["external_address"] ?? null,
            strval($method),
            intval($result["lifetime"] ?? PORT_MAPPING_LIFETIME_SECONDS),
            true,
            $now,
            $result["renew_callback"] ?? null,
            $result["delete_callback"] ?? null
        );
        $address = $mapping->external_address !== null ? " on {$mapping->external_address}" : "";
        log_message(
            "Automatic router mapping established via {$mapping->method}: external TCP {$mapping->external_port}{$address} -> local TCP {$mapping->internal_port}.",
            $log_stream
        );

        return $mapping;
    }

    $mapping = new RuntimePortMapping($internal_port);
    log_message(
        "Automatic router mapping unavailable. Inbound peer listener remains active locally; for Internet-reachable inbound peers, forward TCP port {$internal_port} to this machine. Continuing with outbound connectivity.",
        $log_stream
    );

    return $mapping;
}

final class RuntimePeerListener {
    private $socket = null;
    private ?int $port = null;
    private bool $started = false;
    private bool $closed = false;
    private int $accepted_connections = 0;
    private int $rejected_connections = 0;
    private ?float $last_accepted_at = null;

    public function start($port = 0) {
        if($this->started)
            return false;

        if(!is_int($port) || $port < 0 || $port > 65535)
            throw new InvalidArgumentException("Inbound peer listener port must be between 0 and 65535.");

        $error_number = 0;
        $error_message = "";
        $socket = @stream_socket_server(
            "tcp://0.0.0.0:{$port}",
            $error_number,
            $error_message,
            STREAM_SERVER_BIND | STREAM_SERVER_LISTEN
        );

        if($socket === false)
            throw new RuntimeException("Inbound peer TCP listener could not be opened: {$error_message}");

        if(!stream_set_blocking($socket, false)) {
            fclose($socket);

            throw new RuntimeException("Inbound peer TCP listener could not be made non-blocking.");
        }

        $local_name = @stream_socket_get_name($socket, false);

        if(!is_string($local_name)) {
            fclose($socket);

            throw new RuntimeException("Inbound peer TCP listener port could not be determined.");
        }

        $endpoint = parse_socket_host_port($local_name);
        $this->socket = $socket;
        $this->port = $endpoint->port;
        $this->started = true;

        return true;
    }

    public function accept_available($limit = INBOUND_ACCEPT_BURST, $now = null) {
        if(!$this->started || $this->closed || !is_resource($this->socket))
            return [];

        if(!is_int($limit) || $limit < 1)
            throw new InvalidArgumentException("Inbound peer accept limit must be positive.");

        $now = normalise_peer_time($now);
        $accepted = [];

        while(count($accepted) < $limit) {
            $remote_name = null;
            $socket = @stream_socket_accept($this->socket, 0, $remote_name);

            if($socket === false)
                break;

            if(!stream_set_blocking($socket, false)) {
                fclose($socket);
                $this->rejected_connections++;

                continue;
            }

            try {
                $endpoint = parse_socket_host_port($remote_name);
            } catch(Throwable) {
                fclose($socket);
                $this->rejected_connections++;

                continue;
            }

            $accepted[] = [
                "socket" => $socket,
                "endpoint" => $endpoint,
                "accepted_at" => $now,
            ];
            $this->accepted_connections++;
            $this->last_accepted_at = $now;
        }

        return $accepted;
    }

    public function get_port() {
        return $this->port;
    }

    public function get_stats() {
        return [
            "port" => $this->port,
            "accepted_connections" => $this->accepted_connections,
            "rejected_connections" => $this->rejected_connections,
            "last_accepted_at" => $this->last_accepted_at,
        ];
    }

    public function is_closed() {
        return $this->closed;
    }

    public function close() {
        if($this->closed)
            return false;

        if(is_resource($this->socket))
            fclose($this->socket);

        $this->socket = null;
        $this->closed = true;

        return true;
    }

    public function __destruct() {
        $this->close();
    }
}

function runtime_dynamic_inbound_headroom($connections, $now = null) {
    if(!is_array($connections))
        throw new InvalidArgumentException("Dynamic inbound headroom requires a connection list.");

    $now = normalise_peer_time($now);
    $recent_inbound = 0;

    foreach($connections as $connection) {
        if(
            !($connection instanceof PeerConnection)
            || $connection->is_terminal()
            || !$connection->inbound
            || $connection->connect_started_at === null
            || $now - $connection->connect_started_at > INBOUND_HEADROOM_WINDOW
        )
            continue;

        $recent_inbound++;
    }

    return min(INBOUND_HEADROOM_MAX, $recent_inbound);
}

function runtime_outbound_connection_limit($peer_listener = null, $connections = [], $now = null) {
    if(!($peer_listener instanceof RuntimePeerListener) || $peer_listener->is_closed())
        return DESIRED_CONNECTED_PEERS;

    $headroom = runtime_dynamic_inbound_headroom($connections, $now);

    return max(1, DESIRED_CONNECTED_PEERS - $headroom);
}

function rebalance_runtime_inbound_connection_overflow(
    &$connections,
    &$peer_retry_after,
    $selected_download_peer_keys,
    $selected_upload_peer_keys,
    $now = null
) {
    if(!is_array($connections) || !is_array($peer_retry_after))
        throw new InvalidArgumentException("Inbound overflow rebalance state is invalid.");

    if(!is_array($selected_download_peer_keys) || !is_array($selected_upload_peer_keys))
        throw new InvalidArgumentException("Inbound overflow rebalance peer selections are invalid.");

    $now = normalise_peer_time($now);
    $active_connections = [];
    $recent_established_inbound = 0;

    foreach($connections as $connection) {
        if(!($connection instanceof PeerConnection) || $connection->is_terminal())
            continue;

        $active_connections[] = $connection;

        if(
            $connection->inbound
            && $connection->state === PeerConnection::STATE_ESTABLISHED
            && $connection->connect_started_at !== null
            && $now - $connection->connect_started_at <= INBOUND_HEADROOM_WINDOW
        )
            $recent_established_inbound++;
    }

    $overflow = max(0, count($active_connections) - DESIRED_CONNECTED_PEERS);

    if($overflow === 0 || $recent_established_inbound === 0) {
        return [
            "overflow" => $overflow,
            "recent_established_inbound" => $recent_established_inbound,
            "closed_outbound" => 0,
        ];
    }

    $connecting_outbound = [];
    $replaceable_outbound = [];

    foreach($active_connections as $connection) {
        if($connection->inbound)
            continue;

        if(
            $connection->state === PeerConnection::STATE_CONNECTING
            || $connection->state === PeerConnection::STATE_HANDSHAKING
        ) {
            $connecting_outbound[] = $connection;

            continue;
        }

        if(runtime_connection_optimizer_connection_is_replaceable(
            $connection,
            $selected_download_peer_keys,
            $selected_upload_peer_keys,
            $now
        ))
            $replaceable_outbound[] = $connection;
    }

    usort($replaceable_outbound, "runtime_connection_optimizer_compare_active_connections");
    $candidates = array_merge($connecting_outbound, $replaceable_outbound);
    $close_target = min($overflow, $recent_established_inbound);
    $closed = 0;

    foreach($candidates as $connection) {
        if($closed >= $close_target)
            break;

        close_runtime_piece_connection($connection, $peer_retry_after, $now);
        $closed++;
    }

    return [
        "overflow" => $overflow,
        "recent_established_inbound" => $recent_established_inbound,
        "closed_outbound" => $closed,
    ];
}


// Non-blocking peer sockets.
function peer_endpoint_socket_address($endpoint) {
    if(!($endpoint instanceof PeerEndpoint))
        throw new InvalidArgumentException("Peer socket address requires a peer endpoint.");

    return "tcp://{$endpoint->key}";
}

function open_nonblocking_peer_socket($endpoint) {
    $error_number = 0;
    $error_message = "";
    $socket = @stream_socket_client(
        peer_endpoint_socket_address($endpoint),
        $error_number,
        $error_message,
        0,
        STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT
    );

    if($socket === false)
        throw new RuntimeException("Peer TCP connection could not be started: {$error_message}");

    if(!stream_set_blocking($socket, false)) {
        fclose($socket);

        throw new RuntimeException("Peer TCP socket could not be made non-blocking.");
    }

    return $socket;
}

function peer_socket_is_connected($socket) {
    if(!is_resource($socket))
        throw new InvalidArgumentException("Peer TCP socket is invalid.");

    return @stream_socket_get_name($socket, true) !== false;
}

function write_peer_socket($socket, $data) {
    if(!is_resource($socket) || !is_string($data))
        throw new InvalidArgumentException("Peer TCP write arguments are invalid.");

    return @fwrite($socket, $data);
}

function read_peer_socket($socket) {
    if(!is_resource($socket))
        throw new InvalidArgumentException("Peer TCP socket is invalid.");

    return @fread($socket, PEER_READ_CHUNK_SIZE);
}

function build_empty_peer_piece_bitfield($piece_count) {
    if(!is_int($piece_count) || $piece_count < 0)
        throw new InvalidArgumentException("Peer piece bitfield size requires a non-negative piece count.");

    return str_repeat("\x00", intdiv($piece_count + 7, 8));
}

function peer_piece_bitfield_has_piece($bitfield, $piece_index, $piece_count) {
    if(!is_string($bitfield) || !is_int($piece_count) || $piece_count < 0)
        throw new InvalidArgumentException("Peer piece bitfield inspection arguments are invalid.");

    if(!is_int($piece_index) || $piece_index < 0 || $piece_index >= $piece_count)
        return false;

    $byte_index = intdiv($piece_index, 8);
    $bit_index = 7 - ($piece_index % 8);

    return $byte_index < strlen($bitfield)
        && (ord($bitfield[$byte_index]) & (1 << $bit_index)) !== 0;
}

function peer_piece_bitfield_set_piece(&$bitfield, $piece_index, $piece_count) {
    if(!is_string($bitfield) || !is_int($piece_count) || $piece_count < 1)
        throw new InvalidArgumentException("Peer piece bitfield update arguments are invalid.");

    if(!is_int($piece_index) || $piece_index < 0 || $piece_index >= $piece_count)
        throw new InvalidArgumentException("Peer piece bitfield index is outside the torrent piece range.");

    $expected_length = intdiv($piece_count + 7, 8);

    if(strlen($bitfield) !== $expected_length)
        $bitfield = build_empty_peer_piece_bitfield($piece_count);

    $byte_index = intdiv($piece_index, 8);
    $bit_index = 7 - ($piece_index % 8);
    $mask = 1 << $bit_index;
    $old_byte = ord($bitfield[$byte_index]);

    if(($old_byte & $mask) !== 0)
        return false;

    $bitfield[$byte_index] = chr($old_byte | $mask);

    return true;
}

function peer_piece_bitfield_indexes($bitfield, $piece_count) {
    if(!is_string($bitfield) || !is_int($piece_count) || $piece_count < 0)
        throw new InvalidArgumentException("Peer piece bitfield enumeration arguments are invalid.");

    $piece_indexes = [];

    for($piece_index = 0; $piece_index < $piece_count; $piece_index++) {
        if(peer_piece_bitfield_has_piece($bitfield, $piece_index, $piece_count))
            $piece_indexes[] = $piece_index;
    }

    return $piece_indexes;
}

function peer_piece_bitfield_count($bitfield, $piece_count) {
    if(!is_string($bitfield) || !is_int($piece_count) || $piece_count < 0)
        throw new InvalidArgumentException("Peer piece bitfield count arguments are invalid.");

    static $byte_counts = null;

    if($byte_counts === null) {
        $byte_counts = [];

        for($value = 0; $value < 256; $value++) {
            $count = 0;
            $candidate = $value;

            while($candidate !== 0) {
                $count += $candidate & 1;
                $candidate >>= 1;
            }

            $byte_counts[$value] = $count;
        }
    }

    $count = 0;
    $byte_count = min(strlen($bitfield), intdiv($piece_count + 7, 8));

    for($byte_index = 0; $byte_index < $byte_count; $byte_index++)
        $count += $byte_counts[ord($bitfield[$byte_index])];

    return $count;
}

// Peer connection state.
final class PeerConnection {
    public const STATE_NEW = "NEW";
    public const STATE_CONNECTING = "CONNECTING";
    public const STATE_HANDSHAKING = "HANDSHAKING";
    public const STATE_ESTABLISHED = "ESTABLISHED";
    public const STATE_FAILED = "FAILED";
    public const STATE_CLOSED = "CLOSED";

    public readonly Peer $peer;
    public readonly string $info_hash;
    public readonly string $local_peer_id;
    public readonly string $local_reserved_bytes;
    public readonly string $local_handshake;
    public readonly array $local_extension_ids;
    public readonly string $local_extension_handshake;
    public readonly float $connect_timeout;
    public string $state = self::STATE_NEW;
    public $socket = null;
    public string $input_buffer = "";
    public int $input_buffer_offset = 0;
    public string $output_buffer = "";
    public ?string $expected_peer_id = null;
    public ?string $remote_peer_id = null;
    public ?string $remote_reserved_bytes = null;
    public ?string $failure_reason = null;
    public ?float $connect_started_at = null;
    public ?float $handshake_completed_at = null;
    public bool $local_handshake_sent = false;
    public bool $remote_handshake_received = false;
    public array $received_messages = [];
    public int $remote_message_count = 0;
    public bool $remote_choking = true;
    public bool $remote_interested = false;
    public $local_interested = false;
    public bool $remote_bitfield_received = false;
    public bool $remote_bitfield_allowed = true;
    public bool $remote_piece_information_received = false;
    public string $remote_piece_bitfield = "";
    public int $remote_piece_count = 0;
    public int $remote_piece_version = 0;
    public bool $local_extension_handshake_queued = false;
    public int $remote_extension_handshake_count = 0;
    public ?array $remote_extension_handshake = null;
    public int $remote_pex_message_count = 0;
    public ?float $last_pex_sent_at = null;
    public array $pex_advertised_endpoints = [];
    public $metadata_exchange = null;
    public $metadata_requests = [];
    public $local_metadata_size_advertised = false;
    public $piece_manager = null;
    public $block_requests = [];
    public $request_owner;
    public $received_block_count = 0;
    public int $received_block_bytes = 0;
    public int $received_useful_block_bytes = 0;
    public array $useful_block_bytes_by_piece = [];
    public int $successful_block_request_count = 0;
    public int $failed_block_request_count = 0;
    public float $request_latency_sum_seconds = 0.0;
    public int $request_latency_count = 0;
    public ?float $last_piece_received_at = null;
    public bool $local_choking = true;
    public bool $local_bitfield_queued = false;
    public array $announced_local_pieces = [];
    public array $pending_upload_requests = [];
    public int $uploaded_block_count = 0;
    public int $uploaded_block_bytes = 0;
    public bool $fresh_service_unchoke_recorded = false;
    public int $fresh_service_useful_bytes = 0;
    public bool $fresh_turnover_retiring = false;
    public ?float $fresh_turnover_retire_marked_at = null;
    public ?string $fresh_turnover_candidate_peer_key = null;
    public ?int $local_dht_port = null;
    public bool $local_dht_port_queued = false;
    public bool $inbound = false;
    public bool $metrics_archived = false;

    public function __construct(
        $peer,
        $info_hash,
        $local_peer_id,
        $connect_timeout = CONNECT_TIMEOUT,
        $reserved_bytes = null,
        $extension_ids = null,
        $metadata_exchange = null,
        $piece_manager = null,
        $local_dht_port = null
    ) {
        if(!($peer instanceof Peer))
            throw new InvalidArgumentException("Peer connection requires a peer.");

        if($reserved_bytes === null)
            $reserved_bytes = build_peer_extension_reserved_bytes();

        if($local_dht_port !== null) {
            if(!is_int($local_dht_port) || $local_dht_port < 1 || $local_dht_port > 65535)
                throw new InvalidArgumentException("Peer connection DHT port must be between 1 and 65535 or null.");

            $reserved_bytes = build_peer_dht_reserved_bytes($reserved_bytes);
        }

        if($extension_ids === null) {
            $extension_ids = [
                UT_METADATA_EXTENSION_NAME => UT_METADATA_LOCAL_ID,
                UT_PEX_EXTENSION_NAME => UT_PEX_LOCAL_ID,
            ];
        }

        validate_peer_extension_ids($extension_ids);

        if($metadata_exchange !== null && !($metadata_exchange instanceof MetadataExchange))
            throw new InvalidArgumentException("Peer connection metadata exchange has an invalid type.");

        if($piece_manager !== null && !($piece_manager instanceof PieceManager))
            throw new InvalidArgumentException("Peer connection piece manager has an invalid type.");

        if(
            $metadata_exchange !== null
            && (!is_string($info_hash) || !hash_equals($info_hash, $metadata_exchange->expected_info_hash))
        )
            throw new InvalidArgumentException("Peer connection and metadata exchange info hashes must match.");

        if(
            $piece_manager !== null
            && (!is_string($info_hash) || !hash_equals($info_hash, $piece_manager->metadata->info_hash))
        )
            throw new InvalidArgumentException("Peer connection and piece manager info hashes must match.");

        if(
            $metadata_exchange !== null
            && (($extension_ids[UT_METADATA_EXTENSION_NAME] ?? 0) < 1)
        )
            throw new InvalidArgumentException("Metadata exchange requires a local ut_metadata extension ID.");

        if($extension_ids !== [] && !peer_supports_extension_protocol($reserved_bytes))
            throw new InvalidArgumentException("Local extension IDs require BEP 10 handshake support.");

        if(
            (!is_int($connect_timeout) && !is_float($connect_timeout))
            || !is_finite(floatval($connect_timeout))
            || $connect_timeout <= 0
        )
            throw new InvalidArgumentException("Peer connection timeout must be a finite positive number.");

        $this->local_handshake = build_peer_handshake($info_hash, $local_peer_id, $reserved_bytes);
        $this->peer = $peer;
        $this->info_hash = $info_hash;
        $this->local_peer_id = $local_peer_id;
        $this->local_reserved_bytes = $reserved_bytes;
        $this->local_extension_ids = $extension_ids;
        $this->metadata_exchange = $metadata_exchange;
        $this->piece_manager = $piece_manager;
        $this->remote_piece_bitfield = $piece_manager === null
            ? ""
            : build_empty_peer_piece_bitfield($piece_manager->piece_count);
        $this->local_dht_port = $local_dht_port;
        $this->request_owner = $peer->endpoint->key . "#" . spl_object_id($this);
        $extension_properties = [];

        if($metadata_exchange !== null && $metadata_exchange->is_complete()) {
            $extension_properties["metadata_size"] = $metadata_exchange->metadata_size;
            $this->local_metadata_size_advertised = true;
        }

        $this->local_extension_handshake = encode_peer_extension_handshake(
            $extension_ids,
            $extension_properties
        );
        $this->connect_timeout = floatval($connect_timeout);
    }

    private function close_socket() {
        if(is_resource($this->socket))
            fclose($this->socket);

        $this->socket = null;
    }

    private function fail_connection($reason, $failed_at) {
        if($this->state === self::STATE_FAILED || $this->state === self::STATE_CLOSED)
            return false;

        if(!is_string($reason) || trim($reason) === "")
            $reason = "Peer connection failed.";

        $this->release_metadata_requests();
        $this->release_block_requests(false, true);
        $this->pending_upload_requests = [];
        $this->close_socket();
        $was_established = $this->state === self::STATE_ESTABLISHED;
        $this->state = self::STATE_FAILED;
        $this->failure_reason = trim($reason);
        $this->peer->record_connection_failure($failed_at, null, $this->failure_reason, $was_established);

        return false;
    }

    private function complete_handshake_if_ready($completed_at) {
        if(!$this->local_handshake_sent || !$this->remote_handshake_received)
            return false;

        if($this->state === self::STATE_ESTABLISHED)
            return true;

        $this->state = self::STATE_ESTABLISHED;
        $this->handshake_completed_at = normalise_peer_time($completed_at);
        $this->peer->record_connection_success($this->handshake_completed_at);
        $this->peer->fresh_service_connection_count++;
        $this->queue_initial_bitfield_if_needed();
        $this->queue_extension_handshake_if_supported();
        $this->queue_dht_port_if_supported();

        return true;
    }

    private function queue_initial_bitfield_if_needed() {
        if(
            $this->state !== self::STATE_ESTABLISHED
            || $this->local_bitfield_queued
            || $this->piece_manager === null
            || $this->piece_manager->verified_piece_count === 0
        )
            return false;

        $verified_piece_indexes = $this->piece_manager->get_verified_piece_indexes();
        $this->output_buffer .= encode_peer_bitfield(build_verified_piece_bitfield($this->piece_manager));
        $this->local_bitfield_queued = true;

        foreach($verified_piece_indexes as $piece_index)
            $this->announced_local_pieces[$piece_index] = true;

        return true;
    }

    private function supports_extension_protocol() {
        return $this->remote_reserved_bytes !== null
            && peer_supports_extension_protocol($this->local_reserved_bytes)
            && peer_supports_extension_protocol($this->remote_reserved_bytes);
    }

    private function queue_extension_handshake_if_supported() {
        if(
            $this->state !== self::STATE_ESTABLISHED
            || $this->local_extension_handshake_queued
            || !$this->supports_extension_protocol()
        )
            return false;

        $this->output_buffer .= $this->local_extension_handshake;
        $this->local_extension_handshake_queued = true;

        return true;
    }

    private function supports_dht() {
        return $this->remote_reserved_bytes !== null
            && $this->local_dht_port !== null
            && peer_supports_dht($this->local_reserved_bytes)
            && peer_supports_dht($this->remote_reserved_bytes);
    }

    private function queue_dht_port_if_supported() {
        if(
            $this->state !== self::STATE_ESTABLISHED
            || $this->local_dht_port_queued
            || !$this->supports_dht()
        )
            return false;

        $this->output_buffer .= encode_peer_port($this->local_dht_port);
        $this->local_dht_port_queued = true;

        return true;
    }

    private function release_metadata_requests() {
        if($this->metadata_exchange !== null && $this->metadata_requests !== [])
            $this->metadata_exchange->release_requests(array_keys($this->metadata_requests));

        $this->metadata_requests = [];
    }

    private function block_request_key($piece_index, $begin) {
        return "{$piece_index}:{$begin}";
    }

    private function release_block_requests($queue_cancels = false, $count_as_failures = false) {
        if(!is_bool($queue_cancels) || !is_bool($count_as_failures))
            throw new InvalidArgumentException("Block-request release flags must be boolean.");

        $released_requests = [];

        foreach($this->block_requests as $request) {
            if($queue_cancels && $this->state === self::STATE_ESTABLISHED) {
                $this->queue_peer_message(encode_peer_cancel(
                    $request["piece_index"],
                    $request["begin"],
                    $request["length"]
                ));
            }

            if($this->piece_manager !== null) {
                $this->piece_manager->release_block_request(
                    $request["piece_index"],
                    $request["begin"],
                    $this->request_owner
                );
            }

            $released_requests[] = $request;
        }

        if($count_as_failures)
            $this->failed_block_request_count += count($released_requests);

        $this->block_requests = [];

        return $released_requests;
    }

    public function record_block_request_timeouts($now, $timeout_seconds) {
        $now = normalise_peer_time($now);

        if(
            (!is_int($timeout_seconds) && !is_float($timeout_seconds))
            || !is_finite(floatval($timeout_seconds))
            || $timeout_seconds <= 0
        )
            throw new InvalidArgumentException("Block-request timeout must be a finite positive number.");

        $timed_out = [];

        foreach($this->block_requests as $request_key => $request) {
            if($now - $request["requested_at"] < $timeout_seconds)
                continue;

            if($this->piece_manager !== null) {
                $this->piece_manager->release_block_request(
                    $request["piece_index"],
                    $request["begin"],
                    $this->request_owner
                );
            }

            $timed_out[] = $request;
            unset($this->block_requests[$request_key]);
        }

        $this->failed_block_request_count += count($timed_out);

        return $timed_out;
    }

    public function invalidate_piece_download_contribution($piece_index) {
        if(!is_int($piece_index) || $piece_index < 0)
            throw new InvalidArgumentException("Piece contribution index must be a non-negative integer.");

        $bytes = $this->useful_block_bytes_by_piece[$piece_index] ?? 0;

        if($bytes <= 0)
            return 0;

        $this->received_useful_block_bytes = max(
            0,
            $this->received_useful_block_bytes - $bytes
        );
        unset($this->useful_block_bytes_by_piece[$piece_index]);

        return $bytes;
    }

    public function finalise_piece_download_contribution($piece_index) {
        if(!is_int($piece_index) || $piece_index < 0)
            throw new InvalidArgumentException("Piece contribution index must be a non-negative integer.");

        $bytes = $this->useful_block_bytes_by_piece[$piece_index] ?? 0;
        unset($this->useful_block_bytes_by_piece[$piece_index]);

        return $bytes;
    }

    private function apply_remote_bitfield($bitfield) {
        if($this->piece_manager === null)
            return false;

        if(!is_string($bitfield))
            throw new InvalidArgumentException("Remote peer bitfield must be a byte string.");

        $piece_count = $this->piece_manager->piece_count;
        $expected_length = intdiv($piece_count + 7, 8);

        if(strlen($bitfield) !== $expected_length)
            throw new RuntimeException("Remote peer bitfield length does not match the torrent piece count.");

        if($piece_count % 8 !== 0 && $bitfield !== "") {
            $unused_bit_count = 8 - ($piece_count % 8);
            $unused_mask = (1 << $unused_bit_count) - 1;

            if((ord($bitfield[$expected_length - 1]) & $unused_mask) !== 0)
                throw new RuntimeException("Remote peer bitfield sets unused trailing bits.");
        }

        $this->remote_piece_bitfield = $bitfield;
        $this->remote_piece_count = peer_piece_bitfield_count($bitfield, $piece_count);
        $this->remote_piece_version++;
        $this->remote_piece_information_received = true;
        $this->peer->set_seed($this->remote_piece_count === $piece_count);

        return true;
    }

    private function apply_remote_have($piece_index) {
        if($this->piece_manager === null)
            return false;

        if(!is_int($piece_index) || $piece_index < 0 || $piece_index >= $this->piece_manager->piece_count)
            throw new RuntimeException("Remote peer HAVE index is outside the torrent piece range.");

        if(peer_piece_bitfield_set_piece(
            $this->remote_piece_bitfield,
            $piece_index,
            $this->piece_manager->piece_count
        )) {
            $this->remote_piece_count++;
            $this->remote_piece_version++;
        }

        $this->remote_piece_information_received = true;
        $this->peer->set_seed($this->remote_piece_count === $this->piece_manager->piece_count);

        return true;
    }

    public function has_remote_piece($piece_index) {
        if($this->piece_manager === null)
            throw new LogicException("Remote piece availability requires a piece manager.");

        $this->piece_manager->get_piece_length($piece_index);

        return peer_piece_bitfield_has_piece(
            $this->remote_piece_bitfield,
            $piece_index,
            $this->piece_manager->piece_count
        );
    }

    public function get_remote_piece_indexes() {
        if($this->piece_manager === null)
            return [];

        return peer_piece_bitfield_indexes(
            $this->remote_piece_bitfield,
            $this->piece_manager->piece_count
        );
    }

    public function get_remote_piece_bitfield() {
        return $this->remote_piece_bitfield;
    }

    private function queue_metadata_size_update() {
        if(
            $this->metadata_exchange === null
            || !$this->metadata_exchange->is_complete()
            || $this->local_metadata_size_advertised
        )
            return false;

        $this->queue_peer_message(encode_peer_extension_handshake(
            $this->local_extension_ids,
            [
                "metadata_size" => $this->metadata_exchange->metadata_size,
            ]
        ));
        $this->local_metadata_size_advertised = true;

        return true;
    }

    private function apply_metadata_message($metadata_message) {
        $piece = $metadata_message["piece"];

        if($metadata_message["type"] === "unknown")
            return;

        if($metadata_message["type"] === "request") {
            if($this->metadata_exchange === null || !$this->metadata_exchange->is_complete()) {
                $this->queue_extension_message(
                    UT_METADATA_EXTENSION_NAME,
                    encode_ut_metadata_reject($piece)
                );

                return;
            }

            try {
                $data = $this->metadata_exchange->get_piece($piece);
            } catch(InvalidArgumentException $exception) {
                $this->queue_extension_message(
                    UT_METADATA_EXTENSION_NAME,
                    encode_ut_metadata_reject($piece)
                );

                return;
            }

            $this->queue_extension_message(
                UT_METADATA_EXTENSION_NAME,
                encode_ut_metadata_data($piece, $this->metadata_exchange->metadata_size, $data)
            );

            return;
        }

        if($this->metadata_exchange === null)
            return;

        if($metadata_message["type"] === "reject") {
            $this->metadata_exchange->record_rejection($piece);
            unset($this->metadata_requests[$piece]);

            return;
        }

        $completed = $this->metadata_exchange->add_piece(
            $piece,
            $metadata_message["total_size"],
            $metadata_message["data"]
        );
        unset($this->metadata_requests[$piece]);

        if($completed)
            $this->queue_metadata_size_update();
    }

    private function record_fresh_service_unchoke($handled_at) {
        if($this->fresh_service_unchoke_recorded || $this->handshake_completed_at === null)
            return false;

        $handled_at = normalise_peer_time($handled_at);

        if(
            $handled_at - $this->handshake_completed_at > FRESH_SERVICE_WINDOW
            || $this->uploaded_block_bytes > 0
        )
            return false;

        $this->fresh_service_unchoke_recorded = true;
        $this->peer->fresh_service_unchoke_count++;
        $this->peer->last_fresh_service_unchoke_at = $handled_at;

        return true;
    }

    private function record_fresh_service_useful_bytes($bytes, $handled_at) {
        if(!is_int($bytes) || $bytes < 0)
            throw new InvalidArgumentException("Fresh-service useful byte count must be a non-negative integer.");

        if(
            $bytes === 0
            || !$this->fresh_service_unchoke_recorded
            || $this->handshake_completed_at === null
            || $this->uploaded_block_bytes > 0
        )
            return 0;

        $handled_at = normalise_peer_time($handled_at);

        if($handled_at - $this->handshake_completed_at > FRESH_SERVICE_WINDOW)
            return 0;

        $this->fresh_service_useful_bytes += $bytes;
        $this->peer->fresh_service_useful_bytes += $bytes;

        return $bytes;
    }

    private function apply_piece_message($message, $handled_at) {
        $handled_at = normalise_peer_time($handled_at);
        $this->received_block_bytes += strlen($message["block"] ?? "");

        if($this->piece_manager === null)
            return $message;

        $request_key = $this->block_request_key($message["piece_index"], $message["begin"]);

        if(!isset($this->block_requests[$request_key])) {
            $message["piece_result"] = "unsolicited";

            return $message;
        }

        $request = $this->block_requests[$request_key];
        $latency = max(0.0, $handled_at - $request["requested_at"]);

        if(strlen($message["block"]) !== $request["length"]) {
            $this->piece_manager->release_block_request(
                $request["piece_index"],
                $request["begin"],
                $this->request_owner
            );
            unset($this->block_requests[$request_key]);
            $this->failed_block_request_count++;

            throw new RuntimeException("Peer piece response length does not match its block request.");
        }

        $outstanding_request = $this->piece_manager->get_outstanding_request(
            $message["piece_index"],
            $message["begin"]
        );
        $is_endgame_duplicate = ($request["endgame_duplicate"] ?? false) === true;
        $message["endgame_duplicate"] = $is_endgame_duplicate;

        $this->request_latency_sum_seconds += $latency;
        $this->request_latency_count++;
        $this->successful_block_request_count++;
        $this->last_piece_received_at = $handled_at;
        $this->peer->last_piece_received_at = $handled_at;
        $message["request_latency"] = $latency;

        if($is_endgame_duplicate) {
            if($this->piece_manager->is_block_complete($message["piece_index"], $message["begin"])) {
                unset($this->block_requests[$request_key]);
                $message["piece_result"] = "stale";

                return $message;
            }
        } elseif(
            $outstanding_request === null
            || $outstanding_request["request_owner"] !== $this->request_owner
        ) {
            unset($this->block_requests[$request_key]);
            $message["piece_result"] = "stale";

            return $message;
        }

        $piece_result = $this->piece_manager->add_block(
            $message["piece_index"],
            $message["begin"],
            $message["block"]
        );
        unset($this->block_requests[$request_key]);
        $this->received_block_count++;

        if($piece_result !== false) {
            $block_length = strlen($message["block"]);
            $this->received_useful_block_bytes += $block_length;
            $this->useful_block_bytes_by_piece[$message["piece_index"]] =
                ($this->useful_block_bytes_by_piece[$message["piece_index"]] ?? 0)
                + $block_length;
            $this->record_fresh_service_useful_bytes($block_length, $handled_at);
        }

        if($piece_result === true)
            $message["piece_result"] = "verified";
        elseif($piece_result === false)
            $message["piece_result"] = "verification_failed";
        else
            $message["piece_result"] = "accepted";

        return $message;
    }

    private function apply_received_message($message, $handled_at) {
        $handled_at = normalise_peer_time($handled_at);

        if($message["type"] === "bitfield") {
            if(!$this->remote_bitfield_allowed || $this->remote_bitfield_received)
                throw new RuntimeException("Peer bitfield must be the first peer-wire message.");

            $this->remote_bitfield_received = true;
            $this->remote_bitfield_allowed = false;
            $this->apply_remote_bitfield($message["bitfield"]);
        } elseif(
            $message["type"] !== "extended_handshake"
            && $message["type"] !== "keep_alive"
            && $message["type"] !== "port"
        ) {
            $this->remote_bitfield_allowed = false;

            if($message["type"] === "have")
                $this->apply_remote_have($message["piece_index"]);
        }

        if($message["type"] === "extended_handshake") {
            if(!$this->supports_extension_protocol())
                throw new RuntimeException("Peer sent an extension handshake without negotiated BEP 10 support.");

            $this->peer->update_extension_ids($message["extension_ids"]);

            if(array_key_exists("metadata_size", $message["handshake"])) {
                if($this->peer->get_extension_id(UT_METADATA_EXTENSION_NAME) === null)
                    throw new RuntimeException("Peer advertised metadata_size without ut_metadata support.");

                $this->peer->set_metadata_size($message["handshake"]["metadata_size"]);

                if($this->metadata_exchange !== null)
                    $this->metadata_exchange->set_metadata_size($this->peer->metadata_size);
            }

            $this->remote_extension_handshake = $message["handshake"];
            $this->remote_extension_handshake_count++;
        } elseif($message["type"] === "extended") {
            if(!$this->supports_extension_protocol())
                throw new RuntimeException("Peer sent an extended message without negotiated BEP 10 support.");

            $extension_name = peer_extension_name_for_id(
                $this->local_extension_ids,
                $message["extension_id"]
            );

            if($extension_name === null)
                throw new RuntimeException("Peer used an unnegotiated local extension message ID.");

            $message["extension_name"] = $extension_name;

            if($extension_name === UT_METADATA_EXTENSION_NAME) {
                $message["metadata_message"] = parse_ut_metadata_message($message["payload"]);
                $this->apply_metadata_message($message["metadata_message"]);
            } elseif($extension_name === UT_PEX_EXTENSION_NAME) {
                $message["pex_message"] = parse_ut_pex_message(
                    $message["payload"],
                    $this->remote_pex_message_count === 0
                );
                $this->remote_pex_message_count++;
            }
        } elseif($message["type"] === "piece")
            $message = $this->apply_piece_message($message, $handled_at);

        if($message["type"] === "choke") {
            $this->remote_choking = true;
            $this->peer->remote_choking = true;
            $this->peer->last_choked_at = $handled_at;
            $this->release_block_requests(false, true);
        } elseif($message["type"] === "unchoke") {
            $this->remote_choking = false;
            $this->peer->remote_choking = false;
            $this->peer->last_unchoked_at = $handled_at;
            $this->record_fresh_service_unchoke($handled_at);
        } elseif($message["type"] === "interested")
            $this->remote_interested = true;
        elseif($message["type"] === "not_interested")
            $this->remote_interested = false;

        if(($message["type"] ?? null) === "piece")
            unset($message["block"]);

        $this->received_messages[] = $message;
        $this->remote_message_count++;
    }

    private function compact_input_buffer_if_needed() {
        if($this->input_buffer_offset <= 0)
            return false;

        $buffer_length = strlen($this->input_buffer);

        if(
            $this->input_buffer_offset < PEER_INPUT_BUFFER_COMPACT_THRESHOLD
            && $this->input_buffer_offset * 2 < $buffer_length
        )
            return false;

        $this->input_buffer = substr($this->input_buffer, $this->input_buffer_offset);
        $this->input_buffer_offset = 0;

        return true;
    }

    private function parse_buffered_messages($handled_at) {
        try {
            while(true) {
                $message = consume_peer_message_at_offset(
                    $this->input_buffer,
                    $this->input_buffer_offset
                );

                if($message === null) {
                    $this->compact_input_buffer_if_needed();

                    return true;
                }

                $this->apply_received_message($message, $handled_at);
            }
        } catch(Throwable $exception) {
            return $this->fail_connection(
                "Invalid BitTorrent peer message: {$exception->getMessage()}",
                $handled_at
            );
        }
    }

    public function start_inbound($socket, $started_at = null) {
        if($this->state !== self::STATE_NEW)
            throw new LogicException("Peer connection has already been started.");

        if(!is_resource($socket))
            throw new InvalidArgumentException("Inbound peer connection requires a socket resource.");

        if(!stream_set_blocking($socket, false))
            throw new RuntimeException("Inbound peer TCP socket could not be made non-blocking.");

        $started_at = normalise_peer_time($started_at);
        $this->connect_started_at = $started_at;
        $this->expected_peer_id = $this->peer->peer_id;
        $this->peer->clear_extension_ids();
        $this->peer->record_connection_attempt($started_at);
        $this->socket = $socket;
        $this->state = self::STATE_HANDSHAKING;
        $this->output_buffer = $this->local_handshake;
        $this->inbound = true;

        return true;
    }

    public function start($started_at = null, $connector = null) {
        if($this->state !== self::STATE_NEW)
            throw new LogicException("Peer connection has already been started.");

        $started_at = normalise_peer_time($started_at);

        if(!$this->peer->is_available($started_at))
            throw new RuntimeException("Peer is not available for a new connection.");

        if($this->peer->peer_id !== null && hash_equals($this->local_peer_id, $this->peer->peer_id))
            throw new RuntimeException("Refusing to connect to the local peer ID.");

        if($connector === null)
            $connector = "open_nonblocking_peer_socket";

        if(!is_callable($connector))
            throw new InvalidArgumentException("Peer socket connector must be callable.");

        $this->connect_started_at = $started_at;
        $this->expected_peer_id = $this->peer->peer_id;
        $this->peer->clear_extension_ids();
        $this->peer->record_connection_attempt($started_at);

        try {
            $socket = $connector($this->peer->endpoint);
        } catch(Throwable $exception) {
            return $this->fail_connection(
                "Peer TCP connection could not be started: {$exception->getMessage()}",
                $started_at
            );
        }

        if(!is_resource($socket))
            return $this->fail_connection("Peer TCP connector returned an invalid socket.", $started_at);

        $this->socket = $socket;
        $this->state = self::STATE_CONNECTING;

        return true;
    }

    public function handle_writable($handled_at = null, $connection_checker = null, $writer = null) {
        if(
            $this->state === self::STATE_NEW
            || $this->state === self::STATE_FAILED
            || $this->state === self::STATE_CLOSED
        )
            throw new LogicException("Peer connection is not writable in its current state.");

        $handled_at = normalise_peer_time($handled_at);

        if($this->state === self::STATE_CONNECTING) {
            if($connection_checker === null)
                $connection_checker = "peer_socket_is_connected";

            if(!is_callable($connection_checker))
                throw new InvalidArgumentException("Peer connection checker must be callable.");

            try {
                $connected = $connection_checker($this->socket);
            } catch(Throwable $exception) {
                return $this->fail_connection(
                    "Peer TCP connection check failed: {$exception->getMessage()}",
                    $handled_at
                );
            }

            if($connected !== true)
                return $this->fail_connection("Peer TCP connection attempt failed.", $handled_at);

            $this->state = self::STATE_HANDSHAKING;
            $this->output_buffer = $this->local_handshake;
        }

        if($this->output_buffer === "") {
            $this->complete_handshake_if_ready($handled_at);

            if($this->state === self::STATE_ESTABLISHED && $this->input_buffer !== "")
                return $this->parse_buffered_messages($handled_at);

            return true;
        }

        if($writer === null)
            $writer = "write_peer_socket";

        if(!is_callable($writer))
            throw new InvalidArgumentException("Peer socket writer must be callable.");

        try {
            $bytes_written = $writer($this->socket, $this->output_buffer);
        } catch(Throwable $exception) {
            return $this->fail_connection("Peer TCP write failed: {$exception->getMessage()}", $handled_at);
        }

        if(!is_int($bytes_written) || $bytes_written < 0 || $bytes_written > strlen($this->output_buffer))
            return $this->fail_connection("Peer TCP write returned an invalid byte count.", $handled_at);

        if($bytes_written === 0)
            return true;

        $this->output_buffer = substr($this->output_buffer, $bytes_written);

        if($this->output_buffer === "")
            $this->local_handshake_sent = true;

        $this->complete_handshake_if_ready($handled_at);

        if($this->state === self::STATE_ESTABLISHED && $this->input_buffer !== "")
            return $this->parse_buffered_messages($handled_at);

        return true;
    }

    public function handle_readable($handled_at = null, $reader = null) {
        if($this->state !== self::STATE_HANDSHAKING && $this->state !== self::STATE_ESTABLISHED)
            throw new LogicException("Peer connection is not readable in its current state.");

        $handled_at = normalise_peer_time($handled_at);

        if($reader === null)
            $reader = "read_peer_socket";

        if(!is_callable($reader))
            throw new InvalidArgumentException("Peer socket reader must be callable.");

        try {
            $data = $reader($this->socket);
        } catch(Throwable $exception) {
            return $this->fail_connection("Peer TCP read failed: {$exception->getMessage()}", $handled_at);
        }

        if(!is_string($data) || $data === "")
            return $this->fail_connection("Peer closed the TCP connection.", $handled_at);

        $this->input_buffer .= $data;

        if($this->state === self::STATE_ESTABLISHED)
            return $this->parse_buffered_messages($handled_at);

        try {
            $handshake = consume_peer_handshake(
                $this->input_buffer,
                $this->info_hash,
                $this->expected_peer_id
            );
        } catch(Throwable $exception) {
            return $this->fail_connection("Invalid BitTorrent handshake: {$exception->getMessage()}", $handled_at);
        }

        if($handshake === null)
            return true;

        $this->input_buffer_offset = 0;

        if(hash_equals($this->local_peer_id, $handshake["peer_id"]))
            return $this->fail_connection("Remote handshake uses the local peer ID.", $handled_at);

        $this->remote_peer_id = $handshake["peer_id"];
        $this->remote_reserved_bytes = $handshake["reserved_bytes"];
        $this->remote_handshake_received = true;
        $this->peer->set_peer_id($this->remote_peer_id);
        $this->complete_handshake_if_ready($handled_at);

        if($this->state === self::STATE_ESTABLISHED && $this->input_buffer !== "")
            return $this->parse_buffered_messages($handled_at);

        return true;
    }

    public function queue_peer_message($frame) {
        if($this->state !== self::STATE_ESTABLISHED)
            throw new LogicException("Peer messages can only be queued after the handshake.");

        decode_peer_message($frame);
        $this->output_buffer .= $frame;

        return true;
    }

    public function set_local_choking($choking) {
        if(!is_bool($choking))
            throw new InvalidArgumentException("Local choke state must be boolean.");

        if($this->state !== self::STATE_ESTABLISHED)
            throw new LogicException("Local choke state can only change after the handshake.");

        if($this->local_choking === $choking)
            return false;

        $this->queue_peer_message($choking ? encode_peer_choke() : encode_peer_unchoke());
        $this->local_choking = $choking;

        if($choking)
            $this->pending_upload_requests = [];

        return true;
    }

    public function announce_local_piece($piece_index) {
        if($this->piece_manager === null)
            throw new LogicException("Peer connection has no piece manager.");

        if($this->state !== self::STATE_ESTABLISHED)
            return false;

        if(!$this->piece_manager->is_piece_complete($piece_index))
            throw new LogicException("Only verified pieces may be announced.");

        if(isset($this->announced_local_pieces[$piece_index]))
            return false;

        $this->queue_peer_message(encode_peer_have($piece_index));
        $this->announced_local_pieces[$piece_index] = true;

        return true;
    }

    private function upload_request_key($piece_index, $begin, $length) {
        return "{$piece_index}:{$begin}:{$length}";
    }

    public function queue_upload_request($piece_index, $begin, $length) {
        if($this->piece_manager === null)
            throw new LogicException("Peer connection has no piece manager.");

        if($this->state !== self::STATE_ESTABLISHED)
            throw new LogicException("Upload requests require an established peer connection.");

        if($this->local_choking)
            return false;

        if(!$this->piece_manager->can_serve_range($piece_index, $begin, $length))
            return false;

        $request_key = $this->upload_request_key($piece_index, $begin, $length);

        if(isset($this->pending_upload_requests[$request_key]))
            return false;

        if(count($this->pending_upload_requests) >= BASIC_UPLOAD_MAX_PENDING_REQUESTS_PER_PEER)
            throw new RuntimeException("Peer exceeded the pending upload-request limit.");

        $this->pending_upload_requests[$request_key] = [
            "piece_index" => $piece_index,
            "begin" => $begin,
            "length" => $length,
        ];

        return true;
    }

    public function cancel_upload_request($piece_index, $begin, $length) {
        $request_key = $this->upload_request_key($piece_index, $begin, $length);

        if(!isset($this->pending_upload_requests[$request_key]))
            return false;

        unset($this->pending_upload_requests[$request_key]);

        return true;
    }

    public function get_pending_upload_request_count() {
        return count($this->pending_upload_requests);
    }

    public function peek_pending_upload_request() {
        if($this->pending_upload_requests === [])
            return null;

        return reset($this->pending_upload_requests);
    }

    public function serve_next_upload_request() {
        if($this->piece_manager === null)
            throw new LogicException("Peer connection has no piece manager.");

        if($this->state !== self::STATE_ESTABLISHED || $this->local_choking)
            return null;

        $request = $this->peek_pending_upload_request();

        if($request === null)
            return null;

        $request_key = $this->upload_request_key(
            $request["piece_index"],
            $request["begin"],
            $request["length"]
        );

        if(!$this->piece_manager->can_serve_range(
            $request["piece_index"],
            $request["begin"],
            $request["length"]
        )) {
            unset($this->pending_upload_requests[$request_key]);

            return null;
        }

        $block = $this->piece_manager->read_verified_range(
            $request["piece_index"],
            $request["begin"],
            $request["length"]
        );
        $this->queue_peer_message(encode_peer_piece(
            $request["piece_index"],
            $request["begin"],
            $block
        ));
        unset($this->pending_upload_requests[$request_key]);
        $this->uploaded_block_count++;
        $this->uploaded_block_bytes += strlen($block);

        return $request;
    }

    public function set_local_interested($interested) {
        if(!is_bool($interested))
            throw new InvalidArgumentException("Local peer interest state must be boolean.");

        if($this->state !== self::STATE_ESTABLISHED)
            throw new LogicException("Local peer interest can only change after the handshake.");

        if($this->local_interested === $interested)
            return false;

        if(!$interested)
            $this->release_block_requests(true);

        if($interested)
            $this->queue_peer_message(encode_peer_interested());
        else
            $this->queue_peer_message(encode_peer_not_interested());

        $this->local_interested = $interested;

        return true;
    }

    public function get_outstanding_block_requests() {
        return array_values($this->block_requests);
    }

    public function get_outstanding_block_request_count() {
        return count($this->block_requests);
    }

    public function queue_block_requests($piece_index, $limit = 1, $requested_at = null) {
        if($this->piece_manager === null)
            throw new LogicException("Peer connection has no piece manager.");

        if($this->state !== self::STATE_ESTABLISHED)
            throw new LogicException("Block requests require an established peer connection.");

        if(!$this->local_interested)
            throw new LogicException("Block requests require local interest in peer data.");

        if($this->remote_choking)
            throw new LogicException("Block requests cannot be sent while the remote peer is choking.");

        $requests = $this->piece_manager->reserve_blocks(
            $piece_index,
            $limit,
            $this->request_owner,
            $requested_at
        );
        $output_length = strlen($this->output_buffer);

        try {
            foreach($requests as $request) {
                $this->queue_peer_message(encode_peer_request(
                    $request["piece_index"],
                    $request["begin"],
                    $request["length"]
                ));
                $this->block_requests[$this->block_request_key(
                    $request["piece_index"],
                    $request["begin"]
                )] = $request;
            }
        } catch(Throwable $exception) {
            $this->output_buffer = substr($this->output_buffer, 0, $output_length);

            foreach($requests as $request) {
                $this->piece_manager->release_block_request(
                    $request["piece_index"],
                    $request["begin"],
                    $this->request_owner
                );
                unset($this->block_requests[$this->block_request_key(
                    $request["piece_index"],
                    $request["begin"]
                )]);
            }

            throw $exception;
        }

        return $requests;
    }

    public function has_block_request($piece_index, $begin) {
        if($this->piece_manager === null)
            throw new LogicException("Peer connection has no piece manager.");

        $this->piece_manager->get_block_length($piece_index, $begin);

        return isset($this->block_requests[$this->block_request_key($piece_index, $begin)]);
    }

    public function queue_endgame_duplicate_block_request($piece_index, $begin, $requested_at = null) {
        if($this->piece_manager === null)
            throw new LogicException("Peer connection has no piece manager.");

        if($this->state !== self::STATE_ESTABLISHED)
            throw new LogicException("Endgame block requests require an established peer connection.");

        if(!$this->local_interested)
            throw new LogicException("Endgame block requests require local interest in peer data.");

        if($this->remote_choking)
            throw new LogicException("Endgame block requests cannot be sent while the remote peer is choking.");

        $block_length = $this->piece_manager->get_block_length($piece_index, $begin);
        $request_key = $this->block_request_key($piece_index, $begin);

        if(isset($this->block_requests[$request_key]))
            return false;

        if($this->piece_manager->is_block_complete($piece_index, $begin))
            return false;

        $primary_request = $this->piece_manager->get_outstanding_request($piece_index, $begin);

        if($primary_request === null || $primary_request["request_owner"] === $this->request_owner)
            return false;

        $requested_at = normalise_peer_time($requested_at);
        $request = [
            "piece_index" => $piece_index,
            "begin" => $begin,
            "length" => $block_length,
            "torrent_offset" => $this->piece_manager->get_piece_offset($piece_index) + $begin,
            "request_owner" => $this->request_owner,
            "requested_at" => $requested_at,
            "endgame_duplicate" => true,
            "primary_request_owner" => $primary_request["request_owner"],
        ];
        $output_length = strlen($this->output_buffer);

        try {
            $this->queue_peer_message(encode_peer_request($piece_index, $begin, $block_length));
            $this->block_requests[$request_key] = $request;
        } catch(Throwable $exception) {
            $this->output_buffer = substr($this->output_buffer, 0, $output_length);
            unset($this->block_requests[$request_key]);

            throw $exception;
        }

        return $request;
    }

    public function cancel_block_request($piece_index, $begin) {
        if($this->piece_manager === null)
            throw new LogicException("Peer connection has no piece manager.");

        if($this->state !== self::STATE_ESTABLISHED)
            throw new LogicException("Block requests can only be cancelled on an established connection.");

        $this->piece_manager->get_block_length($piece_index, $begin);
        $request_key = $this->block_request_key($piece_index, $begin);

        if(!isset($this->block_requests[$request_key]))
            return false;

        $request = $this->block_requests[$request_key];
        $this->queue_peer_message(encode_peer_cancel(
            $request["piece_index"],
            $request["begin"],
            $request["length"]
        ));
        $this->piece_manager->release_block_request(
            $request["piece_index"],
            $request["begin"],
            $this->request_owner
        );
        unset($this->block_requests[$request_key]);

        return true;
    }

    public function cancel_block_requests() {
        if($this->state !== self::STATE_ESTABLISHED)
            throw new LogicException("Block requests can only be cancelled on an established connection.");

        return $this->release_block_requests(true);
    }

    public function queue_extension_message($extension_name, $payload) {
        if($this->state !== self::STATE_ESTABLISHED || !$this->supports_extension_protocol())
            throw new LogicException("Extension messages require an established BEP 10 connection.");

        $extension_id = $this->peer->get_extension_id($extension_name);

        if($extension_id === null)
            throw new RuntimeException("Peer did not negotiate the requested extension.");

        return $this->queue_peer_message(encode_peer_extended_message($extension_id, $payload));
    }

    public function queue_metadata_requests($limit = 1) {
        if($this->metadata_exchange === null)
            throw new LogicException("Peer connection has no metadata exchange state.");

        if($this->state !== self::STATE_ESTABLISHED || !$this->supports_extension_protocol())
            throw new LogicException("Metadata requests require an established BEP 10 connection.");

        if($this->peer->get_extension_id(UT_METADATA_EXTENSION_NAME) === null)
            throw new RuntimeException("Peer did not negotiate ut_metadata.");

        if($this->peer->metadata_size === null)
            throw new RuntimeException("Peer did not advertise metadata_size.");

        $this->metadata_exchange->set_metadata_size($this->peer->metadata_size);
        $pieces = $this->metadata_exchange->reserve_pieces($limit);

        try {
            foreach($pieces as $piece) {
                $this->queue_extension_message(
                    UT_METADATA_EXTENSION_NAME,
                    encode_ut_metadata_request($piece)
                );
                $this->metadata_requests[$piece] = true;
            }
        } catch(Throwable $exception) {
            $this->metadata_exchange->release_requests($pieces);

            foreach($pieces as $piece)
                unset($this->metadata_requests[$piece]);

            throw $exception;
        }

        return $pieces;
    }

    public function take_received_messages() {
        $messages = $this->received_messages;
        $this->received_messages = [];

        return $messages;
    }

    public function check_timeout($now = null) {
        if($this->state !== self::STATE_CONNECTING && $this->state !== self::STATE_HANDSHAKING)
            return false;

        $now = normalise_peer_time($now);

        if($this->connect_started_at === null)
            throw new LogicException("Peer connection has no start time.");

        if($now < $this->connect_started_at)
            throw new InvalidArgumentException("Peer connection time cannot move backwards.");

        if($now - $this->connect_started_at < $this->connect_timeout)
            return false;

        $this->fail_connection("Peer connection timed out.", $now);

        return true;
    }

    public function wants_read() {
        return is_resource($this->socket)
            && ($this->state === self::STATE_HANDSHAKING || $this->state === self::STATE_ESTABLISHED);
    }

    public function wants_write() {
        return is_resource($this->socket)
            && ($this->state === self::STATE_CONNECTING || $this->output_buffer !== "");
    }

    public function is_terminal() {
        return $this->state === self::STATE_FAILED || $this->state === self::STATE_CLOSED;
    }

    public function close() {
        if($this->state === self::STATE_CLOSED)
            return false;

        $this->release_metadata_requests();
        $this->release_block_requests();
        $this->pending_upload_requests = [];
        $this->close_socket();
        $this->peer->mark_disconnected();
        $this->state = self::STATE_CLOSED;

        return true;
    }
}

// Demand-driven discovery market state.
function runtime_supplier_market_state($peer_pool, $now = null) {
    if(!($peer_pool instanceof PeerPool))
        throw new InvalidArgumentException("Supplier market state requires a peer pool.");

    $now = normalise_peer_time($now);
    $state = [
        "known_peers" => $peer_pool->get_count(),
        "connecting_peers" => 0,
        "established_peers" => 0,
        "unchoked_peers" => 0,
        "useful_peers" => 0,
        "aggregate_useful_rate" => 0.0,
        "available_peers" => 0,
        "cooldown_peers" => 0,
        "connection_failures" => 0,
        "failure_categories" => [],
    ];

    foreach($peer_pool->get_peers() as $peer) {
        if($peer->is_connecting)
            $state["connecting_peers"]++;

        if($peer->is_connected) {
            $state["established_peers"]++;

            if(!$peer->remote_choking)
                $state["unchoked_peers"]++;

            $useful_rate = max(
                0.0,
                $peer->useful_download_rate_short,
                $peer->useful_download_rate_long
            );

            if(
                $useful_rate > 0.0
                && $peer->last_piece_received_at !== null
                && $now - $peer->last_piece_received_at <= SUPPLIER_USEFUL_RECENCY_SECONDS
            ) {
                $state["useful_peers"]++;
                $state["aggregate_useful_rate"] += $useful_rate;
            }
        }

        if($peer->is_in_cooldown($now))
            $state["cooldown_peers"]++;
        elseif($peer->is_available($now))
            $state["available_peers"]++;

        $state["connection_failures"] += $peer->connection_failures;

        foreach($peer->connection_failure_categories as $category => $count)
            $state["failure_categories"][$category] = ($state["failure_categories"][$category] ?? 0) + $count;
    }

    ksort($state["failure_categories"]);

    return $state;
}

function runtime_discovery_market_state($peer_pool, $now = null) {
    if(!($peer_pool instanceof PeerPool))
        throw new InvalidArgumentException("Discovery market state requires a peer pool.");

    $now = normalise_peer_time($now);
    $supplier_state = runtime_supplier_market_state($peer_pool, $now);
    $known_peers = $supplier_state["known_peers"];
    $connected_peers = $supplier_state["established_peers"];
    $connecting_peers = $supplier_state["connecting_peers"];
    $active_connections = $connected_peers + $connecting_peers;
    $connection_target = min(DESIRED_CONNECTED_PEERS, max(0, $known_peers));
    $supplier_starved = $known_peers > 0 && (
        $connected_peers < MINIMUM_ESTABLISHED_DOWNLOAD_PEERS
        || $supplier_state["unchoked_peers"] < MINIMUM_UNCHOKED_DOWNLOAD_PEERS
        || $supplier_state["useful_peers"] < MINIMUM_USEFUL_DOWNLOAD_PEERS
    );

    if($known_peers < MINIMUM_KNOWN_PEERS || $supplier_starved) {
        $mode = "aggressive";
    } elseif(
        $known_peers < DESIRED_KNOWN_PEERS
        || $active_connections < $connection_target
    ) {
        $mode = "normal";
    } else {
        $mode = "maintenance";
    }

    return [
        "mode" => $mode,
        "known_peers" => $known_peers,
        "connected_peers" => $connected_peers,
        "connecting_peers" => $connecting_peers,
        "active_connections" => $active_connections,
        "connection_target" => $connection_target,
        "supplier_starved" => $supplier_starved,
        "unchoked_peers" => $supplier_state["unchoked_peers"],
        "useful_peers" => $supplier_state["useful_peers"],
        "aggregate_useful_rate" => $supplier_state["aggregate_useful_rate"],
        "available_peers" => $supplier_state["available_peers"],
        "cooldown_peers" => $supplier_state["cooldown_peers"],
    ];
}

function runtime_discovery_tracker_concurrency($mode) {
    return match($mode) {
        "aggressive" => DISCOVERY_AGGRESSIVE_PUBLIC_TRACKER_CONCURRENCY,
        "normal" => DISCOVERY_NORMAL_PUBLIC_TRACKER_CONCURRENCY,
        "maintenance" => 1,
        default => throw new InvalidArgumentException("Unknown discovery mode {$mode}."),
    };
}

function runtime_discovery_tracker_stagger_seconds($mode) {
    return match($mode) {
        "aggressive" => floatval(DISCOVERY_AGGRESSIVE_TRACKER_STAGGER_SECONDS),
        "normal" => floatval(DISCOVERY_NORMAL_TRACKER_STAGGER_SECONDS),
        "maintenance" => floatval(DISCOVERY_MAINTENANCE_TRACKER_STAGGER_SECONDS),
        default => throw new InvalidArgumentException("Unknown discovery mode {$mode}."),
    };
}

function runtime_discovery_dht_refresh_interval($mode) {
    return match($mode) {
        "aggressive" => floatval(DHT_AGGRESSIVE_LOOKUP_REFRESH_INTERVAL),
        "normal" => floatval(DHT_LOOKUP_REFRESH_INTERVAL),
        "maintenance" => floatval(DHT_MAINTENANCE_LOOKUP_REFRESH_INTERVAL),
        default => throw new InvalidArgumentException("Unknown discovery mode {$mode}."),
    };
}

// Runtime orchestration through verified piece management.
function generate_local_peer_id($random_bytes_generator = null) {
    if($random_bytes_generator === null)
        $random_bytes_generator = "random_bytes";

    if(!is_callable($random_bytes_generator))
        throw new InvalidArgumentException("Peer ID random-byte generator must be callable.");

    $random_bytes = $random_bytes_generator(6);

    if(!is_string($random_bytes) || strlen($random_bytes) !== 6)
        throw new RuntimeException("Peer ID random-byte generator must return exactly six bytes.");

    return "-EG0001-" . bin2hex($random_bytes);
}

function build_runtime_tracker_list($magnet, $public_trackers) {
    if(!($magnet instanceof MagnetUri))
        throw new InvalidArgumentException("Runtime tracker discovery requires a parsed magnet URI.");

    if(!is_array($public_trackers))
        throw new InvalidArgumentException("Runtime public trackers must be an array.");

    $trackers = [];
    $seen_trackers = [];

    foreach(array_merge($magnet->trackers, $public_trackers) as $tracker) {
        if(!is_string($tracker) || !is_supported_tracker_url($tracker))
            continue;

        $tracker_key = tracker_deduplication_key($tracker);

        if($tracker_key === null || isset($seen_trackers[$tracker_key]))
            continue;

        $seen_trackers[$tracker_key] = true;
        $trackers[] = $tracker;
    }

    return $trackers;
}

function count_runtime_tracker_duplicates($magnet, $public_trackers) {
    if(!($magnet instanceof MagnetUri) || !is_array($public_trackers))
        throw new InvalidArgumentException("Runtime tracker duplicate counting requires magnet and public trackers.");

    $seen_trackers = [];
    $duplicates = 0;

    foreach(array_merge($magnet->trackers, $public_trackers) as $tracker) {
        if(!is_string($tracker) || !is_supported_tracker_url($tracker))
            continue;

        $tracker_key = tracker_deduplication_key($tracker);

        if($tracker_key === null)
            continue;

        if(isset($seen_trackers[$tracker_key])) {
            $duplicates++;

            continue;
        }

        $seen_trackers[$tracker_key] = true;
    }

    return $duplicates;
}

function describe_runtime_tracker($tracker_url) {
    $parts = parse_url($tracker_url);

    if(!is_array($parts))
        return "tracker";

    $scheme = strtoupper($parts["scheme"] ?? "tracker");
    $host = $parts["host"] ?? "unknown host";
    $port = isset($parts["port"]) ? ":" . $parts["port"] : "";

    return "{$scheme} {$host}{$port}";
}

function announce_runtime_tracker($tracker_url, $info_hash, $peer_id, $peer_pool, $announce_port = CLIENT_ANNOUNCE_PORT) {
    $scheme = strtolower(parse_url($tracker_url, PHP_URL_SCHEME));

    if($scheme === "http" || $scheme === "https") {
        return announce_http_tracker(
            $tracker_url,
            $info_hash,
            $peer_id,
            $announce_port,
            0,
            0,
            TRACKER_METADATA_LEFT,
            $peer_pool,
            "started",
            DESIRED_KNOWN_PEERS
        );
    }

    if($scheme === "udp") {
        return announce_udp_tracker(
            $tracker_url,
            $info_hash,
            $peer_id,
            $announce_port,
            0,
            0,
            TRACKER_METADATA_LEFT,
            $peer_pool,
            "started",
            DESIRED_KNOWN_PEERS
        );
    }

    throw new InvalidArgumentException("Runtime tracker scheme is unsupported.");
}

function add_runtime_tracker_peers($tracker_url, $result, $peer_pool, $announced_at = null) {
    if(!is_string($tracker_url) || !($peer_pool instanceof PeerPool) || !is_array($result))
        throw new InvalidArgumentException("Runtime tracker result is invalid.");

    if(!isset($result["peers"]) || !is_array($result["peers"]))
        throw new InvalidArgumentException("Runtime tracker result has no peer list.");

    $announced_at = normalise_peer_time($announced_at);
    $source = "tracker:{$tracker_url}";
    $previous_peer_count = $peer_pool->get_count();

    foreach($result["peers"] as $endpoint)
        $peer_pool->add_peer($endpoint, $source, $announced_at);

    return $peer_pool->get_count() - $previous_peer_count;
}

function open_parallel_udp_tracker_state(
    $tracker_url,
    $info_hash,
    $peer_id,
    $started_at = null,
    $event = "started",
    $downloaded = 0,
    $uploaded = 0,
    $left = TRACKER_METADATA_LEFT,
    $announce_port = CLIENT_ANNOUNCE_PORT
) {
    $started_at = normalise_peer_time($started_at);
    validate_tracker_counter("downloaded byte count", $downloaded);
    validate_tracker_counter("uploaded byte count", $uploaded);
    validate_tracker_counter("remaining byte count", $left);
    udp_tracker_event_code($event);
    $tracker = parse_udp_tracker_url($tracker_url);
    $error_number = 0;
    $error_message = "";
    $socket = @stream_socket_client(
        $tracker["socket_address"],
        $error_number,
        $error_message,
        0,
        STREAM_CLIENT_CONNECT
    );

    if($socket === false)
        throw new RuntimeException("UDP tracker socket could not be opened: {$error_message}");

    if(!stream_set_blocking($socket, false)) {
        fclose($socket);

        throw new RuntimeException("UDP tracker socket could not be made non-blocking.");
    }

    $connect_transaction_id = generate_udp_tracker_transaction_id();
    $announce_transaction_id = generate_udp_tracker_transaction_id();
    $key = generate_udp_tracker_key();
    $request = build_udp_tracker_connect_request($connect_transaction_id);
    $bytes_written = @fwrite($socket, $request);

    if($bytes_written !== strlen($request)) {
        fclose($socket);

        throw new RuntimeException("UDP tracker connect request could not be sent.");
    }

    return [
        "tracker_url" => $tracker_url,
        "description" => describe_runtime_tracker($tracker_url),
        "tracker" => $tracker,
        "socket" => $socket,
        "stage" => "connect",
        "connect_transaction_id" => $connect_transaction_id,
        "announce_transaction_id" => $announce_transaction_id,
        "key" => $key,
        "request" => $request,
        "attempt" => 1,
        "timeout" => floatval(UDP_TRACKER_INITIAL_TIMEOUT),
        "deadline" => $started_at + floatval(UDP_TRACKER_INITIAL_TIMEOUT),
        "event" => $event,
        "downloaded" => $downloaded,
        "uploaded" => $uploaded,
        "left" => $left,
        "announce_port" => $announce_port,
    ];
}

function retry_parallel_udp_tracker_state(&$state, $now) {
    if(!is_array($state) || !isset($state["socket"], $state["request"], $state["attempt"], $state["timeout"]))
        throw new InvalidArgumentException("Parallel UDP tracker state is invalid.");

    $now = normalise_peer_time($now);

    if($state["attempt"] >= UDP_TRACKER_MAX_ATTEMPTS)
        return false;

    $state["attempt"]++;
    $state["timeout"] *= UDP_TRACKER_BACKOFF_FACTOR;
    $bytes_written = @fwrite($state["socket"], $state["request"]);

    if($bytes_written !== strlen($state["request"]))
        throw new RuntimeException("UDP tracker retry datagram could not be sent.");

    $state["deadline"] = $now + $state["timeout"];

    return true;
}

function begin_parallel_udp_tracker_announce(&$state, $connection_id, $info_hash, $peer_id, $now) {
    if(!is_array($state))
        throw new InvalidArgumentException("Parallel UDP tracker state is invalid.");

    $now = normalise_peer_time($now);
    $request = build_udp_tracker_announce_request(
        $connection_id,
        $info_hash,
        $peer_id,
        $state["announce_port"],
        $state["downloaded"],
        $state["uploaded"],
        $state["left"],
        $state["event"],
        DESIRED_KNOWN_PEERS,
        $state["announce_transaction_id"],
        $state["key"]
    );
    $bytes_written = @fwrite($state["socket"], $request);

    if($bytes_written !== strlen($request))
        throw new RuntimeException("UDP tracker announce request could not be sent.");

    $state["stage"] = "announce";
    $state["request"] = $request;
    $state["attempt"] = 1;
    $state["timeout"] = floatval(UDP_TRACKER_INITIAL_TIMEOUT);
    $state["deadline"] = $now + floatval(UDP_TRACKER_INITIAL_TIMEOUT);
}

function discover_runtime_udp_trackers_parallel(
    $tracker_urls,
    $info_hash,
    $peer_id,
    $peer_pool,
    $log_stream
) {
    if(!is_array($tracker_urls) || !($peer_pool instanceof PeerPool))
        throw new InvalidArgumentException("Parallel UDP tracker discovery arguments are invalid.");

    $states = [];
    $attempts = 0;
    $successes = 0;
    $failures = 0;

    foreach($tracker_urls as $tracker_url) {
        if($peer_pool->get_count() >= DESIRED_KNOWN_PEERS)
            break;

        $attempts++;
        $description = describe_runtime_tracker($tracker_url);

        try {
            $states[] = open_parallel_udp_tracker_state($tracker_url, $info_hash, $peer_id);
        } catch(Throwable $exception) {
            $failures++;
            log_message("Tracker discovery failed for {$description}: {$exception->getMessage()}", $log_stream);
        }
    }

    if($states !== [])
        log_message("Parallel UDP tracker discovery started for " . count($states) . " trackers.", $log_stream);

    try {
        while($states !== [] && $peer_pool->get_count() < DESIRED_KNOWN_PEERS) {
            $read_sockets = [];
            $socket_states = [];

            foreach($states as $state_index => $state) {
                if(!is_resource($state["socket"]))
                    continue;

                $read_sockets[] = $state["socket"];
                $socket_states[get_resource_id($state["socket"])] = $state_index;
            }

            if($read_sockets === [])
                break;

            $write_sockets = null;
            $except_sockets = null;
            $selected = @stream_select(
                $read_sockets,
                $write_sockets,
                $except_sockets,
                0,
                RUNTIME_SELECT_TIMEOUT_MICROSECONDS
            );

            if($selected === false)
                throw new RuntimeException("UDP tracker socket selection failed.");

            $now = microtime(true);

            foreach($read_sockets as $socket) {
                $socket_id = get_resource_id($socket);

                if(!isset($socket_states[$socket_id]))
                    continue;

                $state_index = $socket_states[$socket_id];

                if(!isset($states[$state_index]))
                    continue;

                $response = @fread($socket, 65535);

                if(!is_string($response) || $response === "")
                    continue;

                $description = $states[$state_index]["description"];

                try {
                    if($states[$state_index]["stage"] === "connect") {
                        $connect_result = parse_udp_tracker_connect_response(
                            $response,
                            $states[$state_index]["connect_transaction_id"]
                        );
                        begin_parallel_udp_tracker_announce(
                            $states[$state_index],
                            $connect_result["connection_id"],
                            $info_hash,
                            $peer_id,
                            $now
                        );

                        continue;
                    }

                    $announce_result = parse_udp_tracker_announce_response(
                        $response,
                        $states[$state_index]["announce_transaction_id"],
                        $states[$state_index]["tracker"]["address_family"]
                    );
                    $new_peer_count = add_runtime_tracker_peers(
                        $states[$state_index]["tracker_url"],
                        $announce_result,
                        $peer_pool,
                        $now
                    );
                    $successes++;
                    log_message(
                        "Tracker discovery complete for {$description}: added {$new_peer_count} peers; {$peer_pool->get_count()} known.",
                        $log_stream
                    );
                    fclose($states[$state_index]["socket"]);
                    unset($states[$state_index]);
                } catch(Throwable $exception) {
                    $failures++;
                    log_message("Tracker discovery failed for {$description}: {$exception->getMessage()}", $log_stream);

                    if(is_resource($states[$state_index]["socket"]))
                        fclose($states[$state_index]["socket"]);

                    unset($states[$state_index]);
                }
            }

            foreach(array_keys($states) as $state_index) {
                if($now < $states[$state_index]["deadline"])
                    continue;

                $description = $states[$state_index]["description"];

                try {
                    if(retry_parallel_udp_tracker_state($states[$state_index], $now))
                        continue;
                } catch(Throwable $exception) {
                    $failures++;
                    log_message("Tracker discovery failed for {$description}: {$exception->getMessage()}", $log_stream);

                    if(is_resource($states[$state_index]["socket"]))
                        fclose($states[$state_index]["socket"]);

                    unset($states[$state_index]);

                    continue;
                }

                $failures++;
                log_message(
                    "Tracker discovery failed for {$description}: UDP tracker request timed out after " . UDP_TRACKER_MAX_ATTEMPTS . " attempts.",
                    $log_stream
                );

                if(is_resource($states[$state_index]["socket"]))
                    fclose($states[$state_index]["socket"]);

                unset($states[$state_index]);
            }
        }
    } finally {
        foreach($states as $state) {
            if(is_resource($state["socket"]))
                fclose($state["socket"]);
        }
    }

    return [
        "attempts" => $attempts,
        "successes" => $successes,
        "failures" => $failures,
    ];
}

function discover_runtime_http_trackers_parallel(
    $tracker_urls,
    $info_hash,
    $peer_id,
    $peer_pool,
    $log_stream
) {
    if(!is_array($tracker_urls) || !($peer_pool instanceof PeerPool))
        throw new InvalidArgumentException("Parallel HTTP tracker discovery arguments are invalid.");

    if($tracker_urls === [])
        return ["attempts" => 0, "successes" => 0, "failures" => 0, "parallel" => true];

    if(!function_exists("curl_multi_init") || !function_exists("curl_init"))
        return null;

    $multi_handle = curl_multi_init();
    $handles = [];
    $attempts = 0;
    $successes = 0;
    $failures = 0;

    try {
        foreach($tracker_urls as $tracker_url) {
            if($peer_pool->get_count() >= DESIRED_KNOWN_PEERS)
                break;

            $attempts++;
            $announce_url = build_http_tracker_announce_url(
                $tracker_url,
                $info_hash,
                $peer_id,
                CLIENT_ANNOUNCE_PORT,
                0,
                0,
                TRACKER_METADATA_LEFT,
                "started",
                DESIRED_KNOWN_PEERS
            );
            $handle = curl_init();

            if($handle === false) {
                $failures++;
                log_message(
                    "Tracker discovery failed for " . describe_runtime_tracker($tracker_url) . ": cURL handle could not be created.",
                    $log_stream
                );

                continue;
            }

            curl_setopt_array($handle, [
                CURLOPT_URL => $announce_url,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_MAXREDIRS => 3,
                CURLOPT_CONNECTTIMEOUT => HTTP_TRACKER_TIMEOUT,
                CURLOPT_TIMEOUT => HTTP_TRACKER_TIMEOUT,
                CURLOPT_USERAGENT => "GreedyBitTorrentClient/1.0",
                CURLOPT_SSL_VERIFYPEER => true,
                CURLOPT_SSL_VERIFYHOST => 2,
            ]);
            curl_multi_add_handle($multi_handle, $handle);
            $handles[spl_object_id($handle)] = [
                "handle" => $handle,
                "tracker_url" => $tracker_url,
                "description" => describe_runtime_tracker($tracker_url),
            ];
        }

        if($handles !== [])
            log_message("Parallel HTTP/HTTPS tracker discovery started for " . count($handles) . " trackers.", $log_stream);

        $running = count($handles);

        while($running > 0 && $peer_pool->get_count() < DESIRED_KNOWN_PEERS) {
            do {
                $multi_status = curl_multi_exec($multi_handle, $running);
            } while(defined("CURLM_CALL_MULTI_PERFORM") && $multi_status === CURLM_CALL_MULTI_PERFORM);

            if($multi_status !== CURLM_OK)
                throw new RuntimeException("HTTP tracker multi-request processing failed.");

            while(($message = curl_multi_info_read($multi_handle)) !== false) {
                $handle = $message["handle"];
                $handle_id = spl_object_id($handle);

                if(!isset($handles[$handle_id]))
                    continue;

                $state = $handles[$handle_id];
                $body = curl_multi_getcontent($handle);
                $http_status = intval(curl_getinfo($handle, CURLINFO_RESPONSE_CODE));

                try {
                    if($message["result"] !== CURLE_OK)
                        throw new RuntimeException(curl_error($handle) ?: "HTTP tracker request failed.");

                    if($http_status < 200 || $http_status >= 300)
                        throw new RuntimeException("HTTP tracker returned status {$http_status}.");

                    if(!is_string($body))
                        throw new RuntimeException("HTTP tracker returned no response body.");

                    $result = parse_http_tracker_response($body);
                    $new_peer_count = add_runtime_tracker_peers(
                        $state["tracker_url"],
                        $result,
                        $peer_pool
                    );
                    $successes++;
                    log_message(
                        "Tracker discovery complete for {$state["description"]}: added {$new_peer_count} peers; {$peer_pool->get_count()} known.",
                        $log_stream
                    );
                } catch(Throwable $exception) {
                    $failures++;
                    log_message(
                        "Tracker discovery failed for {$state["description"]}: {$exception->getMessage()}",
                        $log_stream
                    );
                }

                curl_multi_remove_handle($multi_handle, $handle);
                unset($handles[$handle_id]);
            }

            if($running > 0 && $peer_pool->get_count() < DESIRED_KNOWN_PEERS) {
                $selected = curl_multi_select($multi_handle, 0.25);

                if($selected === -1)
                    usleep(10000);
            }
        }
    } finally {
        foreach($handles as $state) {
            curl_multi_remove_handle($multi_handle, $state["handle"]);
        }

        curl_multi_close($multi_handle);
    }

    return [
        "attempts" => $attempts,
        "successes" => $successes,
        "failures" => $failures,
        "parallel" => true,
    ];
}

function discover_runtime_trackers_sequential(
    $tracker_urls,
    $magnet,
    $local_peer_id,
    $peer_pool,
    $log_stream,
    $tracker_announcer
) {
    $attempts = 0;
    $successes = 0;
    $failures = 0;

    foreach($tracker_urls as $tracker_url) {
        if($peer_pool->get_count() >= DESIRED_KNOWN_PEERS)
            break;

        $attempts++;
        $description = describe_runtime_tracker($tracker_url);
        $previous_peer_count = $peer_pool->get_count();

        try {
            $tracker_announcer($tracker_url, $magnet->info_hash, $local_peer_id, $peer_pool);
        } catch(Throwable $exception) {
            $failures++;
            log_message("Tracker discovery failed for {$description}: {$exception->getMessage()}", $log_stream);

            continue;
        }

        $successes++;
        $new_peer_count = $peer_pool->get_count() - $previous_peer_count;
        log_message(
            "Tracker discovery complete for {$description}: added {$new_peer_count} peers; {$peer_pool->get_count()} known.",
            $log_stream
        );
    }

    return [
        "attempts" => $attempts,
        "successes" => $successes,
        "failures" => $failures,
    ];
}

function discover_runtime_peers(
    $magnet,
    $public_trackers,
    $local_peer_id,
    $peer_pool,
    $log_stream,
    $tracker_announcer = null
) {
    if(!($magnet instanceof MagnetUri))
        throw new InvalidArgumentException("Runtime peer discovery requires a parsed magnet URI.");

    if(!($peer_pool instanceof PeerPool))
        throw new InvalidArgumentException("Runtime peer discovery requires a peer pool.");

    if(!is_string($local_peer_id) || strlen($local_peer_id) !== 20)
        throw new InvalidArgumentException("Runtime peer discovery requires a 20-byte peer ID.");

    if($tracker_announcer !== null && !is_callable($tracker_announcer))
        throw new InvalidArgumentException("Runtime tracker announcer must be callable.");

    $explicit_peers_added = 0;
    $explicit_peers_rejected = 0;

    foreach($magnet->explicit_peers as $endpoint) {
        $previous_peer_count = $peer_pool->get_count();

        try {
            $peer_pool->add_peer($endpoint, "magnet:x.pe");
        } catch(Throwable) {
            $explicit_peers_rejected++;

            continue;
        }

        if($peer_pool->get_count() > $previous_peer_count)
            $explicit_peers_added++;
    }

    if($magnet->explicit_peers !== []) {
        log_message(
            sprintf(
                "Magnet peers: %d added, %d rejected.",
                $explicit_peers_added,
                $explicit_peers_rejected
            ),
            $log_stream
        );
    }

    $trackers = build_runtime_tracker_list($magnet, $public_trackers);
    $tracker_attempts = 0;
    $tracker_successes = 0;
    $tracker_failures = 0;

    if($tracker_announcer !== null) {
        $result = discover_runtime_trackers_sequential(
            $trackers,
            $magnet,
            $local_peer_id,
            $peer_pool,
            $log_stream,
            $tracker_announcer
        );
        $tracker_attempts += $result["attempts"];
        $tracker_successes += $result["successes"];
        $tracker_failures += $result["failures"];
    } else {
        $udp_trackers = [];
        $http_trackers = [];

        foreach($trackers as $tracker_url) {
            $scheme = strtolower(strval(parse_url($tracker_url, PHP_URL_SCHEME)));

            if($scheme === "udp")
                $udp_trackers[] = $tracker_url;
            elseif($scheme === "http" || $scheme === "https")
                $http_trackers[] = $tracker_url;
        }

        if($udp_trackers !== [] && $peer_pool->get_count() < DESIRED_KNOWN_PEERS) {
            $result = discover_runtime_udp_trackers_parallel(
                $udp_trackers,
                $magnet->info_hash,
                $local_peer_id,
                $peer_pool,
                $log_stream
            );
            $tracker_attempts += $result["attempts"];
            $tracker_successes += $result["successes"];
            $tracker_failures += $result["failures"];
        }

        if($http_trackers !== [] && $peer_pool->get_count() < DESIRED_KNOWN_PEERS) {
            $result = discover_runtime_http_trackers_parallel(
                $http_trackers,
                $magnet->info_hash,
                $local_peer_id,
                $peer_pool,
                $log_stream
            );

            if($result === null) {
                log_message(
                    "PHP cURL is unavailable; HTTP/HTTPS tracker discovery is using the sequential fallback.",
                    $log_stream
                );
                $result = discover_runtime_trackers_sequential(
                    $http_trackers,
                    $magnet,
                    $local_peer_id,
                    $peer_pool,
                    $log_stream,
                    "announce_runtime_tracker"
                );
            }

            $tracker_attempts += $result["attempts"];
            $tracker_successes += $result["successes"];
            $tracker_failures += $result["failures"];
        }
    }

    return [
        "trackers" => $trackers,
        "explicit_peers_added" => $explicit_peers_added,
        "explicit_peers_rejected" => $explicit_peers_rejected,
        "tracker_attempts" => $tracker_attempts,
        "tracker_successes" => $tracker_successes,
        "tracker_failures" => $tracker_failures,
        "known_peers" => $peer_pool->get_count(),
    ];
}

final class RuntimeTrackerDiscovery {
    private readonly MagnetUri $magnet;
    private readonly array $public_trackers;
    private readonly string $local_peer_id;
    private readonly PeerPool $peer_pool;
    private readonly int $announce_port;
    private $log_stream;
    private array $trackers = [];
    private array $udp_states = [];
    private $http_multi_handle = null;
    private array $http_handles = [];
    private array $active_tracker_urls = [];
    private array $tracker_schedules = [];
    private array $tracker_sources = [];
    private array $pending_public_trackers = [];
    private ?float $next_public_tracker_start_at = null;
    private ?string $last_discovery_mode = null;
    private int $magnet_trackers_started = 0;
    private int $public_trackers_started = 0;
    private bool $started = false;
    private bool $closed = false;
    private int $tracker_attempts = 0;
    private int $tracker_successes = 0;
    private int $tracker_failures = 0;
    private int $initial_remaining = 0;
    private int $explicit_peers_added = 0;
    private int $explicit_peers_rejected = 0;
    private int $tracker_duplicates_removed = 0;
    private array $successful_tracker_urls = [];
    private array $lifecycle_event_results = [];
    private int $downloaded = 0;
    private int $uploaded = 0;
    private int $left = TRACKER_METADATA_LEFT;

    public function __construct($magnet, $public_trackers, $local_peer_id, $peer_pool, $log_stream, $announce_port = CLIENT_ANNOUNCE_PORT) {
        if(!($magnet instanceof MagnetUri))
            throw new InvalidArgumentException("Runtime tracker discovery requires a parsed magnet URI.");

        if(!is_array($public_trackers))
            throw new InvalidArgumentException("Runtime tracker discovery requires a public tracker list.");

        if(!is_string($local_peer_id) || strlen($local_peer_id) !== 20)
            throw new InvalidArgumentException("Runtime tracker discovery requires a 20-byte peer ID.");

        if(!($peer_pool instanceof PeerPool))
            throw new InvalidArgumentException("Runtime tracker discovery requires a peer pool.");

        if(!is_resource($log_stream))
            throw new InvalidArgumentException("Runtime tracker discovery requires a log stream.");

        if(!is_int($announce_port) || $announce_port < 1 || $announce_port > 65535)
            throw new InvalidArgumentException("Runtime tracker discovery requires a valid announce port.");

        $this->magnet = $magnet;
        $this->public_trackers = $public_trackers;
        $this->local_peer_id = $local_peer_id;
        $this->peer_pool = $peer_pool;
        $this->announce_port = $announce_port;
        $this->log_stream = $log_stream;
        $this->trackers = build_runtime_tracker_list($magnet, $public_trackers);
        $this->tracker_duplicates_removed = count_runtime_tracker_duplicates($magnet, $public_trackers);
        $magnet_tracker_keys = [];

        foreach($magnet->trackers as $tracker) {
            $tracker_key = tracker_deduplication_key($tracker);

            if($tracker_key !== null)
                $magnet_tracker_keys[$tracker_key] = true;
        }

        foreach($this->trackers as $tracker_url) {
            $tracker_key = tracker_deduplication_key($tracker_url);
            $source = $tracker_key !== null && isset($magnet_tracker_keys[$tracker_key])
                ? "magnet"
                : "public";
            $this->tracker_sources[$tracker_url] = $source;

            if($source === "public")
                $this->pending_public_trackers[] = $tracker_url;
        }
    }

    private function add_explicit_peers() {
        foreach($this->magnet->explicit_peers as $endpoint) {
            $previous_peer_count = $this->peer_pool->get_count();

            try {
                $this->peer_pool->add_peer($endpoint, "magnet:x.pe");
            } catch(Throwable) {
                $this->explicit_peers_rejected++;

                continue;
            }

            if($this->peer_pool->get_count() > $previous_peer_count)
                $this->explicit_peers_added++;
        }

        if($this->magnet->explicit_peers !== []) {
            log_message(
                sprintf(
                    "Magnet peers: %d added, %d rejected.",
                    $this->explicit_peers_added,
                    $this->explicit_peers_rejected
                ),
                $this->log_stream
            );
        }
    }

    private function ensure_http_multi_handle() {
        if($this->http_multi_handle !== null)
            return true;

        if(!function_exists("curl_multi_init") || !function_exists("curl_init"))
            return false;

        $this->http_multi_handle = curl_multi_init();

        return $this->http_multi_handle !== false;
    }

    private function record_tracker_success($tracker_url, $event) {
        $this->successful_tracker_urls[$tracker_url] = true;

        if($event === "completed" || $event === "stopped")
            $this->lifecycle_event_results[$event][$tracker_url] = true;
    }

    private function cancel_active_requests() {
        foreach($this->udp_states as $state) {
            if(is_resource($state["socket"]))
                fclose($state["socket"]);
        }

        $this->udp_states = [];

        if($this->http_multi_handle !== null) {
            foreach($this->http_handles as $state)
                curl_multi_remove_handle($this->http_multi_handle, $state["handle"]);
        }

        $this->http_handles = [];
        $this->active_tracker_urls = [];
    }

    private function mark_request_finished($tracker_url, $event, $success, $interval = null, $now = null) {
        unset($this->active_tracker_urls[$tracker_url]);

        if($event === "started" && $this->initial_remaining > 0)
            $this->initial_remaining--;

        if(!$success || $interval === null)
            return;

        $now = normalise_peer_time($now);
        $interval = max(1, intval($interval));
        $this->tracker_schedules[$tracker_url] = [
            "interval" => $interval,
            "next_announce_at" => $now + $interval,
        ];
    }

    private function log_tracker_failure($tracker_url, $event, $message) {
        $this->tracker_failures++;
        log_message(
            "Tracker discovery failed for " . describe_runtime_tracker($tracker_url) . ": {$message}",
            $this->log_stream
        );
        $this->mark_request_finished($tracker_url, $event, false);
    }

    private function start_udp_request($tracker_url, $event, $now) {
        try {
            $state = open_parallel_udp_tracker_state(
                $tracker_url,
                $this->magnet->info_hash,
                $this->local_peer_id,
                $now,
                $event,
                $this->downloaded,
                $this->uploaded,
                $this->left,
                $this->announce_port
            );
        } catch(Throwable $exception) {
            $this->log_tracker_failure($tracker_url, $event, $exception->getMessage());

            return false;
        }

        $state["event"] = $event;
        $this->udp_states[$tracker_url] = $state;
        $this->active_tracker_urls[$tracker_url] = true;

        return true;
    }

    private function start_http_request($tracker_url, $event, $now) {
        if(!$this->ensure_http_multi_handle()) {
            $this->log_tracker_failure(
                $tracker_url,
                $event,
                "PHP cURL is unavailable; non-blocking HTTP tracker discovery cannot start."
            );

            return false;
        }

        try {
            $announce_url = build_http_tracker_announce_url(
                $tracker_url,
                $this->magnet->info_hash,
                $this->local_peer_id,
                $this->announce_port,
                $this->uploaded,
                $this->downloaded,
                $this->left,
                $event,
                DESIRED_KNOWN_PEERS
            );
        } catch(Throwable $exception) {
            $this->log_tracker_failure($tracker_url, $event, $exception->getMessage());

            return false;
        }

        $handle = curl_init();

        if($handle === false) {
            $this->log_tracker_failure($tracker_url, $event, "cURL handle could not be created.");

            return false;
        }

        curl_setopt_array($handle, [
            CURLOPT_URL => $announce_url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS => 3,
            CURLOPT_CONNECTTIMEOUT => HTTP_TRACKER_TIMEOUT,
            CURLOPT_TIMEOUT => HTTP_TRACKER_TIMEOUT,
            CURLOPT_USERAGENT => "GreedyBitTorrentClient/1.0",
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_NOSIGNAL => true,
        ]);

        $status = curl_multi_add_handle($this->http_multi_handle, $handle);

        if($status !== CURLM_OK) {
            $this->log_tracker_failure($tracker_url, $event, "cURL multi handle could not accept the tracker request.");

            return false;
        }

        $this->http_handles[spl_object_id($handle)] = [
            "handle" => $handle,
            "tracker_url" => $tracker_url,
            "description" => describe_runtime_tracker($tracker_url),
            "event" => $event,
            "started_at" => $now,
        ];
        $this->active_tracker_urls[$tracker_url] = true;

        return true;
    }

    private function start_tracker_request($tracker_url, $event, $now) {
        if(isset($this->active_tracker_urls[$tracker_url]))
            return false;

        unset($this->tracker_schedules[$tracker_url]);
        $this->tracker_attempts++;
        $scheme = strtolower(strval(parse_url($tracker_url, PHP_URL_SCHEME)));

        if($scheme === "udp")
            return $this->start_udp_request($tracker_url, $event, $now);

        if($scheme === "http" || $scheme === "https")
            return $this->start_http_request($tracker_url, $event, $now);

        $this->log_tracker_failure($tracker_url, $event, "unsupported tracker scheme.");

        return false;
    }

    private function start_due_reannounces($now) {
        foreach(array_keys($this->tracker_schedules) as $tracker_url) {
            if(!isset($this->tracker_schedules[$tracker_url]))
                continue;

            if($now < $this->tracker_schedules[$tracker_url]["next_announce_at"])
                continue;

            $this->start_tracker_request($tracker_url, "", $now);
        }
    }

    private function get_active_public_tracker_count() {
        $count = 0;

        foreach(array_keys($this->active_tracker_urls) as $tracker_url) {
            if(($this->tracker_sources[$tracker_url] ?? null) === "public")
                $count++;
        }

        return $count;
    }

    private function log_discovery_mode_if_changed($state) {
        $mode = $state["mode"];

        if($mode === $this->last_discovery_mode)
            return;

        $this->last_discovery_mode = $mode;
        log_message(
            sprintf(
                "Discovery demand is %s: %d known peers, %d connected/connecting, target %d connected; suppliers %d established, %d unchoked, %d recently useful%s.",
                $mode,
                $state["known_peers"],
                $state["active_connections"],
                $state["connection_target"],
                $state["connected_peers"],
                $state["unchoked_peers"],
                $state["useful_peers"],
                $state["supplier_starved"] ? "; supplier-starved" : ""
            ),
            $this->log_stream
        );
    }

    private function start_pending_public_trackers($now, $initial = false) {
        if($this->pending_public_trackers === [])
            return 0;

        $state = runtime_discovery_market_state($this->peer_pool);
        $this->log_discovery_mode_if_changed($state);
        $mode = $state["mode"];
        $target_concurrency = runtime_discovery_tracker_concurrency($mode);
        $active_public = $this->get_active_public_tracker_count();

        if($active_public >= $target_concurrency)
            return 0;

        if($this->next_public_tracker_start_at !== null && $now < $this->next_public_tracker_start_at)
            return 0;

        $available_slots = $target_concurrency - $active_public;
        $max_starts = $initial ? min(2, $available_slots) : 1;
        $started = 0;

        while($started < $max_starts && $this->pending_public_trackers !== []) {
            $tracker_url = array_shift($this->pending_public_trackers);

            if($this->start_tracker_request($tracker_url, "started", $now)) {
                $this->public_trackers_started++;
                $started++;
            }

            $this->next_public_tracker_start_at = $now + runtime_discovery_tracker_stagger_seconds($mode);

            // A failed start is still an attempted discovery source; stagger before trying another.
            if($started === 0)
                break;
        }

        return $started;
    }

    public function start() {
        if($this->closed)
            throw new LogicException("Runtime tracker discovery is closed.");

        if($this->started)
            return false;

        $this->started = true;
        $this->add_explicit_peers();
        log_message(
            sprintf(
                "Tracker set: %d unique tracker%s; %d duplicate entr%s removed across magnet and public sources.",
                count($this->trackers),
                count($this->trackers) === 1 ? "" : "s",
                $this->tracker_duplicates_removed,
                $this->tracker_duplicates_removed === 1 ? "y" : "ies"
            ),
            $this->log_stream
        );
        $this->initial_remaining = count($this->trackers);
        $now = microtime(true);

        foreach($this->trackers as $tracker_url) {
            if(($this->tracker_sources[$tracker_url] ?? null) !== "magnet")
                continue;

            if($this->start_tracker_request($tracker_url, "started", $now))
                $this->magnet_trackers_started++;
        }

        $public_started = $this->start_pending_public_trackers($now, true);
        log_message(
            sprintf(
                "Demand-driven tracker discovery: %d magnet tracker%s started immediately; %d/%d public tracker%s started, remainder staggered by market demand.",
                $this->magnet_trackers_started,
                $this->magnet_trackers_started === 1 ? "" : "s",
                $public_started,
                count(array_filter($this->tracker_sources, static fn($source) => $source === "public")),
                count(array_filter($this->tracker_sources, static fn($source) => $source === "public")) === 1 ? "" : "s"
            ),
            $this->log_stream
        );

        return true;
    }

    private function poll_http() {
        if($this->http_multi_handle === null || $this->http_handles === [])
            return;

        do {
            $multi_status = curl_multi_exec($this->http_multi_handle, $running);
        } while(defined("CURLM_CALL_MULTI_PERFORM") && $multi_status === CURLM_CALL_MULTI_PERFORM);

        if($multi_status !== CURLM_OK)
            throw new RuntimeException("HTTP tracker multi-request processing failed.");

        while(($message = curl_multi_info_read($this->http_multi_handle)) !== false) {
            $handle = $message["handle"];
            $handle_id = spl_object_id($handle);

            if(!isset($this->http_handles[$handle_id]))
                continue;

            $state = $this->http_handles[$handle_id];
            $body = curl_multi_getcontent($handle);
            $http_status = intval(curl_getinfo($handle, CURLINFO_RESPONSE_CODE));
            $success = false;
            $interval = null;

            try {
                if($message["result"] !== CURLE_OK)
                    throw new RuntimeException(curl_error($handle) ?: "HTTP tracker request failed.");

                if($http_status < 200 || $http_status >= 300)
                    throw new RuntimeException("HTTP tracker returned status {$http_status}.");

                if(!is_string($body))
                    throw new RuntimeException("HTTP tracker returned no response body.");

                $result = parse_http_tracker_response($body);
                $new_peer_count = add_runtime_tracker_peers(
                    $state["tracker_url"],
                    $result,
                    $this->peer_pool
                );
                $this->tracker_successes++;
                $success = true;
                $interval = $result["interval"];
                $this->record_tracker_success($state["tracker_url"], $state["event"]);
                $operation = $state["event"] === "completed" || $state["event"] === "stopped"
                    ? "Tracker {$state["event"]} announce complete"
                    : "Tracker discovery complete";
                log_message(
                    "{$operation} for {$state["description"]}: added {$new_peer_count} peers; {$this->peer_pool->get_count()} known.",
                    $this->log_stream
                );
            } catch(Throwable $exception) {
                $this->tracker_failures++;
                log_message(
                    "Tracker discovery failed for {$state["description"]}: {$exception->getMessage()}",
                    $this->log_stream
                );
            }

            curl_multi_remove_handle($this->http_multi_handle, $handle);
            unset($this->http_handles[$handle_id]);
            $this->mark_request_finished(
                $state["tracker_url"],
                $state["event"],
                $success,
                $interval,
                microtime(true)
            );
        }
    }

    private function poll_udp($timeout_microseconds) {
        $read_sockets = [];
        $socket_tracker_urls = [];

        foreach($this->udp_states as $tracker_url => $state) {
            if(!is_resource($state["socket"]))
                continue;

            $read_sockets[] = $state["socket"];
            $socket_tracker_urls[get_resource_id($state["socket"])] = $tracker_url;
        }

        if($read_sockets !== []) {
            $write_sockets = null;
            $except_sockets = null;
            $timeout_seconds = intdiv($timeout_microseconds, 1000000);
            $timeout_remainder = $timeout_microseconds % 1000000;
            $selected = @stream_select(
                $read_sockets,
                $write_sockets,
                $except_sockets,
                $timeout_seconds,
                $timeout_remainder
            );

            if($selected === false)
                throw new RuntimeException("UDP tracker socket selection failed.");
        } elseif($timeout_microseconds > 0 && $this->http_multi_handle !== null && $this->http_handles !== []) {
            $selected = curl_multi_select($this->http_multi_handle, $timeout_microseconds / 1000000);

            if($selected === -1)
                usleep(min($timeout_microseconds, 10000));
        } elseif($timeout_microseconds > 0) {
            usleep(min($timeout_microseconds, 10000));
        }

        $now = microtime(true);

        foreach($read_sockets as $socket) {
            $socket_id = get_resource_id($socket);

            if(!isset($socket_tracker_urls[$socket_id]))
                continue;

            $tracker_url = $socket_tracker_urls[$socket_id];

            if(!isset($this->udp_states[$tracker_url]))
                continue;

            $response = @fread($socket, 65535);

            if(!is_string($response) || $response === "")
                continue;

            $state = &$this->udp_states[$tracker_url];
            $success = false;
            $interval = null;

            try {
                if($state["stage"] === "connect") {
                    $connect_result = parse_udp_tracker_connect_response(
                        $response,
                        $state["connect_transaction_id"]
                    );
                    begin_parallel_udp_tracker_announce(
                        $state,
                        $connect_result["connection_id"],
                        $this->magnet->info_hash,
                        $this->local_peer_id,
                        $now
                    );
                    unset($state);

                    continue;
                }

                $announce_result = parse_udp_tracker_announce_response(
                    $response,
                    $state["announce_transaction_id"],
                    $state["tracker"]["address_family"]
                );
                $new_peer_count = add_runtime_tracker_peers(
                    $tracker_url,
                    $announce_result,
                    $this->peer_pool,
                    $now
                );
                $this->tracker_successes++;
                $success = true;
                $interval = $announce_result["interval"];
                $this->record_tracker_success($tracker_url, $state["event"]);
                $operation = $state["event"] === "completed" || $state["event"] === "stopped"
                    ? "Tracker {$state["event"]} announce complete"
                    : "Tracker discovery complete";
                log_message(
                    "{$operation} for {$state["description"]}: added {$new_peer_count} peers; {$this->peer_pool->get_count()} known.",
                    $this->log_stream
                );
            } catch(Throwable $exception) {
                $this->tracker_failures++;
                log_message(
                    "Tracker discovery failed for {$state["description"]}: {$exception->getMessage()}",
                    $this->log_stream
                );
            }

            $event = $state["event"];

            if(is_resource($state["socket"]))
                fclose($state["socket"]);

            unset($state);
            unset($this->udp_states[$tracker_url]);
            $this->mark_request_finished($tracker_url, $event, $success, $interval, $now);
        }

        foreach(array_keys($this->udp_states) as $tracker_url) {
            if(!isset($this->udp_states[$tracker_url]))
                continue;

            if($now < $this->udp_states[$tracker_url]["deadline"])
                continue;

            $event = $this->udp_states[$tracker_url]["event"];

            try {
                if(retry_parallel_udp_tracker_state($this->udp_states[$tracker_url], $now))
                    continue;
            } catch(Throwable $exception) {
                if(is_resource($this->udp_states[$tracker_url]["socket"]))
                    fclose($this->udp_states[$tracker_url]["socket"]);

                unset($this->udp_states[$tracker_url]);
                $this->log_tracker_failure($tracker_url, $event, $exception->getMessage());

                continue;
            }

            if(is_resource($this->udp_states[$tracker_url]["socket"]))
                fclose($this->udp_states[$tracker_url]["socket"]);

            unset($this->udp_states[$tracker_url]);
            $this->log_tracker_failure(
                $tracker_url,
                $event,
                "UDP tracker request timed out after " . UDP_TRACKER_MAX_ATTEMPTS . " attempts."
            );
        }
    }

    public function poll($timeout_microseconds = 0) {
        if(!$this->started || $this->closed)
            return false;

        if(!is_int($timeout_microseconds) || $timeout_microseconds < 0)
            throw new InvalidArgumentException("Tracker discovery poll timeout must be a non-negative integer.");

        $now = microtime(true);
        $this->start_pending_public_trackers($now);
        $this->start_due_reannounces($now);
        $this->poll_http();
        $this->poll_udp($timeout_microseconds);
        $this->poll_http();

        return true;
    }

    public function announce_lifecycle_event($event, $timeout_seconds = TRACKER_LIFECYCLE_ANNOUNCE_TIMEOUT) {
        if(!is_string($event) || !in_array($event, ["completed", "stopped"], true))
            throw new InvalidArgumentException("Tracker lifecycle event must be completed or stopped.");

        if((!is_int($timeout_seconds) && !is_float($timeout_seconds)) || !is_finite(floatval($timeout_seconds)) || $timeout_seconds < 0)
            throw new InvalidArgumentException("Tracker lifecycle timeout must be a finite non-negative number of seconds.");

        $result = [
            "event" => $event,
            "attempted" => 0,
            "succeeded" => 0,
            "failed" => 0,
        ];

        if(!$this->started || $this->closed)
            return $result;

        $tracker_urls = array_keys($this->successful_tracker_urls);
        $this->cancel_active_requests();
        $this->tracker_schedules = [];
        $this->pending_public_trackers = [];
        $this->next_public_tracker_start_at = null;

        if($tracker_urls === []) {
            log_message("Tracker {$event} announce: no previously successful tracker to notify.", $this->log_stream);

            return $result;
        }

        unset($this->lifecycle_event_results[$event]);
        $now = microtime(true);

        foreach($tracker_urls as $tracker_url) {
            $result["attempted"]++;
            $this->start_tracker_request($tracker_url, $event, $now);
        }

        $deadline = microtime(true) + floatval($timeout_seconds);

        while($this->has_active_requests() && microtime(true) < $deadline) {
            $this->poll_http();
            $remaining_microseconds = max(0, intval(($deadline - microtime(true)) * 1000000));
            $this->poll_udp(min(50000, $remaining_microseconds));
            $this->poll_http();
        }

        if($this->has_active_requests())
            $this->cancel_active_requests();

        $result["succeeded"] = count($this->lifecycle_event_results[$event] ?? []);
        $result["failed"] = max(0, $result["attempted"] - $result["succeeded"]);
        log_message(
            sprintf(
                "Tracker %s announce: %d/%d successful; %d failed or timed out within %.2fs.",
                $event,
                $result["succeeded"],
                $result["attempted"],
                $result["failed"],
                floatval($timeout_seconds)
            ),
            $this->log_stream
        );

        return $result;
    }

    public function set_transfer_counters($downloaded, $uploaded, $left) {
        validate_tracker_counter("downloaded byte count", $downloaded);
        validate_tracker_counter("uploaded byte count", $uploaded);
        validate_tracker_counter("remaining byte count", $left);
        $this->downloaded = $downloaded;
        $this->uploaded = $uploaded;
        $this->left = $left;
    }

    public function has_pending_initial() {
        return $this->initial_remaining > 0;
    }

    public function has_active_requests() {
        return $this->udp_states !== [] || $this->http_handles !== [];
    }

    public function get_stats() {
        return [
            "trackers" => $this->trackers,
            "explicit_peers_added" => $this->explicit_peers_added,
            "explicit_peers_rejected" => $this->explicit_peers_rejected,
            "tracker_attempts" => $this->tracker_attempts,
            "tracker_successes" => $this->tracker_successes,
            "tracker_failures" => $this->tracker_failures,
            "pending_initial" => $this->initial_remaining,
            "active_requests" => count($this->udp_states) + count($this->http_handles),
            "scheduled_reannounces" => count($this->tracker_schedules),
            "pending_public_trackers" => count($this->pending_public_trackers),
            "magnet_trackers_started" => $this->magnet_trackers_started,
            "public_trackers_started" => $this->public_trackers_started,
            "discovery_mode" => runtime_discovery_market_state($this->peer_pool)["mode"],
            "known_peers" => $this->peer_pool->get_count(),
            "successful_tracker_count" => count($this->successful_tracker_urls),
            "lifecycle_events" => $this->lifecycle_event_results,
        ];
    }

    public function is_closed() {
        return $this->closed;
    }

    public function close() {
        if($this->closed)
            return false;

        $this->cancel_active_requests();

        if($this->http_multi_handle !== null) {
            curl_multi_close($this->http_multi_handle);
            $this->http_multi_handle = null;
        }
        $this->tracker_schedules = [];
        $this->closed = true;

        return true;
    }

    public function __destruct() {
        $this->close();
    }
}

// BEP 5 DHT peer discovery.
function validate_dht_node_id($node_id, $name = "DHT node ID") {
    if(!is_string($node_id) || strlen($node_id) !== 20)
        throw new InvalidArgumentException("{$name} must contain exactly 20 bytes.");
}

function generate_dht_node_id() {
    return random_bytes(20);
}

function dht_xor_distance($left, $right) {
    validate_dht_node_id($left, "Left DHT ID");
    validate_dht_node_id($right, "Right DHT ID");

    return $left ^ $right;
}

function compare_dht_distance($left_id, $right_id, $target_id) {
    validate_dht_node_id($left_id, "Left DHT node ID");
    validate_dht_node_id($right_id, "Right DHT node ID");
    validate_dht_node_id($target_id, "DHT distance target");

    return strcmp(dht_xor_distance($left_id, $target_id), dht_xor_distance($right_id, $target_id));
}

function dht_bucket_index($local_node_id, $remote_node_id) {
    validate_dht_node_id($local_node_id, "Local DHT node ID");
    validate_dht_node_id($remote_node_id, "Remote DHT node ID");

    $distance = dht_xor_distance($local_node_id, $remote_node_id);

    for($byte_index = 0; $byte_index < 20; $byte_index++) {
        $byte = ord($distance[$byte_index]);

        if($byte === 0)
            continue;

        for($bit = 7; $bit >= 0; $bit--) {
            if(($byte & (1 << $bit)) !== 0)
                return ($byte_index * 8) + (7 - $bit);
        }
    }

    return null;
}

function parse_compact_dht_nodes($compact_nodes) {
    if(!is_string($compact_nodes))
        throw new InvalidArgumentException("Compact DHT nodes must be a byte string.");

    if(strlen($compact_nodes) % 26 !== 0)
        throw new InvalidArgumentException("Compact DHT node data has an incomplete record.");

    $nodes = [];

    for($offset = 0; $offset < strlen($compact_nodes); $offset += 26) {
        $node_id = substr($compact_nodes, $offset, 20);
        $host = inet_ntop(substr($compact_nodes, $offset + 20, 4));
        $port_data = unpack("nport", substr($compact_nodes, $offset + 24, 2));

        if($host === false || !is_array($port_data) || $port_data["port"] < 1)
            throw new InvalidArgumentException("Compact DHT node record is invalid.");

        $nodes[] = [
            "node_id" => $node_id,
            "endpoint" => new PeerEndpoint($host, $port_data["port"]),
        ];
    }

    return $nodes;
}

function encode_compact_dht_nodes($nodes) {
    if(!is_array($nodes))
        throw new InvalidArgumentException("DHT node list must be an array.");

    $compact = "";

    foreach($nodes as $node) {
        if($node instanceof DhtNodeContact) {
            $node_id = $node->node_id;
            $endpoint = $node->endpoint;
        } elseif(is_array($node) && isset($node["node_id"], $node["endpoint"])) {
            $node_id = $node["node_id"];
            $endpoint = $node["endpoint"];
        } else {
            throw new InvalidArgumentException("DHT compact-node entry is invalid.");
        }

        validate_dht_node_id($node_id);

        if(!($endpoint instanceof PeerEndpoint) || $endpoint->is_ipv6)
            continue;

        $packed_host = @inet_pton($endpoint->host);

        if($packed_host === false || strlen($packed_host) !== 4)
            continue;

        $compact .= $node_id . $packed_host . pack("n", $endpoint->port);
    }

    return $compact;
}

function parse_dht_socket_endpoint($source) {
    if(!is_string($source) || trim($source) === "")
        throw new InvalidArgumentException("DHT UDP source endpoint is invalid.");

    $endpoint = PeerEndpoint::from_string($source);

    if($endpoint->is_ipv6)
        throw new InvalidArgumentException("DHT currently supports IPv4 nodes only.");

    return $endpoint;
}

function validate_dht_transaction_id($transaction_id) {
    if(!is_string($transaction_id) || $transaction_id === "" || strlen($transaction_id) > 8)
        throw new InvalidArgumentException("DHT transaction ID must be a short non-empty byte string.");
}

function encode_dht_query($transaction_id, $query, $arguments) {
    validate_dht_transaction_id($transaction_id);

    if(!is_string($query) || !in_array($query, ["ping", "find_node", "get_peers", "announce_peer"], true))
        throw new InvalidArgumentException("Unsupported DHT query name.");

    if(!is_array($arguments) || ($arguments !== [] && array_is_list($arguments)))
        throw new InvalidArgumentException("DHT query arguments must be a dictionary.");

    return bencode_encode(new BencodeDictionary([
        "a" => new BencodeDictionary($arguments),
        "q" => $query,
        "t" => $transaction_id,
        "v" => DHT_CLIENT_VERSION,
        "y" => "q",
    ]));
}

function encode_dht_response($transaction_id, $response) {
    validate_dht_transaction_id($transaction_id);

    if(!is_array($response) || ($response !== [] && array_is_list($response)))
        throw new InvalidArgumentException("DHT response values must be a dictionary.");

    return bencode_encode(new BencodeDictionary([
        "r" => new BencodeDictionary($response),
        "t" => $transaction_id,
        "v" => DHT_CLIENT_VERSION,
        "y" => "r",
    ]));
}

function encode_dht_error($transaction_id, $code, $message) {
    validate_dht_transaction_id($transaction_id);

    if(!is_int($code) || $code < 200 || $code > 299)
        throw new InvalidArgumentException("DHT error code must be a 2xx integer.");

    if(!is_string($message) || trim($message) === "")
        throw new InvalidArgumentException("DHT error message must be non-empty.");

    return bencode_encode(new BencodeDictionary([
        "e" => [$code, $message],
        "t" => $transaction_id,
        "v" => DHT_CLIENT_VERSION,
        "y" => "e",
    ]));
}

function parse_dht_message($packet) {
    if(!is_string($packet) || $packet === "" || strlen($packet) > DHT_MAX_PACKET_LENGTH)
        throw new InvalidArgumentException("DHT packet is empty or too large.");

    $message = bencode_decode($packet);

    if(!is_array($message) || array_is_list($message))
        throw new RuntimeException("DHT packet must contain a bencoded dictionary.");

    if(!isset($message["t"], $message["y"]) || !is_string($message["t"]) || !is_string($message["y"]))
        throw new RuntimeException("DHT packet is missing transaction or message type fields.");

    validate_dht_transaction_id($message["t"]);

    if($message["y"] === "q") {
        if(!isset($message["q"], $message["a"]) || !is_string($message["q"]) || !is_array($message["a"]) || array_is_list($message["a"]))
            throw new RuntimeException("DHT query packet is malformed.");
    } elseif($message["y"] === "r") {
        if(!isset($message["r"]) || !is_array($message["r"]) || array_is_list($message["r"]))
            throw new RuntimeException("DHT response packet is malformed.");
    } elseif($message["y"] === "e") {
        if(!isset($message["e"]) || !is_array($message["e"]) || count($message["e"]) !== 2)
            throw new RuntimeException("DHT error packet is malformed.");
    } else {
        throw new RuntimeException("DHT packet has an unsupported message type.");
    }

    return $message;
}

final class DhtNodeContact {
    public string $node_id;
    public PeerEndpoint $endpoint;
    public float $first_seen_at;
    public float $last_seen_at;
    public ?float $last_response_at = null;
    public ?float $last_query_at = null;
    public int $failure_count = 0;

    public function __construct($node_id, $endpoint, $seen_at = null) {
        validate_dht_node_id($node_id);

        if(!($endpoint instanceof PeerEndpoint) || $endpoint->is_ipv6)
            throw new InvalidArgumentException("DHT node contact requires an IPv4 endpoint.");

        $seen_at = normalise_peer_time($seen_at);
        $this->node_id = $node_id;
        $this->endpoint = $endpoint;
        $this->first_seen_at = $seen_at;
        $this->last_seen_at = $seen_at;
    }

    public function mark_response($seen_at = null) {
        $seen_at = normalise_peer_time($seen_at);
        $this->last_seen_at = max($this->last_seen_at, $seen_at);
        $this->last_response_at = $seen_at;
        $this->failure_count = 0;
    }

    public function mark_query($seen_at = null) {
        $seen_at = normalise_peer_time($seen_at);
        $this->last_seen_at = max($this->last_seen_at, $seen_at);
        $this->last_query_at = $seen_at;
    }

    public function mark_failure() {
        $this->failure_count++;
    }

    public function is_good($now = null) {
        $now = normalise_peer_time($now);
        $recent_response = $this->last_response_at !== null
            && $now - $this->last_response_at <= DHT_NODE_GOOD_SECONDS;
        $recent_query = $this->last_response_at !== null
            && $this->last_query_at !== null
            && $now - $this->last_query_at <= DHT_NODE_GOOD_SECONDS;

        return $this->failure_count < DHT_NODE_BAD_FAILURES && ($recent_response || $recent_query);
    }

    public function is_bad() {
        return $this->failure_count >= DHT_NODE_BAD_FAILURES;
    }
}

final class DhtRoutingTable {
    public readonly string $local_node_id;
    private array $buckets = [];
    private array $contacts = [];

    public function __construct($local_node_id) {
        validate_dht_node_id($local_node_id, "Local DHT node ID");
        $this->local_node_id = $local_node_id;
    }

    public function add_node($node_id, $endpoint, $seen_at = null, $responded = false) {
        validate_dht_node_id($node_id);

        if(!($endpoint instanceof PeerEndpoint) || $endpoint->is_ipv6)
            return null;

        if(hash_equals($this->local_node_id, $node_id))
            return null;

        $seen_at = normalise_peer_time($seen_at);
        $key = $endpoint->key;

        if(isset($this->contacts[$key])) {
            $contact = $this->contacts[$key];

            if(!hash_equals($contact->node_id, $node_id)) {
                $old_bucket_index = dht_bucket_index($this->local_node_id, $contact->node_id);

                if($old_bucket_index !== null)
                    unset($this->buckets[$old_bucket_index][$key]);

                $contact->node_id = $node_id;
            }

            $contact->last_seen_at = max($contact->last_seen_at, $seen_at);

            if($responded)
                $contact->mark_response($seen_at);

            $bucket_index = dht_bucket_index($this->local_node_id, $node_id);

            if($bucket_index !== null)
                $this->buckets[$bucket_index][$key] = $contact;

            return $contact;
        }

        $bucket_index = dht_bucket_index($this->local_node_id, $node_id);

        if($bucket_index === null)
            return null;

        if(!isset($this->buckets[$bucket_index]))
            $this->buckets[$bucket_index] = [];

        if(count($this->buckets[$bucket_index]) >= DHT_K) {
            $replacement_key = null;
            $oldest_seen_at = INF;

            foreach($this->buckets[$bucket_index] as $candidate_key => $candidate) {
                if($candidate->is_bad()) {
                    $replacement_key = $candidate_key;

                    break;
                }

                if(!$candidate->is_good($seen_at) && $candidate->last_seen_at < $oldest_seen_at) {
                    $oldest_seen_at = $candidate->last_seen_at;
                    $replacement_key = $candidate_key;
                }
            }

            if($replacement_key === null)
                return null;

            unset($this->contacts[$replacement_key]);
            unset($this->buckets[$bucket_index][$replacement_key]);
        }

        $contact = new DhtNodeContact($node_id, $endpoint, $seen_at);

        if($responded)
            $contact->mark_response($seen_at);

        $this->contacts[$key] = $contact;
        $this->buckets[$bucket_index][$key] = $contact;

        return $contact;
    }

    public function mark_response($endpoint, $node_id, $seen_at = null) {
        return $this->add_node($node_id, $endpoint, $seen_at, true);
    }

    public function mark_query($endpoint, $node_id, $seen_at = null) {
        $contact = $this->add_node($node_id, $endpoint, $seen_at, false);

        if($contact !== null)
            $contact->mark_query($seen_at);

        return $contact;
    }

    public function mark_failure($endpoint) {
        if($endpoint instanceof PeerEndpoint)
            $key = $endpoint->key;
        elseif(is_string($endpoint))
            $key = $endpoint;
        else
            return false;

        if(!isset($this->contacts[$key]))
            return false;

        $this->contacts[$key]->mark_failure();

        return true;
    }

    public function find_by_endpoint($endpoint) {
        if($endpoint instanceof PeerEndpoint)
            $key = $endpoint->key;
        elseif(is_string($endpoint))
            $key = $endpoint;
        else
            return null;

        return $this->contacts[$key] ?? null;
    }

    public function get_closest($target_id, $limit = DHT_K, $now = null) {
        validate_dht_node_id($target_id, "DHT routing target");

        if(!is_int($limit) || $limit < 1)
            throw new InvalidArgumentException("DHT closest-node limit must be positive.");

        $now = normalise_peer_time($now);
        $contacts = array_values(array_filter(
            $this->contacts,
            static function($contact) {
                return !$contact->is_bad();
            }
        ));

        usort(
            $contacts,
            static function($left, $right) use ($target_id) {
                return compare_dht_distance($left->node_id, $right->node_id, $target_id);
            }
        );

        return array_slice($contacts, 0, $limit);
    }

    public function get_count() {
        return count($this->contacts);
    }

    public function get_bucket_count() {
        return count(array_filter($this->buckets, static fn($bucket) => $bucket !== []));
    }

    public function get_contacts() {
        return array_values($this->contacts);
    }
}

function dht_token_for_ip($ip_address, $secret) {
    if(filter_var($ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false)
        throw new InvalidArgumentException("DHT token IP must be IPv4.");

    if(!is_string($secret) || strlen($secret) < 16)
        throw new InvalidArgumentException("DHT token secret is too short.");

    return substr(sha1($ip_address . $secret, true), 0, 8);
}

function resolve_dht_bootstrap_nodes($bootstrap_nodes) {
    if(!is_array($bootstrap_nodes))
        throw new InvalidArgumentException("DHT bootstrap nodes must be an array.");

    $resolved = [];
    $seen = [];

    foreach($bootstrap_nodes as $bootstrap_node) {
        try {
            $endpoint = PeerEndpoint::from_string($bootstrap_node);
        } catch(Throwable) {
            continue;
        }

        if($endpoint->is_ipv6)
            continue;

        if(filter_var($endpoint->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false) {
            $candidate_hosts = [$endpoint->host];
        } else {
            $candidate_hosts = @gethostbynamel($endpoint->host);

            if(!is_array($candidate_hosts))
                $candidate_hosts = [];
        }

        foreach($candidate_hosts as $host) {
            try {
                $resolved_endpoint = new PeerEndpoint($host, $endpoint->port);
            } catch(Throwable) {
                continue;
            }

            if(isset($seen[$resolved_endpoint->key]))
                continue;

            $seen[$resolved_endpoint->key] = true;
            $resolved[] = $resolved_endpoint;
        }
    }

    return $resolved;
}

final class RuntimeDhtDiscovery {
    private readonly MagnetUri $magnet;
    private readonly PeerPool $peer_pool;
    private $log_stream;
    private readonly string $node_id;
    private DhtRoutingTable $routing_table;
    private array $bootstrap_nodes;
    private array $resolved_bootstrap_nodes = [];
    private $socket = null;
    private ?int $port = null;
    private bool $started = false;
    private bool $closed = false;
    private int $transaction_counter = 0;
    private array $transactions = [];
    private array $lookup_candidates = [];
    private array $queried_lookup_endpoints = [];
    private array $bootstrap_queried = [];
    private bool $lookup_active = false;
    private ?float $last_lookup_started_at = null;
    private ?float $last_lookup_completed_at = null;
    private string $token_secret_current;
    private string $token_secret_previous;
    private float $token_secret_rotated_at;
    private array $peer_store = [];
    private int $queries_sent = 0;
    private int $queries_received = 0;
    private int $responses_received = 0;
    private int $errors_received = 0;
    private int $query_timeouts = 0;
    private int $lookup_count = 0;
    private int $peers_added = 0;
    private int $nodes_learned = 0;
    private ?int $peer_port = null;
    private int $announce_peer_queries_sent = 0;
    private int $announce_peer_responses = 0;

    public function __construct($magnet, $peer_pool, $log_stream, $bootstrap_nodes = null, $node_id = null, $peer_port = null) {
        if(!($magnet instanceof MagnetUri))
            throw new InvalidArgumentException("Runtime DHT discovery requires a parsed magnet URI.");

        if(!($peer_pool instanceof PeerPool))
            throw new InvalidArgumentException("Runtime DHT discovery requires a peer pool.");

        if(!is_resource($log_stream))
            throw new InvalidArgumentException("Runtime DHT discovery requires a log stream.");

        if($bootstrap_nodes === null)
            $bootstrap_nodes = DHT_BOOTSTRAP_NODES;

        if(!is_array($bootstrap_nodes))
            throw new InvalidArgumentException("Runtime DHT bootstrap nodes must be an array.");

        if($node_id === null)
            $node_id = generate_dht_node_id();

        validate_dht_node_id($node_id);

        if($peer_port !== null && (!is_int($peer_port) || $peer_port < 1 || $peer_port > 65535))
            throw new InvalidArgumentException("Runtime DHT peer port must be between 1 and 65535 or null.");

        $this->magnet = $magnet;
        $this->peer_pool = $peer_pool;
        $this->log_stream = $log_stream;
        $this->node_id = $node_id;
        $this->routing_table = new DhtRoutingTable($node_id);
        $this->bootstrap_nodes = $bootstrap_nodes;
        $this->peer_port = $peer_port;
        $this->token_secret_current = random_bytes(20);
        $this->token_secret_previous = random_bytes(20);
        $this->token_secret_rotated_at = microtime(true);
    }

    private function next_transaction_id() {
        for($attempt = 0; $attempt < 65536; $attempt++) {
            $transaction_id = pack("n", $this->transaction_counter & 0xffff);
            $this->transaction_counter = ($this->transaction_counter + 1) & 0xffff;

            if(!isset($this->transactions[$transaction_id]))
                return $transaction_id;
        }

        throw new RuntimeException("DHT transaction ID space is exhausted.");
    }

    private function send_packet($endpoint, $packet) {
        if(!($endpoint instanceof PeerEndpoint) || $endpoint->is_ipv6)
            return false;

        if(!is_resource($this->socket))
            throw new LogicException("DHT socket is not open.");

        $bytes_written = @stream_socket_sendto($this->socket, $packet, 0, $endpoint->key);

        return is_int($bytes_written) && $bytes_written === strlen($packet);
    }

    private function has_pending_query_to($endpoint, $query = null) {
        foreach($this->transactions as $transaction) {
            if($transaction["endpoint"]->key !== $endpoint->key)
                continue;

            if($query === null || $transaction["query"] === $query)
                return true;
        }

        return false;
    }

    private function send_query($endpoint, $query, $arguments, $purpose, $now = null) {
        if(!($endpoint instanceof PeerEndpoint) || $endpoint->is_ipv6)
            return false;

        $now = normalise_peer_time($now);
        $transaction_id = $this->next_transaction_id();
        $packet = encode_dht_query($transaction_id, $query, $arguments);

        if(!$this->send_packet($endpoint, $packet))
            return false;

        $this->transactions[$transaction_id] = [
            "endpoint" => $endpoint,
            "query" => $query,
            "purpose" => $purpose,
            "sent_at" => $now,
            "deadline" => $now + DHT_QUERY_TIMEOUT,
        ];
        $this->queries_sent++;

        return true;
    }

    private function send_ping($endpoint, $purpose, $now = null) {
        return $this->send_query(
            $endpoint,
            "ping",
            ["id" => $this->node_id],
            $purpose,
            $now
        );
    }

    private function send_find_node($endpoint, $target, $purpose, $now = null) {
        validate_dht_node_id($target, "DHT find_node target");

        return $this->send_query(
            $endpoint,
            "find_node",
            ["id" => $this->node_id, "target" => $target],
            $purpose,
            $now
        );
    }

    private function send_get_peers($endpoint, $purpose, $now = null) {
        return $this->send_query(
            $endpoint,
            "get_peers",
            ["id" => $this->node_id, "info_hash" => $this->magnet->info_hash],
            $purpose,
            $now
        );
    }

    private function send_announce_peer($endpoint, $token, $now = null) {
        if($this->peer_port === null || !is_string($token) || $token === "")
            return false;

        $sent = $this->send_query(
            $endpoint,
            "announce_peer",
            [
                "id" => $this->node_id,
                "info_hash" => $this->magnet->info_hash,
                "port" => $this->peer_port,
                "token" => $token,
                "implied_port" => 0,
            ],
            "announce_peer",
            $now
        );

        if($sent)
            $this->announce_peer_queries_sent++;

        return $sent;
    }

    private function send_response($endpoint, $transaction_id, $response) {
        return $this->send_packet($endpoint, encode_dht_response($transaction_id, $response));
    }

    private function send_error($endpoint, $transaction_id, $code, $message) {
        return $this->send_packet($endpoint, encode_dht_error($transaction_id, $code, $message));
    }

    private function rotate_token_secret_if_due($now) {
        if($now - $this->token_secret_rotated_at < DHT_TOKEN_ROTATE_INTERVAL)
            return;

        $this->token_secret_previous = $this->token_secret_current;
        $this->token_secret_current = random_bytes(20);
        $this->token_secret_rotated_at = $now;
    }

    private function make_token($ip_address) {
        return dht_token_for_ip($ip_address, $this->token_secret_current);
    }

    private function validate_token($ip_address, $token) {
        if(!is_string($token))
            return false;

        return hash_equals(dht_token_for_ip($ip_address, $this->token_secret_current), $token)
            || hash_equals(dht_token_for_ip($ip_address, $this->token_secret_previous), $token);
    }

    private function purge_peer_store($now) {
        foreach(array_keys($this->peer_store) as $info_hash_key) {
            foreach($this->peer_store[$info_hash_key] as $endpoint_key => $record) {
                if($now - $record["announced_at"] > DHT_PEER_STORE_TTL)
                    unset($this->peer_store[$info_hash_key][$endpoint_key]);
            }

            if($this->peer_store[$info_hash_key] === [])
                unset($this->peer_store[$info_hash_key]);
        }
    }

    private function get_stored_peer_values($info_hash, $now) {
        $this->purge_peer_store($now);
        $key = bin2hex($info_hash);

        if(!isset($this->peer_store[$key]))
            return [];

        $values = [];

        foreach($this->peer_store[$key] as $record) {
            $packed_host = @inet_pton($record["endpoint"]->host);

            if($packed_host === false || strlen($packed_host) !== 4)
                continue;

            $values[] = $packed_host . pack("n", $record["endpoint"]->port);
        }

        return $values;
    }

    private function store_announced_peer($info_hash, $endpoint, $now) {
        validate_dht_node_id($info_hash, "DHT announce info hash");

        if(!($endpoint instanceof PeerEndpoint) || $endpoint->is_ipv6)
            return false;

        $key = bin2hex($info_hash);

        if(!isset($this->peer_store[$key]))
            $this->peer_store[$key] = [];

        $this->peer_store[$key][$endpoint->key] = [
            "endpoint" => $endpoint,
            "announced_at" => $now,
        ];

        if(hash_equals($info_hash, $this->magnet->info_hash)) {
            $before = $this->peer_pool->get_count();
            $this->peer_pool->add_peer($endpoint, "dht:announce_peer", $now);

            if($this->peer_pool->get_count() > $before)
                $this->peers_added++;
        }

        return true;
    }

    private function queue_lookup_candidate($endpoint, $node_id = null) {
        if(!($endpoint instanceof PeerEndpoint) || $endpoint->is_ipv6)
            return false;

        if($node_id !== null)
            validate_dht_node_id($node_id);

        if(isset($this->queried_lookup_endpoints[$endpoint->key]))
            return false;

        if(isset($this->lookup_candidates[$endpoint->key])) {
            if($this->lookup_candidates[$endpoint->key]["node_id"] === null && $node_id !== null)
                $this->lookup_candidates[$endpoint->key]["node_id"] = $node_id;

            return false;
        }

        $this->lookup_candidates[$endpoint->key] = [
            "endpoint" => $endpoint,
            "node_id" => $node_id,
        ];

        return true;
    }

    private function add_compact_nodes($compact_nodes, $now) {
        $added = 0;

        foreach(parse_compact_dht_nodes($compact_nodes) as $node) {
            $before = $this->routing_table->get_count();
            $contact = $this->routing_table->add_node(
                $node["node_id"],
                $node["endpoint"],
                $now,
                false
            );

            if($this->routing_table->get_count() > $before) {
                $this->nodes_learned++;
                $added++;
            }

            if($contact !== null)
                $this->queue_lookup_candidate($contact->endpoint, $contact->node_id);
        }

        return $added;
    }

    private function add_peer_values($values, $now) {
        if(!is_array($values) || !array_is_list($values))
            throw new RuntimeException("DHT get_peers values must be a list.");

        $added = 0;

        foreach($values as $compact_peer) {
            if(!is_string($compact_peer) || strlen($compact_peer) !== 6)
                continue;

            try {
                $endpoints = parse_compact_peer_endpoints($compact_peer, 4);
            } catch(Throwable) {
                continue;
            }

            foreach($endpoints as $endpoint) {
                $before = $this->peer_pool->get_count();
                $this->peer_pool->add_peer($endpoint, "dht:get_peers", $now);

                if($this->peer_pool->get_count() > $before) {
                    $this->peers_added++;
                    $added++;
                }
            }
        }

        return $added;
    }

    private function pending_lookup_query_count() {
        $count = 0;

        foreach($this->transactions as $transaction) {
            if(in_array($transaction["purpose"], ["bootstrap", "lookup", "peer_port"], true))
                $count++;
        }

        return $count;
    }

    private function pump_bootstrap($now) {
        while($this->pending_lookup_query_count() < DHT_ALPHA) {
            $candidate = null;

            foreach($this->resolved_bootstrap_nodes as $endpoint) {
                if(isset($this->bootstrap_queried[$endpoint->key]))
                    continue;

                $candidate = $endpoint;

                break;
            }

            if($candidate === null)
                break;

            $this->bootstrap_queried[$candidate->key] = true;
            $this->send_find_node($candidate, $this->node_id, "bootstrap", $now);
        }
    }

    private function pump_lookup($now) {
        if(!$this->lookup_active)
            return;

        while($this->pending_lookup_query_count() < DHT_ALPHA) {
            $candidates = array_values($this->lookup_candidates);

            usort(
                $candidates,
                function($left, $right) {
                    if($left["node_id"] === null && $right["node_id"] === null)
                        return strcmp($left["endpoint"]->key, $right["endpoint"]->key);

                    if($left["node_id"] === null)
                        return 1;

                    if($right["node_id"] === null)
                        return -1;

                    return compare_dht_distance(
                        $left["node_id"],
                        $right["node_id"],
                        $this->magnet->info_hash
                    );
                }
            );

            $candidate = null;

            foreach($candidates as $possible) {
                if(isset($this->queried_lookup_endpoints[$possible["endpoint"]->key]))
                    continue;

                $candidate = $possible;

                break;
            }

            if($candidate === null)
                break;

            $endpoint = $candidate["endpoint"];
            $this->queried_lookup_endpoints[$endpoint->key] = true;
            unset($this->lookup_candidates[$endpoint->key]);
            $this->send_get_peers($endpoint, "lookup", $now);
        }

        if(
            $this->pending_lookup_query_count() === 0
            && $this->lookup_candidates === []
            && count($this->bootstrap_queried) >= count($this->resolved_bootstrap_nodes)
        ) {
            $this->lookup_active = false;
            $this->last_lookup_completed_at = $now;
        }
    }

    private function begin_lookup($now) {
        $this->lookup_count++;
        $this->lookup_active = true;
        $this->last_lookup_started_at = $now;
        $this->lookup_candidates = [];
        $this->queried_lookup_endpoints = [];
        $this->bootstrap_queried = [];

        foreach($this->routing_table->get_closest($this->magnet->info_hash, DHT_K, $now) as $contact)
            $this->queue_lookup_candidate($contact->endpoint, $contact->node_id);

        $this->pump_bootstrap($now);
        $this->pump_lookup($now);
    }

    private function add_response_node($endpoint, $node_id, $now) {
        $before = $this->routing_table->get_count();
        $contact = $this->routing_table->mark_response($endpoint, $node_id, $now);

        if($this->routing_table->get_count() > $before)
            $this->nodes_learned++;

        if($contact !== null)
            $this->queue_lookup_candidate($contact->endpoint, $contact->node_id);

        return $contact;
    }

    private function handle_response($message, $source_endpoint, $now) {
        $transaction_id = $message["t"];

        if(!isset($this->transactions[$transaction_id]))
            return;

        $transaction = $this->transactions[$transaction_id];
        unset($this->transactions[$transaction_id]);

        if($transaction["endpoint"]->key !== $source_endpoint->key)
            return;

        $response = $message["r"];

        if(!isset($response["id"]) || !is_string($response["id"]) || strlen($response["id"]) !== 20)
            return;

        $this->responses_received++;
        $this->add_response_node($source_endpoint, $response["id"], $now);

        if($transaction["purpose"] === "peer_port" && !$this->lookup_active)
            $this->begin_lookup($now);

        if(isset($response["nodes"])) {
            if(!is_string($response["nodes"]))
                return;

            try {
                $this->add_compact_nodes($response["nodes"], $now);
            } catch(Throwable) {
                return;
            }
        }

        if($transaction["query"] === "get_peers" && isset($response["values"])) {
            try {
                $added = $this->add_peer_values($response["values"], $now);

                if($added > 0) {
                    log_message(
                        "DHT get_peers from {$source_endpoint->key}: added {$added} peer" . ($added === 1 ? "" : "s") . "; {$this->peer_pool->get_count()} known.",
                        $this->log_stream
                    );
                }
            } catch(Throwable) {
                // Ignore malformed peer values without poisoning the rest of the lookup.
            }
        }

        if(
            $transaction["query"] === "get_peers"
            && $this->peer_port !== null
            && isset($response["token"])
            && is_string($response["token"])
            && $response["token"] !== ""
        )
            $this->send_announce_peer($source_endpoint, $response["token"], $now);

        if($transaction["query"] === "announce_peer")
            $this->announce_peer_responses++;

        if($transaction["purpose"] === "bootstrap")
            $this->queue_lookup_candidate($source_endpoint, $response["id"]);

        $this->pump_bootstrap($now);
        $this->pump_lookup($now);
    }

    private function handle_error($message, $source_endpoint) {
        $transaction_id = $message["t"];

        if(isset($this->transactions[$transaction_id])) {
            $transaction = $this->transactions[$transaction_id];
            unset($this->transactions[$transaction_id]);

            if($transaction["endpoint"]->key === $source_endpoint->key)
                $this->routing_table->mark_failure($source_endpoint);
        }

        $this->errors_received++;
    }

    private function validate_query_node_id($arguments) {
        if(!isset($arguments["id"]) || !is_string($arguments["id"]) || strlen($arguments["id"]) !== 20)
            throw new RuntimeException("DHT query has an invalid node ID.");

        return $arguments["id"];
    }

    private function handle_query($message, $source_endpoint, $now) {
        $this->queries_received++;
        $transaction_id = $message["t"];
        $query = $message["q"];
        $arguments = $message["a"];

        try {
            $remote_node_id = $this->validate_query_node_id($arguments);
            $this->routing_table->mark_query($source_endpoint, $remote_node_id, $now);

            if($query === "ping") {
                $this->send_response($source_endpoint, $transaction_id, ["id" => $this->node_id]);

                return;
            }

            if($query === "find_node") {
                if(!isset($arguments["target"]) || !is_string($arguments["target"]) || strlen($arguments["target"]) !== 20)
                    throw new RuntimeException("DHT find_node query has an invalid target.");

                $nodes = $this->routing_table->get_closest($arguments["target"], DHT_K, $now);
                $this->send_response(
                    $source_endpoint,
                    $transaction_id,
                    [
                        "id" => $this->node_id,
                        "nodes" => encode_compact_dht_nodes($nodes),
                    ]
                );

                return;
            }

            if($query === "get_peers") {
                if(!isset($arguments["info_hash"]) || !is_string($arguments["info_hash"]) || strlen($arguments["info_hash"]) !== 20)
                    throw new RuntimeException("DHT get_peers query has an invalid info hash.");

                $response = [
                    "id" => $this->node_id,
                    "token" => $this->make_token($source_endpoint->host),
                ];
                $values = $this->get_stored_peer_values($arguments["info_hash"], $now);

                if($values !== [])
                    $response["values"] = $values;
                else
                    $response["nodes"] = encode_compact_dht_nodes(
                        $this->routing_table->get_closest($arguments["info_hash"], DHT_K, $now)
                    );

                $this->send_response($source_endpoint, $transaction_id, $response);

                return;
            }

            if($query === "announce_peer") {
                if(
                    !isset($arguments["info_hash"], $arguments["token"]) 
                    || !is_string($arguments["info_hash"])
                    || strlen($arguments["info_hash"]) !== 20
                    || !$this->validate_token($source_endpoint->host, $arguments["token"])
                )
                    throw new RuntimeException("DHT announce_peer query has an invalid info hash or token.");

                $implied_port = ($arguments["implied_port"] ?? 0) !== 0;

                if($implied_port) {
                    $port = $source_endpoint->port;
                } else {
                    $port = $arguments["port"] ?? null;

                    if(!is_int($port) || $port < 1 || $port > 65535)
                        throw new RuntimeException("DHT announce_peer query has an invalid port.");
                }

                $peer_endpoint = new PeerEndpoint($source_endpoint->host, $port);
                $this->store_announced_peer($arguments["info_hash"], $peer_endpoint, $now);
                $this->send_response($source_endpoint, $transaction_id, ["id" => $this->node_id]);

                return;
            }

            $this->send_error($source_endpoint, $transaction_id, 204, "Method Unknown");
        } catch(Throwable) {
            $this->send_error($source_endpoint, $transaction_id, 203, "Protocol Error");
        }
    }

    private function handle_packet($packet, $source, $now) {
        try {
            $source_endpoint = parse_dht_socket_endpoint($source);
            $message = parse_dht_message($packet);
        } catch(Throwable) {
            return;
        }

        if($message["y"] === "q")
            $this->handle_query($message, $source_endpoint, $now);
        elseif($message["y"] === "r")
            $this->handle_response($message, $source_endpoint, $now);
        else
            $this->handle_error($message, $source_endpoint);
    }

    private function expire_transactions($now) {
        foreach(array_keys($this->transactions) as $transaction_id) {
            if(!isset($this->transactions[$transaction_id]))
                continue;

            $transaction = $this->transactions[$transaction_id];

            if($now < $transaction["deadline"])
                continue;

            unset($this->transactions[$transaction_id]);
            $this->query_timeouts++;
            $this->routing_table->mark_failure($transaction["endpoint"]);
        }

        $this->pump_bootstrap($now);
        $this->pump_lookup($now);
    }

    public function start() {
        if($this->started)
            return false;

        $error_number = 0;
        $error_message = "";
        $socket = @stream_socket_server(
            "udp://0.0.0.0:0",
            $error_number,
            $error_message,
            STREAM_SERVER_BIND
        );

        if($socket === false)
            throw new RuntimeException("DHT UDP socket could not be opened: {$error_message}");

        if(!stream_set_blocking($socket, false)) {
            fclose($socket);

            throw new RuntimeException("DHT UDP socket could not be made non-blocking.");
        }

        $socket_name = stream_socket_get_name($socket, false);

        if(!is_string($socket_name) || preg_match('/:(\\d+)$/', $socket_name, $matches) !== 1) {
            fclose($socket);

            throw new RuntimeException("DHT UDP socket port could not be determined.");
        }

        $this->socket = $socket;
        $this->port = intval($matches[1]);
        $this->resolved_bootstrap_nodes = resolve_dht_bootstrap_nodes($this->bootstrap_nodes);
        $this->started = true;
        $now = microtime(true);
        $this->begin_lookup($now);

        log_message(
            sprintf(
                "DHT discovery started on UDP port %d with node ID %s; %d bootstrap endpoint%s resolved.",
                $this->port,
                bin2hex($this->node_id),
                count($this->resolved_bootstrap_nodes),
                count($this->resolved_bootstrap_nodes) === 1 ? "" : "s"
            ),
            $this->log_stream
        );

        return true;
    }

    public function add_peer_node($host, $port, $now = null) {
        if(!$this->started || $this->closed)
            return false;

        try {
            $endpoint = new PeerEndpoint($host, $port);
        } catch(Throwable) {
            return false;
        }

        if($endpoint->is_ipv6 || $this->routing_table->find_by_endpoint($endpoint) !== null)
            return false;

        if($this->has_pending_query_to($endpoint))
            return false;

        return $this->send_ping($endpoint, "peer_port", $now);
    }

    public function poll($timeout_microseconds = 0) {
        if(!$this->started || $this->closed)
            return false;

        if(!is_int($timeout_microseconds) || $timeout_microseconds < 0)
            throw new InvalidArgumentException("DHT poll timeout must be a non-negative integer.");

        $now = microtime(true);
        $this->rotate_token_secret_if_due($now);

        $discovery_state = runtime_discovery_market_state($this->peer_pool);
        $lookup_refresh_interval = runtime_discovery_dht_refresh_interval($discovery_state["mode"]);

        if(
            !$this->lookup_active
            && (
                $this->last_lookup_completed_at === null
                || $now - $this->last_lookup_completed_at >= $lookup_refresh_interval
            )
        )
            $this->begin_lookup($now);

        $read_sockets = [$this->socket];
        $write_sockets = null;
        $except_sockets = null;
        $timeout_seconds = intdiv($timeout_microseconds, 1000000);
        $timeout_remainder = $timeout_microseconds % 1000000;
        $selected = @stream_select(
            $read_sockets,
            $write_sockets,
            $except_sockets,
            $timeout_seconds,
            $timeout_remainder
        );

        if($selected === false)
            throw new RuntimeException("DHT UDP socket selection failed.");

        if($selected > 0) {
            while(true) {
                $source = null;
                $packet = @stream_socket_recvfrom($this->socket, DHT_MAX_PACKET_LENGTH, 0, $source);

                if(!is_string($packet) || $packet === "")
                    break;

                $this->handle_packet($packet, $source, microtime(true));
            }
        }

        $now = microtime(true);
        $this->expire_transactions($now);
        $this->pump_bootstrap($now);
        $this->pump_lookup($now);

        return true;
    }

    public function has_active_requests() {
        return $this->transactions !== [] || $this->lookup_active;
    }

    public function get_port() {
        return $this->port;
    }

    public function get_node_id() {
        return $this->node_id;
    }

    public function get_stats() {
        return [
            "node_id" => $this->node_id,
            "port" => $this->port,
            "routing_nodes" => $this->routing_table->get_count(),
            "routing_buckets" => $this->routing_table->get_bucket_count(),
            "queries_sent" => $this->queries_sent,
            "queries_received" => $this->queries_received,
            "responses_received" => $this->responses_received,
            "errors_received" => $this->errors_received,
            "query_timeouts" => $this->query_timeouts,
            "lookup_count" => $this->lookup_count,
            "active_queries" => count($this->transactions),
            "peers_added" => $this->peers_added,
            "peer_port" => $this->peer_port,
            "announce_peer_queries_sent" => $this->announce_peer_queries_sent,
            "announce_peer_responses" => $this->announce_peer_responses,
            "discovery_mode" => runtime_discovery_market_state($this->peer_pool)["mode"],
            "known_peers" => $this->peer_pool->get_count(),
        ];
    }

    public function is_closed() {
        return $this->closed;
    }

    public function close() {
        if($this->closed)
            return false;

        if(is_resource($this->socket))
            fclose($this->socket);

        $this->socket = null;
        $this->transactions = [];
        $this->lookup_candidates = [];
        $this->closed = true;

        return true;
    }

    public function __destruct() {
        $this->close();
    }
}

// BEP 11 runtime peer exchange.
function runtime_pex_source_name($connection) {
    if(!($connection instanceof PeerConnection))
        throw new InvalidArgumentException("PEX source requires a peer connection.");

    return "pex:{$connection->peer->endpoint->key}";
}

function add_runtime_pex_peers($peer_pool, $connection, $pex_message, $now = null) {
    if(!($peer_pool instanceof PeerPool))
        throw new InvalidArgumentException("PEX discovery requires a peer pool.");

    if(!($connection instanceof PeerConnection))
        throw new InvalidArgumentException("PEX discovery requires a source peer connection.");

    if(!is_array($pex_message) || !isset($pex_message["added"], $pex_message["dropped"]))
        throw new InvalidArgumentException("PEX discovery requires a parsed ut_pex message.");

    $now = normalise_peer_time($now);
    $host_counts = [];

    foreach($pex_message["added"] as $contact) {
        if(
            !is_array($contact)
            || !isset($contact["endpoint"])
            || !($contact["endpoint"] instanceof PeerEndpoint)
        )
            throw new InvalidArgumentException("PEX discovery contains an invalid added contact.");

        $host = $contact["endpoint"]->host;
        $host_counts[$host] = ($host_counts[$host] ?? 0) + 1;
    }

    $source = runtime_pex_source_name($connection);
    $new_peer_count = 0;
    $accepted_contact_count = 0;

    foreach($pex_message["added"] as $contact) {
        $endpoint = $contact["endpoint"];

        // BEP 11 recommends ignoring duplicate IPs with different ports from one PEX source.
        if(($host_counts[$endpoint->host] ?? 0) !== 1)
            continue;

        if($endpoint->key === $connection->peer->endpoint->key)
            continue;

        $was_known = $peer_pool->has_peer($endpoint);
        $peer_pool->add_peer($endpoint, $source, $now);
        $accepted_contact_count++;

        if(!$was_known)
            $new_peer_count++;
    }

    return [
        "advertised" => count($pex_message["added"]),
        "accepted" => $accepted_contact_count,
        "added" => $new_peer_count,
        "dropped" => count($pex_message["dropped"]),
    ];
}

function build_runtime_pex_snapshot($connections) {
    if(!is_array($connections))
        throw new InvalidArgumentException("PEX snapshot requires a connection list.");

    $snapshot = [];

    foreach($connections as $connection) {
        if(!($connection instanceof PeerConnection))
            throw new InvalidArgumentException("PEX snapshot contains an invalid connection.");

        if(
            $connection->is_terminal()
            || $connection->state !== PeerConnection::STATE_ESTABLISHED
            || $connection->inbound
        )
            continue;

        $endpoint = $connection->peer->endpoint;

        if(pex_endpoint_address_family($endpoint) === null)
            continue;

        $flags = 0x10;

        if($connection->peer->is_seed)
            $flags |= 0x02;

        $snapshot[$endpoint->key] = [
            "endpoint" => $endpoint,
            "flags" => $flags,
        ];
    }

    return $snapshot;
}

function service_runtime_pex($connections, $now = null) {
    if(!is_array($connections))
        throw new InvalidArgumentException("PEX servicing requires a connection list.");

    $now = normalise_peer_time($now);
    $snapshot = build_runtime_pex_snapshot($connections);
    $queued_messages = 0;

    foreach($connections as $connection) {
        if(
            $connection->is_terminal()
            || $connection->state !== PeerConnection::STATE_ESTABLISHED
            || $connection->peer->get_extension_id(UT_PEX_EXTENSION_NAME) === null
        )
            continue;

        if(
            $connection->last_pex_sent_at !== null
            && $now - $connection->last_pex_sent_at < PEX_INTERVAL
        )
            continue;

        $desired = $snapshot;
        unset($desired[$connection->peer->endpoint->key]);
        $added_contacts = [];
        $dropped_endpoints = [];

        foreach($desired as $endpoint_key => $contact) {
            if(isset($connection->pex_advertised_endpoints[$endpoint_key]))
                continue;

            $added_contacts[] = $contact;

            if(count($added_contacts) >= PEX_MAX_ADDED_PER_MESSAGE)
                break;
        }

        foreach($connection->pex_advertised_endpoints as $endpoint_key => $advertised_contact) {
            if(isset($desired[$endpoint_key]))
                continue;

            $dropped_endpoints[] = $advertised_contact["endpoint"];

            if(count($dropped_endpoints) >= PEX_MAX_DROPPED_PER_MESSAGE)
                break;
        }

        $connection->last_pex_sent_at = $now;

        if($added_contacts === [] && $dropped_endpoints === [])
            continue;

        $payload = encode_ut_pex_message($added_contacts, $dropped_endpoints);
        $connection->queue_extension_message(UT_PEX_EXTENSION_NAME, $payload);

        foreach($added_contacts as $contact)
            $connection->pex_advertised_endpoints[$contact["endpoint"]->key] = $contact;

        foreach($dropped_endpoints as $endpoint)
            unset($connection->pex_advertised_endpoints[$endpoint->key]);

        $queued_messages++;
    }

    return $queued_messages;
}

function count_runtime_pex_peers($peer_pool) {
    if(!($peer_pool instanceof PeerPool))
        throw new InvalidArgumentException("PEX peer counting requires a peer pool.");

    $count = 0;

    foreach($peer_pool->get_peers() as $peer) {
        foreach($peer->get_sources() as $source) {
            if(str_starts_with($source, "pex:")) {
                $count++;

                break;
            }
        }
    }

    return $count;
}

function runtime_peer_is_outbound_connectable($peer) {
    if(!($peer instanceof Peer))
        throw new InvalidArgumentException("Outbound-connectable check requires a peer.");

    foreach($peer->get_sources() as $source) {
        if($source !== "inbound:source-port")
            return true;
    }

    return false;
}

function accept_runtime_inbound_peer_connections(
    &$connections,
    $peer_listener,
    $peer_pool,
    $info_hash,
    $local_peer_id,
    $metadata_exchange = null,
    $piece_manager = null,
    $local_dht_port = null,
    $now = null
) {
    if($peer_listener === null)
        return [];

    if(!($peer_listener instanceof RuntimePeerListener))
        throw new InvalidArgumentException("Inbound peer acceptance requires a peer listener.");

    if(!($peer_pool instanceof PeerPool))
        throw new InvalidArgumentException("Inbound peer acceptance requires a peer pool.");

    $now = normalise_peer_time($now);
    $active_connection_count = 0;

    foreach($connections as $connection) {
        if($connection instanceof PeerConnection && !$connection->is_terminal())
            $active_connection_count++;
    }

    $accepted_connections = [];

    foreach($peer_listener->accept_available(INBOUND_ACCEPT_BURST, $now) as $accepted) {
        if($active_connection_count >= DESIRED_CONNECTED_PEERS + INBOUND_HANDSHAKE_OVERFLOW) {
            if(is_resource($accepted["socket"]))
                fclose($accepted["socket"]);

            continue;
        }

        $peer = $peer_pool->add_peer(
            $accepted["endpoint"],
            "inbound:source-port",
            $now
        );
        $connection = new PeerConnection(
            $peer,
            $info_hash,
            $local_peer_id,
            CONNECT_TIMEOUT,
            null,
            null,
            $metadata_exchange,
            $piece_manager,
            $local_dht_port
        );

        try {
            $connection->start_inbound($accepted["socket"], $now);
        } catch(Throwable) {
            if(is_resource($accepted["socket"]))
                fclose($accepted["socket"]);

            continue;
        }

        $connections[] = $connection;
        $accepted_connections[] = $connection;
        $active_connection_count++;
    }

    return $accepted_connections;
}

function fill_runtime_metadata_connections_live(
    &$connections,
    &$request_activity,
    $peer_pool,
    &$attempted_peer_keys,
    $metadata_exchange,
    $magnet,
    $local_peer_id,
    $connector,
    $local_dht_port = null,
    $peer_listener = null
) {
    if(!($peer_pool instanceof PeerPool))
        throw new InvalidArgumentException("Live metadata connection filling requires a peer pool.");

    $active_connection_count = 0;
    $outbound_connection_count = 0;
    $outbound_connection_limit = runtime_outbound_connection_limit($peer_listener, $connections);

    foreach($connections as $connection) {
        if($connection->is_terminal())
            continue;

        $active_connection_count++;

        if(!$connection->inbound)
            $outbound_connection_count++;
    }

    if(
        $active_connection_count >= DESIRED_CONNECTED_PEERS
        || $outbound_connection_count >= $outbound_connection_limit
    )
        return $active_connection_count;

    foreach($peer_pool->get_available_peers() as $peer) {
        if(
            $active_connection_count >= DESIRED_CONNECTED_PEERS
            || $outbound_connection_count >= $outbound_connection_limit
        )
            break;

        if(!runtime_peer_is_outbound_connectable($peer))
            continue;

        if(isset($attempted_peer_keys[$peer->endpoint->key]))
            continue;

        $attempted_peer_keys[$peer->endpoint->key] = true;
        $connection = new PeerConnection(
            $peer,
            $magnet->info_hash,
            $local_peer_id,
            CONNECT_TIMEOUT,
            null,
            null,
            $metadata_exchange,
            null,
            $local_dht_port
        );

        try {
            if(!$connection->start(null, $connector))
                continue;
        } catch(Throwable) {
            continue;
        }

        $connections[] = $connection;
        $request_activity[spl_object_id($connection)] = [
            "pending_count" => 0,
            "remote_message_count" => 0,
            "last_activity_at" => microtime(true),
        ];
        $active_connection_count++;
    }

    return $active_connection_count;
}

function fill_runtime_metadata_connections(
    &$connections,
    &$request_activity,
    $available_peers,
    &$next_peer_index,
    $metadata_exchange,
    $magnet,
    $local_peer_id,
    $connector,
    $local_dht_port = null
) {
    $active_connection_count = 0;

    foreach($connections as $connection) {
        if(!$connection->is_terminal())
            $active_connection_count++;
    }

    while(
        $active_connection_count < DESIRED_CONNECTED_PEERS
        && $next_peer_index < count($available_peers)
    ) {
        $peer = $available_peers[$next_peer_index];
        $next_peer_index++;
        $connection = new PeerConnection(
            $peer,
            $magnet->info_hash,
            $local_peer_id,
            CONNECT_TIMEOUT,
            null,
            null,
            $metadata_exchange,
            null,
            $local_dht_port
        );

        try {
            if(!$connection->start(null, $connector))
                continue;
        } catch(Throwable) {
            continue;
        }

        $connections[] = $connection;
        $request_activity[spl_object_id($connection)] = [
            "pending_count" => 0,
            "remote_message_count" => 0,
            "last_activity_at" => microtime(true),
        ];
        $active_connection_count++;
    }

    return $active_connection_count;
}

function retrieve_runtime_metadata(
    $peer_pool,
    $magnet,
    $local_peer_id,
    $log_stream,
    $connector = null,
    $tracker_discovery = null,
    $dht_discovery = null,
    $peer_listener = null,
    $port_mapping = null
) {
    if(!($peer_pool instanceof PeerPool))
        throw new InvalidArgumentException("Runtime metadata retrieval requires a peer pool.");

    if(!($magnet instanceof MagnetUri))
        throw new InvalidArgumentException("Runtime metadata retrieval requires a parsed magnet URI.");

    if(!is_string($local_peer_id) || strlen($local_peer_id) !== 20)
        throw new InvalidArgumentException("Runtime metadata retrieval requires a 20-byte peer ID.");

    if($connector !== null && !is_callable($connector))
        throw new InvalidArgumentException("Runtime peer connector must be callable.");

    if($tracker_discovery !== null && !($tracker_discovery instanceof RuntimeTrackerDiscovery))
        throw new InvalidArgumentException("Runtime metadata retrieval tracker discovery is invalid.");

    if($dht_discovery !== null && !($dht_discovery instanceof RuntimeDhtDiscovery))
        throw new InvalidArgumentException("Runtime metadata retrieval DHT discovery is invalid.");

    if($peer_listener !== null && !($peer_listener instanceof RuntimePeerListener))
        throw new InvalidArgumentException("Runtime metadata retrieval inbound listener is invalid.");

    if($port_mapping !== null && !($port_mapping instanceof RuntimePortMapping))
        throw new InvalidArgumentException("Runtime metadata retrieval port mapping is invalid.");

    $metadata_exchange = MetadataExchange::from_magnet($magnet);
    $connections = [];
    $request_activity = [];
    $attempted_peer_keys = [];
    $last_metadata_size = null;
    $last_received_piece_count = 0;
    $select_failed = false;
    $initial_known_peers = $peer_pool->get_count();

    log_message(
        sprintf(
            "Metadata retrieval: up to %d concurrent connections from live peer discovery; %d peers currently known.",
            DESIRED_CONNECTED_PEERS,
            $initial_known_peers
        ),
        $log_stream
    );

    while(!$metadata_exchange->is_complete()) {
        if($tracker_discovery !== null)
            $tracker_discovery->poll(0);

        if($dht_discovery !== null)
            $dht_discovery->poll(0);

        if($port_mapping !== null)
            $port_mapping->poll();

        $accepted_inbound = accept_runtime_inbound_peer_connections(
            $connections,
            $peer_listener,
            $peer_pool,
            $magnet->info_hash,
            $local_peer_id,
            $metadata_exchange,
            null,
            $dht_discovery?->get_port()
        );

        foreach($accepted_inbound as $connection) {
            $request_activity[spl_object_id($connection)] = [
                "pending_count" => 0,
                "remote_message_count" => 0,
                "last_activity_at" => microtime(true),
            ];
        }

        $active_connection_count = fill_runtime_metadata_connections_live(
            $connections,
            $request_activity,
            $peer_pool,
            $attempted_peer_keys,
            $metadata_exchange,
            $magnet,
            $local_peer_id,
            $connector,
            $dht_discovery?->get_port(),
            $peer_listener
        );
        $now = microtime(true);

        foreach($connections as $connection) {
            if($connection->is_terminal())
                continue;

            $metadata_rejected = false;

            foreach($connection->take_received_messages() as $message) {
                if(($message["type"] ?? null) === "port" && $dht_discovery !== null) {
                    $dht_discovery->add_peer_node(
                        $connection->peer->endpoint->host,
                        $message["port"],
                        $now
                    );

                    continue;
                }

                if(
                    ($message["type"] ?? null) === "extended"
                    && ($message["extension_name"] ?? null) === UT_PEX_EXTENSION_NAME
                    && isset($message["pex_message"])
                ) {
                    $pex_result = add_runtime_pex_peers(
                        $peer_pool,
                        $connection,
                        $message["pex_message"],
                        $now
                    );

                    if($pex_result["added"] > 0) {
                        log_message(
                            sprintf(
                                "PEX from %s: added %d peer%s from %d advertised contact%s; %d known.",
                                $connection->peer->endpoint->key,
                                $pex_result["added"],
                                $pex_result["added"] === 1 ? "" : "s",
                                $pex_result["advertised"],
                                $pex_result["advertised"] === 1 ? "" : "s",
                                $peer_pool->get_count()
                            ),
                            $log_stream
                        );
                    }

                    continue;
                }

                if(
                    ($message["type"] ?? null) === "extended"
                    && ($message["extension_name"] ?? null) === UT_METADATA_EXTENSION_NAME
                    && ($message["metadata_message"]["type"] ?? null) === "reject"
                ) {
                    $metadata_rejected = true;

                    break;
                }
            }

            if($metadata_rejected) {
                $connection->close();

                continue;
            }

            $connection->check_timeout($now);

            if($connection->is_terminal())
                continue;

            if($connection->state === PeerConnection::STATE_ESTABLISHED) {
                if(
                    $connection->remote_reserved_bytes === null
                    || !peer_supports_extension_protocol($connection->remote_reserved_bytes)
                ) {
                    $connection->close();

                    continue;
                }

                if($connection->remote_extension_handshake_count === 0) {
                    if(
                        $connection->handshake_completed_at !== null
                        && $now - $connection->handshake_completed_at >= RUNTIME_METADATA_REQUEST_TIMEOUT
                    ) {
                        $connection->close();

                        continue;
                    }
                } elseif(
                    $connection->peer->get_extension_id(UT_METADATA_EXTENSION_NAME) === null
                    || $connection->peer->metadata_size === null
                ) {
                    $connection->close();

                    continue;
                }
            }

            if(
                $connection->state === PeerConnection::STATE_ESTABLISHED
                && $connection->peer->get_extension_id(UT_METADATA_EXTENSION_NAME) !== null
                && $connection->peer->metadata_size !== null
            ) {
                $request_limit = RUNTIME_METADATA_REQUEST_PIPELINE
                    - count($connection->metadata_requests);

                if($request_limit > 0) {
                    try {
                        $connection->queue_metadata_requests($request_limit);
                    } catch(Throwable) {
                        $connection->close();

                        continue;
                    }
                }
            }

            $connection_id = spl_object_id($connection);
            $pending_count = count($connection->metadata_requests);
            $activity = $request_activity[$connection_id];

            if(
                $pending_count !== $activity["pending_count"]
                || $connection->remote_message_count !== $activity["remote_message_count"]
            ) {
                $activity["pending_count"] = $pending_count;
                $activity["remote_message_count"] = $connection->remote_message_count;
                $activity["last_activity_at"] = $now;
                $request_activity[$connection_id] = $activity;
            } elseif(
                $pending_count > 0
                && $now - $activity["last_activity_at"] >= RUNTIME_METADATA_REQUEST_TIMEOUT
            ) {
                $connection->close();
            }
        }

        service_runtime_pex($connections, $now);

        if($metadata_exchange->metadata_size !== null && $last_metadata_size === null) {
            $last_metadata_size = $metadata_exchange->metadata_size;
            log_message(
                sprintf(
                    "Metadata announced: %d bytes in %d blocks.",
                    $metadata_exchange->metadata_size,
                    $metadata_exchange->piece_count
                ),
                $log_stream
            );
        }

        $received_piece_count = count($metadata_exchange->pieces);

        if($received_piece_count !== $last_received_piece_count) {
            $last_received_piece_count = $received_piece_count;
            log_message(
                "Metadata progress: {$received_piece_count}/{$metadata_exchange->piece_count} blocks received.",
                $log_stream
            );
        }

        if($metadata_exchange->is_complete())
            break;

        $read_sockets = [];
        $write_sockets = [];
        $socket_connections = [];

        foreach($connections as $connection) {
            if($connection->is_terminal() || !is_resource($connection->socket))
                continue;

            $socket_id = get_resource_id($connection->socket);
            $socket_connections[$socket_id] = $connection;

            if($connection->wants_read())
                $read_sockets[] = $connection->socket;

            if($connection->wants_write())
                $write_sockets[] = $connection->socket;
        }

        if($read_sockets === [] && $write_sockets === []) {
            $active_connection_count = fill_runtime_metadata_connections_live(
                $connections,
                $request_activity,
                $peer_pool,
                $attempted_peer_keys,
                $metadata_exchange,
                $magnet,
                $local_peer_id,
                $connector,
                $dht_discovery?->get_port()
            );

            if($active_connection_count > 0)
                continue;

            $tracker_pending = $tracker_discovery !== null && $tracker_discovery->has_pending_initial();
            $dht_pending = $dht_discovery !== null && $dht_discovery->has_active_requests();

            if($tracker_pending || $dht_pending) {
                if($tracker_pending)
                    $tracker_discovery->poll(intdiv(RUNTIME_SELECT_TIMEOUT_MICROSECONDS, 2));
                else
                    $tracker_discovery?->poll(0);

                if($dht_pending)
                    $dht_discovery->poll(intdiv(RUNTIME_SELECT_TIMEOUT_MICROSECONDS, 2));
                else
                    $dht_discovery?->poll(0);

                continue;
            }

            break;
        }

        $except_sockets = [];
        $selected = @stream_select(
            $read_sockets,
            $write_sockets,
            $except_sockets,
            0,
            RUNTIME_SELECT_TIMEOUT_MICROSECONDS
        );

        if($selected === false) {
            $select_failed = true;

            break;
        }

        $handled_at = microtime(true);

        foreach($write_sockets as $socket) {
            $connection = $socket_connections[get_resource_id($socket)];

            if(!$connection->is_terminal() && $connection->wants_write())
                $connection->handle_writable($handled_at);
        }

        foreach($read_sockets as $socket) {
            $connection = $socket_connections[get_resource_id($socket)];

            if(!$connection->is_terminal() && $connection->wants_read())
                $connection->handle_readable($handled_at);
        }
    }

    foreach($connections as $connection) {
        if(!$connection->is_terminal())
            $connection->close();
    }

    if($metadata_exchange->is_complete()) {
        log_message(
            sprintf("Metadata verified against btih %s.", $magnet->info_hash_hex),
            $log_stream
        );

        return $metadata_exchange;
    }

    if($select_failed)
        throw new RuntimeException("Peer socket selection failed during metadata retrieval.");

    throw new RuntimeException(
        "All " . count($attempted_peer_keys) . " discovered peer candidates were exhausted without complete, verified metadata."
    );
}

function log_runtime_piece_information($piece_manager, $log_stream) {
    if(!($piece_manager instanceof PieceManager))
        throw new InvalidArgumentException("Runtime piece reporting requires a piece manager.");

    $total_block_count = 0;

    for($piece_index = 0; $piece_index < $piece_manager->piece_count; $piece_index++)
        $total_block_count += $piece_manager->get_block_count($piece_index);

    log_message(
        sprintf(
            "Piece manager: %d expected hashes, %d canonical blocks, %d/%d pieces verified.",
            count($piece_manager->expected_hashes),
            $total_block_count,
            $piece_manager->verified_piece_count,
            $piece_manager->piece_count
        ),
        $log_stream
    );

    $piece_log_count = min($piece_manager->piece_count, RUNTIME_PIECE_LOG_LIMIT);

    for($piece_index = 0; $piece_index < $piece_log_count; $piece_index++) {
        $piece_state = $piece_manager->get_piece_state($piece_index);
        log_message(
            sprintf(
                "Piece %d: offset %d, length %d, %d blocks, SHA-1 %s.",
                $piece_index,
                $piece_state["torrent_offset"],
                $piece_state["length"],
                $piece_state["block_count"],
                bin2hex($piece_state["expected_hash"])
            ),
            $log_stream
        );
    }

    if($piece_log_count < $piece_manager->piece_count) {
        $remaining_piece_count = $piece_manager->piece_count - $piece_log_count;
        log_message(
            "Piece catalogue contains {$remaining_piece_count} additional loaded pieces.",
            $log_stream
        );
    }
}

function initialise_runtime_piece_manager($metadata_exchange, $base_path, $log_stream) {
    if(!($metadata_exchange instanceof MetadataExchange) || !$metadata_exchange->is_complete())
        throw new InvalidArgumentException("Runtime storage requires complete, verified metadata.");

    $metadata = $metadata_exchange->get_torrent_metadata();
    $storage = new TorrentStorage($metadata, $base_path);
    $storage->initialise();
    $piece_manager = new PieceManager($metadata, $storage);

    log_message(
        sprintf(
            "Torrent: %s; %d bytes; %d files; %d pieces; nominal piece length %d.",
            $metadata->name,
            $metadata->total_length,
            count($metadata->files),
            $metadata->piece_count,
            $metadata->piece_length
        ),
        $log_stream
    );

    foreach($storage->files as $file) {
        log_message(
            sprintf("File: %s; %d bytes.", $file["relative_path"], $file["length"]),
            $log_stream
        );
    }

    log_message("Storage initialised in {$storage->base_path}.", $log_stream);
    log_runtime_piece_information($piece_manager, $log_stream);

    return [
        "metadata" => $metadata,
        "storage" => $storage,
        "piece_manager" => $piece_manager,
    ];
}

// Per-peer performance measurement.
function update_runtime_ewma($previous, $sample, $alpha) {
    if($previous !== null && (
        (!is_int($previous) && !is_float($previous))
        || !is_finite(floatval($previous))
    ))
        throw new InvalidArgumentException("EWMA previous value must be finite or null.");

    if(
        (!is_int($sample) && !is_float($sample))
        || !is_finite(floatval($sample))
    )
        throw new InvalidArgumentException("EWMA sample must be finite.");

    if(
        (!is_int($alpha) && !is_float($alpha))
        || !is_finite(floatval($alpha))
        || $alpha <= 0
        || $alpha > 1
    )
        throw new InvalidArgumentException("EWMA alpha must be greater than zero and no greater than one.");

    if($previous === null)
        return floatval($sample);

    return (floatval($alpha) * floatval($sample))
        + ((1.0 - floatval($alpha)) * floatval($previous));
}

function create_runtime_peer_metrics_state($now = null) {
    $now = normalise_peer_time($now);

    return [
        "last_sample_at" => $now,
        "previous_downloaded_bytes" => [],
        "previous_useful_downloaded_bytes" => [],
        "previous_uploaded_bytes" => [],
        "previous_successful_requests" => [],
        "previous_failed_requests" => [],
        "previous_latency_sum_seconds" => [],
        "previous_latency_count" => [],
        "latest" => [],
        "sample_count" => 0,
    ];
}

function aggregate_runtime_peer_payload_totals($connections) {
    if(!is_array($connections))
        throw new InvalidArgumentException("Peer payload aggregation requires a connection list.");

    $totals = [];

    foreach($connections as $connection) {
        if(!($connection instanceof PeerConnection))
            throw new InvalidArgumentException("Peer payload aggregation received an invalid connection.");

        $peer_key = $connection->peer->endpoint->key;

        if(!isset($totals[$peer_key])) {
            $totals[$peer_key] = [
                "peer" => $connection->peer,
                "downloaded_bytes" => $connection->peer->archived_downloaded_payload_bytes,
                "useful_downloaded_bytes" => $connection->peer->archived_useful_downloaded_payload_bytes,
                "uploaded_bytes" => $connection->peer->archived_uploaded_payload_bytes,
                "successful_requests" => $connection->peer->archived_successful_block_requests,
                "failed_requests" => $connection->peer->archived_failed_block_requests,
                "latency_sum_seconds" => $connection->peer->archived_request_latency_sum_seconds,
                "latency_count" => $connection->peer->archived_request_latency_count,
                "outstanding_requests" => 0,
                "active_connections" => 0,
                "established_connections" => 0,
                "connection_started_at" => null,
                "remote_unchoked" => false,
                "remote_interested" => false,
                "local_interested" => false,
                "local_unchoked" => false,
            ];
        }

        if(!$connection->metrics_archived) {
            $totals[$peer_key]["downloaded_bytes"] += $connection->received_block_bytes;
            $totals[$peer_key]["useful_downloaded_bytes"] += $connection->received_useful_block_bytes;
            $totals[$peer_key]["uploaded_bytes"] += $connection->uploaded_block_bytes;
            $totals[$peer_key]["successful_requests"] += $connection->successful_block_request_count;
            $totals[$peer_key]["failed_requests"] += $connection->failed_block_request_count;
            $totals[$peer_key]["latency_sum_seconds"] += $connection->request_latency_sum_seconds;
            $totals[$peer_key]["latency_count"] += $connection->request_latency_count;
        }
        $totals[$peer_key]["outstanding_requests"] += $connection->get_outstanding_block_request_count();

        if(!$connection->is_terminal()) {
            $totals[$peer_key]["active_connections"]++;

            if($connection->connect_started_at !== null) {
                $started_at = $totals[$peer_key]["connection_started_at"];
                $totals[$peer_key]["connection_started_at"] = $started_at === null
                    ? $connection->connect_started_at
                    : min($started_at, $connection->connect_started_at);
            }
        }

        if(!$connection->is_terminal() && $connection->state === PeerConnection::STATE_ESTABLISHED) {
            $totals[$peer_key]["established_connections"]++;
            $totals[$peer_key]["remote_unchoked"] = $totals[$peer_key]["remote_unchoked"]
                || !$connection->remote_choking;
            $totals[$peer_key]["remote_interested"] = $totals[$peer_key]["remote_interested"]
                || $connection->remote_interested;
            $totals[$peer_key]["local_interested"] = $totals[$peer_key]["local_interested"]
                || $connection->local_interested;
            $totals[$peer_key]["local_unchoked"] = $totals[$peer_key]["local_unchoked"]
                || !$connection->local_choking;
        }
    }

    return $totals;
}

function sample_runtime_peer_metrics($connections, &$metrics_state, $now = null, $force = false) {
    if(!is_array($metrics_state) || !isset(
        $metrics_state["last_sample_at"],
        $metrics_state["previous_downloaded_bytes"],
        $metrics_state["previous_useful_downloaded_bytes"],
        $metrics_state["previous_uploaded_bytes"],
        $metrics_state["previous_successful_requests"],
        $metrics_state["previous_failed_requests"],
        $metrics_state["previous_latency_sum_seconds"],
        $metrics_state["previous_latency_count"],
        $metrics_state["latest"],
        $metrics_state["sample_count"]
    ))
        throw new InvalidArgumentException("Peer metrics state is invalid.");

    if(!is_bool($force))
        throw new InvalidArgumentException("Peer metrics force flag must be boolean.");

    $now = normalise_peer_time($now);
    $elapsed = $now - $metrics_state["last_sample_at"];

    if(!$force && $elapsed < PEER_EVALUATION_INTERVAL)
        return null;

    if($elapsed <= 0)
        $elapsed = 0.001;

    $totals = aggregate_runtime_peer_payload_totals($connections);
    $latest = [];

    foreach($totals as $peer_key => $total) {
        $downloaded_bytes = $total["downloaded_bytes"];
        $useful_downloaded_bytes = $total["useful_downloaded_bytes"];
        $uploaded_bytes = $total["uploaded_bytes"];
        $successful_requests = $total["successful_requests"];
        $failed_requests = $total["failed_requests"];
        $latency_sum_seconds = $total["latency_sum_seconds"];
        $latency_count = $total["latency_count"];
        $peer = $total["peer"];

        $previous_downloaded = $metrics_state["previous_downloaded_bytes"][$peer_key]
            ?? $peer->archived_downloaded_payload_bytes;
        $previous_useful_downloaded = $metrics_state["previous_useful_downloaded_bytes"][$peer_key]
            ?? $peer->archived_useful_downloaded_payload_bytes;
        $previous_uploaded = $metrics_state["previous_uploaded_bytes"][$peer_key]
            ?? $peer->archived_uploaded_payload_bytes;
        $previous_successful = $metrics_state["previous_successful_requests"][$peer_key]
            ?? $peer->archived_successful_block_requests;
        $previous_failed = $metrics_state["previous_failed_requests"][$peer_key]
            ?? $peer->archived_failed_block_requests;
        $previous_latency_sum = $metrics_state["previous_latency_sum_seconds"][$peer_key]
            ?? $peer->archived_request_latency_sum_seconds;
        $previous_latency_count = $metrics_state["previous_latency_count"][$peer_key]
            ?? $peer->archived_request_latency_count;

        $downloaded_delta = max(0, $downloaded_bytes - $previous_downloaded);
        $useful_downloaded_delta = max(
            0,
            $useful_downloaded_bytes - $previous_useful_downloaded
        );
        $uploaded_delta = max(0, $uploaded_bytes - $previous_uploaded);
        $successful_delta = max(0, $successful_requests - $previous_successful);
        $failed_delta = max(0, $failed_requests - $previous_failed);
        $latency_sum_delta = max(0.0, $latency_sum_seconds - $previous_latency_sum);
        $latency_count_delta = max(0, $latency_count - $previous_latency_count);

        $download_rate = $downloaded_delta / $elapsed;
        $useful_download_rate = $useful_downloaded_delta / $elapsed;
        $upload_rate = $uploaded_delta / $elapsed;
        $request_latency_sample = $latency_count_delta > 0
            ? $latency_sum_delta / $latency_count_delta
            : null;
        $request_outcome_count = $successful_delta + $failed_delta;
        $reliability_sample = $request_outcome_count > 0
            ? $successful_delta / $request_outcome_count
            : null;
        $lifetime_outcome_count = $successful_requests + $failed_requests;
        $lifetime_reliability = $lifetime_outcome_count > 0
            ? $successful_requests / $lifetime_outcome_count
            : null;
        $recent_ratio = $uploaded_delta > 0 ? $downloaded_delta / $uploaded_delta : null;
        $lifetime_ratio = $uploaded_bytes > 0 ? $downloaded_bytes / $uploaded_bytes : null;
        $first_sample = $peer->performance_sample_count === 0;

        $peer->download_rate_short = update_runtime_ewma(
            $first_sample ? null : $peer->download_rate_short,
            $download_rate,
            PEER_METRIC_SHORT_EWMA_ALPHA
        );
        $peer->download_rate_long = update_runtime_ewma(
            $first_sample ? null : $peer->download_rate_long,
            $download_rate,
            PEER_METRIC_LONG_EWMA_ALPHA
        );
        $peer->useful_download_rate_short = update_runtime_ewma(
            $first_sample ? null : $peer->useful_download_rate_short,
            $useful_download_rate,
            PEER_METRIC_SHORT_EWMA_ALPHA
        );
        $peer->useful_download_rate_long = update_runtime_ewma(
            $first_sample ? null : $peer->useful_download_rate_long,
            $useful_download_rate,
            PEER_METRIC_LONG_EWMA_ALPHA
        );
        $peer->upload_rate_short = update_runtime_ewma(
            $first_sample ? null : $peer->upload_rate_short,
            $upload_rate,
            PEER_METRIC_SHORT_EWMA_ALPHA
        );
        $peer->upload_rate_long = update_runtime_ewma(
            $first_sample ? null : $peer->upload_rate_long,
            $upload_rate,
            PEER_METRIC_LONG_EWMA_ALPHA
        );

        if($request_latency_sample !== null) {
            $peer->request_latency_short = update_runtime_ewma(
                $peer->request_latency_short,
                $request_latency_sample,
                PEER_METRIC_SHORT_EWMA_ALPHA
            );
            $peer->request_latency_long = update_runtime_ewma(
                $peer->request_latency_long,
                $request_latency_sample,
                PEER_METRIC_LONG_EWMA_ALPHA
            );
        }

        if($reliability_sample !== null) {
            $peer->reliability_short = update_runtime_ewma(
                $peer->reliability_short,
                $reliability_sample,
                PEER_METRIC_SHORT_EWMA_ALPHA
            );
            $peer->reliability_long = update_runtime_ewma(
                $peer->reliability_long,
                $reliability_sample,
                PEER_METRIC_LONG_EWMA_ALPHA
            );
        }

        $peer->downloaded_payload_bytes = $downloaded_bytes;
        $peer->useful_downloaded_payload_bytes = $useful_downloaded_bytes;
        $peer->uploaded_payload_bytes = $uploaded_bytes;
        $peer->recent_download_rate = $peer->download_rate_short;
        $peer->recent_upload_rate = $peer->upload_rate_short;
        $peer->recent_download_per_upload = $recent_ratio;
        $peer->lifetime_download_per_upload = $lifetime_ratio;
        $peer->successful_block_requests = $successful_requests;
        $peer->failed_block_requests = $failed_requests;
        $peer->lifetime_reliability = $lifetime_reliability;
        $peer->performance_sample_count++;
        $peer->last_performance_sample_at = $now;

        $connection_age = $total["connection_started_at"] === null
            ? 0.0
            : max(0.0, $now - $total["connection_started_at"]);

        $latest[$peer_key] = [
            "peer_key" => $peer_key,
            "peer_id" => $peer->peer_id,
            "connection_age" => $connection_age,
            "downloaded_bytes" => $downloaded_bytes,
            "useful_downloaded_bytes" => $useful_downloaded_bytes,
            "uploaded_bytes" => $uploaded_bytes,
            "downloaded_delta" => $downloaded_delta,
            "useful_downloaded_delta" => $useful_downloaded_delta,
            "uploaded_delta" => $uploaded_delta,
            "download_rate" => $download_rate,
            "useful_download_rate" => $useful_download_rate,
            "upload_rate" => $upload_rate,
            "download_rate_short" => $peer->download_rate_short,
            "download_rate_long" => $peer->download_rate_long,
            "useful_download_rate_short" => $peer->useful_download_rate_short,
            "useful_download_rate_long" => $peer->useful_download_rate_long,
            "upload_rate_short" => $peer->upload_rate_short,
            "upload_rate_long" => $peer->upload_rate_long,
            "request_latency" => $peer->request_latency_short,
            "request_latency_short" => $peer->request_latency_short,
            "request_latency_long" => $peer->request_latency_long,
            "reliability" => $peer->reliability_long ?? $lifetime_reliability,
            "reliability_short" => $peer->reliability_short,
            "reliability_long" => $peer->reliability_long,
            "lifetime_reliability" => $lifetime_reliability,
            "successful_requests" => $successful_requests,
            "failed_requests" => $failed_requests,
            "successful_request_delta" => $successful_delta,
            "failed_request_delta" => $failed_delta,
            "outstanding_requests" => $total["outstanding_requests"],
            "request_pipeline_depth" => $peer->request_pipeline_depth,
            "request_pipeline_max_depth" => $peer->request_pipeline_max_depth,
            "request_pipeline_estimated_rate" => $peer->request_pipeline_estimated_rate,
            "request_pipeline_latency" => $peer->request_pipeline_latency,
            "request_pipeline_target_bytes" => $peer->request_pipeline_target_bytes,
            "request_pipeline_target_seconds" => $peer->request_pipeline_target_seconds,
            "last_piece_received_at" => $peer->last_piece_received_at,
            "last_choked_at" => $peer->last_choked_at,
            "last_unchoked_at" => $peer->last_unchoked_at,
            "recent_download_per_upload" => $recent_ratio,
            "lifetime_download_per_upload" => $lifetime_ratio,
            "active_connections" => $total["active_connections"],
            "established_connections" => $total["established_connections"],
            "remote_unchoked" => $total["remote_unchoked"],
            "remote_interested" => $total["remote_interested"],
            "local_interested" => $total["local_interested"],
            "local_unchoked" => $total["local_unchoked"],
            "is_seed" => $peer->is_seed,
            "policy_state" => $peer->policy_state,
            "peer" => $peer,
        ];
    }

    $metrics_state["previous_downloaded_bytes"] = [];
    $metrics_state["previous_useful_downloaded_bytes"] = [];
    $metrics_state["previous_uploaded_bytes"] = [];
    $metrics_state["previous_successful_requests"] = [];
    $metrics_state["previous_failed_requests"] = [];
    $metrics_state["previous_latency_sum_seconds"] = [];
    $metrics_state["previous_latency_count"] = [];

    foreach($totals as $peer_key => $total) {
        $metrics_state["previous_downloaded_bytes"][$peer_key] = $total["downloaded_bytes"];
        $metrics_state["previous_useful_downloaded_bytes"][$peer_key] =
            $total["useful_downloaded_bytes"];
        $metrics_state["previous_uploaded_bytes"][$peer_key] = $total["uploaded_bytes"];
        $metrics_state["previous_successful_requests"][$peer_key] =
            $total["successful_requests"];
        $metrics_state["previous_failed_requests"][$peer_key] = $total["failed_requests"];
        $metrics_state["previous_latency_sum_seconds"][$peer_key] =
            $total["latency_sum_seconds"];
        $metrics_state["previous_latency_count"][$peer_key] = $total["latency_count"];
    }

    $metrics_state["latest"] = $latest;
    $metrics_state["last_sample_at"] = $now;
    $metrics_state["sample_count"]++;

    return $latest;
}

function format_runtime_optional_metric($value, $multiplier = 1.0, $format = "%.2f", $suffix = "") {
    if($value === null)
        return "n/a";

    return sprintf($format, floatval($value) * floatval($multiplier)) . $suffix;
}

function log_runtime_peer_metrics($metrics, $log_stream) {
    if(!is_array($metrics))
        throw new InvalidArgumentException("Peer metrics log requires a metric list.");

    $active_metrics = array_values(array_filter(
        $metrics,
        static fn($metric) => $metric["established_connections"] > 0
            || $metric["downloaded_delta"] > 0
            || $metric["uploaded_delta"] > 0
            || $metric["successful_request_delta"] > 0
            || $metric["failed_request_delta"] > 0
    ));

    usort(
        $active_metrics,
        static function($left, $right) {
            if(abs($left["useful_download_rate_short"] - $right["useful_download_rate_short"]) > 0.000001)
                return $left["useful_download_rate_short"] > $right["useful_download_rate_short"] ? -1 : 1;

            return strcmp($left["peer_key"], $right["peer_key"]);
        }
    );

    if(PEER_METRIC_LOG_LIMIT >= 0)
        $active_metrics = array_slice($active_metrics, 0, PEER_METRIC_LOG_LIMIT);

    foreach($active_metrics as $metric) {
        $latency_short = format_runtime_optional_metric(
            $metric["request_latency_short"],
            1000.0,
            "%.1f",
            " ms"
        );
        $latency_long = format_runtime_optional_metric(
            $metric["request_latency_long"],
            1000.0,
            "%.1f",
            " ms"
        );
        $reliability_short = format_runtime_optional_metric(
            $metric["reliability_short"],
            100.0,
            "%.1f",
            "%"
        );
        $reliability_long = format_runtime_optional_metric(
            $metric["reliability_long"],
            100.0,
            "%.1f",
            "%"
        );

        log_message(
            sprintf(
                "Peer metric %s: useful down %.2f/%.2f KiB/s short/long, raw %.2f/%.2f KiB/s, up %.2f/%.2f KiB/s; latency %s/%s; reliability %s/%s; useful %d of %d downloaded bytes, %d uploaded; requests %d ok/%d failed/%d outstanding, pipeline %d; age %.1fs; remote %s, %s; local %s, %s; %s; policy %s.",
                $metric["peer_key"],
                $metric["useful_download_rate_short"] / 1024,
                $metric["useful_download_rate_long"] / 1024,
                $metric["download_rate_short"] / 1024,
                $metric["download_rate_long"] / 1024,
                $metric["upload_rate_short"] / 1024,
                $metric["upload_rate_long"] / 1024,
                $latency_short,
                $latency_long,
                $reliability_short,
                $reliability_long,
                $metric["useful_downloaded_bytes"],
                $metric["downloaded_bytes"],
                $metric["uploaded_bytes"],
                $metric["successful_requests"],
                $metric["failed_requests"],
                $metric["outstanding_requests"],
                $metric["request_pipeline_depth"],
                $metric["connection_age"],
                $metric["remote_unchoked"] ? "unchoked" : "choking",
                $metric["remote_interested"] ? "interested" : "not interested",
                $metric["local_unchoked"] ? "unchoked" : "choked",
                $metric["local_interested"] ? "interested" : "not interested",
                $metric["is_seed"] ? "seed" : "leecher/unknown",
                $metric["policy_state"]
            ),
            $log_stream
        );
    }
}

// Per-peer reciprocity upload-price discovery.
function create_runtime_greedy_price_state($now = null) {
    $now = normalise_peer_time($now);

    return [
        "last_update_at" => $now - OPTIMISER_INTERVAL,
        "latest" => [],
        "update_count" => 0,
        "observation_count" => 0,
    ];
}

function clamp_runtime_greedy_upload_price($price) {
    if(
        (!is_int($price) && !is_float($price))
        || !is_finite(floatval($price))
        || $price < 0
    )
        throw new InvalidArgumentException("Greedy upload price must be a finite non-negative number.");

    return min(floatval($price), floatval(UPLOAD_LIMIT_BYTES_PER_SECOND));
}

function runtime_greedy_price_lower_probe($allocation) {
    $allocation = clamp_runtime_greedy_upload_price($allocation);

    if($allocation <= UPLOAD_ALLOCATION_QUANTUM)
        return floatval(UPLOAD_ALLOCATION_QUANTUM);

    return max(
        floatval(UPLOAD_ALLOCATION_QUANTUM),
        clamp_runtime_greedy_upload_price($allocation * (1.0 - PRICE_DECREASE_RATIO))
    );
}

function runtime_greedy_price_recovery_probe($allocation, $last_good_allocation = null) {
    $allocation = max(floatval(UPLOAD_ALLOCATION_QUANTUM), floatval($allocation));
    $last_good = $last_good_allocation === null ? 0.0 : max(0.0, floatval($last_good_allocation));

    return clamp_runtime_greedy_upload_price(max(
        $last_good,
        $allocation * (1.0 + PRICE_INCREASE_RATIO),
        $allocation + UPLOAD_ALLOCATION_QUANTUM
    ));
}

function runtime_greedy_record_price_observation(
    $peer,
    $now,
    $state_before,
    $state_after,
    $allocation_before,
    $allocation_after,
    $metric,
    $outcome,
    &$price_state
) {
    if(!($peer instanceof Peer))
        throw new InvalidArgumentException("Greedy price observation requires a peer.");

    $observation = [
        "timestamp" => normalise_peer_time($now),
        "peer_key" => $peer->endpoint->key,
        "state_before" => $state_before,
        "state_after" => $state_after,
        "allocation_before" => floatval($allocation_before),
        "allocation_after" => floatval($allocation_after),
        "actual_upload_rate" => max(0.0, floatval($metric["upload_rate"] ?? 0.0)),
        "uploaded_delta" => max(0, intval($metric["uploaded_delta"] ?? 0)),
        "useful_download_rate" => max(0.0, floatval($metric["useful_download_rate"] ?? 0.0)),
        "useful_downloaded_delta" => max(0, intval($metric["useful_downloaded_delta"] ?? 0)),
        "remote_unchoked" => (bool)($metric["remote_unchoked"] ?? false),
        "remote_interested" => (bool)($metric["remote_interested"] ?? false),
        "local_unchoked" => (bool)($metric["local_unchoked"] ?? false),
        "estimated_upload_price" => $peer->greedy_upload_price,
        "outcome" => $outcome,
    ];

    $peer->greedy_price_history[] = $observation;
    $price_state["observation_count"]++;

    return $observation;
}

function runtime_greedy_price_response_collapsed($peer, $metric) {
    if(!($peer instanceof Peer))
        throw new InvalidArgumentException("Greedy price collapse detection requires a peer.");

    if(
        intval($metric["established_connections"] ?? 0) > 0
        && !($metric["remote_unchoked"] ?? false)
    )
        return true;

    $useful_delta = max(0, intval($metric["useful_downloaded_delta"] ?? 0));

    if($useful_delta === 0)
        return true;

    $last_good_rate = $peer->greedy_last_good_download_rate;

    if($last_good_rate === null || $last_good_rate <= 0)
        return false;

    $current_rate = max(0.0, floatval($metric["useful_download_rate"] ?? 0.0));

    return $current_rate < $last_good_rate * PRICE_RESPONSE_COLLAPSE_RATIO;
}

function update_runtime_greedy_price_estimates($metrics, &$price_state, $now = null, $force = false) {
    if(!is_array($metrics))
        throw new InvalidArgumentException("Greedy price estimation requires peer metrics.");

    if(!is_array($price_state) || !isset(
        $price_state["last_update_at"],
        $price_state["latest"],
        $price_state["update_count"],
        $price_state["observation_count"]
    ))
        throw new InvalidArgumentException("Greedy price state is invalid.");

    if(!is_bool($force))
        throw new InvalidArgumentException("Greedy price force flag must be boolean.");

    $now = normalise_peer_time($now);

    if(!$force && $now - $price_state["last_update_at"] < OPTIMISER_INTERVAL)
        return null;

    $latest = [];

    foreach($metrics as $peer_key => $metric) {
        if(!isset($metric["peer"]) || !($metric["peer"] instanceof Peer))
            throw new InvalidArgumentException("Greedy price estimation received a metric without its peer.");

        $peer = $metric["peer"];

        if($peer->is_seed) {
            $peer->greedy_upload_price = 0.0;
            $peer->greedy_upload_allocation = 0.0;
            $peer->greedy_price_state = "SEED_FREE";
            $peer->last_greedy_price_sample_at = $now;

            continue;
        }

        $uploaded_delta = max(0, intval($metric["uploaded_delta"] ?? 0));
        $was_selected = (
            (bool)($metric["remote_interested"] ?? false)
            && (bool)($metric["local_unchoked"] ?? false)
        ) || $uploaded_delta > 0 || (
            $peer->greedy_price_state === "STABLE"
            && $peer->greedy_upload_price === 0.0
            && (bool)($metric["remote_interested"] ?? false)
        );

        if(!$was_selected)
            continue;

        $state_before = $peer->greedy_price_state;
        $allocation_before = max(0.0, $peer->greedy_upload_allocation);

        if($state_before === "UNKNOWN") {
            $peer->greedy_price_state = "PROBING";
            $peer->greedy_upload_allocation = max(
                floatval(UPLOAD_ALLOCATION_QUANTUM),
                $allocation_before
            );
            $allocation_before = $peer->greedy_upload_allocation;
            $state_before = "PROBING";
        }

        $useful_delta = max(0, intval($metric["useful_downloaded_delta"] ?? 0));
        $useful_rate = max(0.0, floatval($metric["useful_download_rate"] ?? 0.0));
        $remote_unchoked = (bool)($metric["remote_unchoked"] ?? false);
        $outcome = "NO_EXPOSURE";

        if($uploaded_delta === 0 && $useful_delta > 0 && $remote_unchoked) {
            $peer->greedy_upload_price = 0.0;
            $peer->greedy_upload_allocation = 0.0;
            $peer->greedy_last_good_upload_allocation = 0.0;
            $peer->greedy_last_good_download_rate = $useful_rate;
            $peer->greedy_price_state = "STABLE";
            $peer->greedy_last_price_change_at = $now;
            $outcome = "FREE_RESPONSE";
        } elseif($uploaded_delta === 0) {
            if(
                $peer->greedy_price_state === "STABLE"
                && $peer->greedy_upload_price === 0.0
                && !$remote_unchoked
            ) {
                $peer->greedy_upload_allocation = floatval(UPLOAD_ALLOCATION_QUANTUM);
                $peer->greedy_upload_price = floatval(UPLOAD_ALLOCATION_QUANTUM);
                $peer->greedy_price_state = "PROBING";
                $peer->greedy_last_price_change_at = $now;
                $outcome = "FREE_RESPONSE_ENDED";
            }
        } else {
            $collapsed = runtime_greedy_price_response_collapsed($peer, $metric);
            $state = $peer->greedy_price_state;

            if($state === "PROBING") {
                if(!$collapsed) {
                    $peer->greedy_upload_price = $allocation_before;
                    $peer->greedy_last_good_upload_allocation = $allocation_before;
                    $peer->greedy_last_good_download_rate = $useful_rate;
                    $peer->greedy_price_state = "RECIPROCATING";
                    $peer->greedy_last_price_change_at = $now;
                    $outcome = "RECIPROCITY_FOUND";
                } else {
                    $peer->greedy_upload_allocation = runtime_greedy_price_recovery_probe(
                        $allocation_before,
                        $peer->greedy_last_good_upload_allocation
                    );
                    $peer->greedy_price_state = "PROBING";
                    $peer->greedy_last_price_change_at = $now;
                    $outcome = "PROBE_INCREASE";
                }
            } elseif($state === "RECIPROCATING") {
                if(!$collapsed) {
                    $peer->greedy_upload_price = $allocation_before;
                    $peer->greedy_last_good_upload_allocation = $allocation_before;
                    $peer->greedy_last_good_download_rate = $useful_rate;
                    $lower = runtime_greedy_price_lower_probe($allocation_before);
                    $peer->greedy_upload_allocation = $lower;
                    $peer->greedy_price_state = $lower < $allocation_before
                        ? "PRICE_SEARCH"
                        : "STABLE";
                    $peer->greedy_last_price_change_at = $now;
                    $outcome = $lower < $allocation_before
                        ? "LOWER_PROBE"
                        : "FLOOR_STABLE";
                } else {
                    $peer->greedy_upload_allocation = runtime_greedy_price_recovery_probe(
                        $allocation_before,
                        $peer->greedy_last_good_upload_allocation
                    );
                    $peer->greedy_upload_price = $peer->greedy_upload_allocation;
                    $peer->greedy_price_state = "STABLE";
                    $peer->greedy_last_price_change_at = $now;
                    $outcome = "RECOVERY_INCREASE";
                }
            } elseif($state === "PRICE_SEARCH" || $state === "RETEST") {
                if(!$collapsed) {
                    $peer->greedy_upload_price = $allocation_before;
                    $peer->greedy_last_good_upload_allocation = $allocation_before;
                    $peer->greedy_last_good_download_rate = $useful_rate;
                    $lower = runtime_greedy_price_lower_probe($allocation_before);
                    $peer->greedy_upload_allocation = $lower;
                    $peer->greedy_price_state = $lower < $allocation_before
                        ? "PRICE_SEARCH"
                        : "STABLE";
                    $peer->greedy_last_price_change_at = $now;
                    $outcome = $lower < $allocation_before
                        ? "LOWER_PROBE"
                        : "FLOOR_STABLE";
                } else {
                    $peer->greedy_upload_allocation = runtime_greedy_price_recovery_probe(
                        $allocation_before,
                        $peer->greedy_last_good_upload_allocation
                    );
                    $peer->greedy_upload_price = $peer->greedy_upload_allocation;
                    $peer->greedy_price_state = "STABLE";
                    $peer->greedy_last_price_change_at = $now;
                    $outcome = "RECOVERY_INCREASE";
                }
            } elseif($state === "STABLE") {
                if($collapsed) {
                    $peer->greedy_upload_allocation = runtime_greedy_price_recovery_probe(
                        $allocation_before,
                        $peer->greedy_last_good_upload_allocation
                    );
                    $peer->greedy_upload_price = $peer->greedy_upload_allocation;
                    $peer->greedy_price_state = "STABLE";
                    $peer->greedy_last_price_change_at = $now;
                    $outcome = "RECOVERY_INCREASE";
                } elseif(
                    $peer->greedy_upload_price !== null
                    && $peer->greedy_upload_price > UPLOAD_ALLOCATION_QUANTUM
                    && $peer->greedy_last_price_change_at !== null
                    && $now - $peer->greedy_last_price_change_at >= PRICE_RETEST_INTERVAL
                ) {
                    $peer->greedy_last_good_upload_allocation = $allocation_before;
                    $peer->greedy_last_good_download_rate = $useful_rate;
                    $peer->greedy_upload_allocation = runtime_greedy_price_lower_probe($allocation_before);
                    $peer->greedy_price_state = "RETEST";
                    $peer->greedy_last_price_change_at = $now;
                    $outcome = "RETEST_LOWER";
                } else {
                    $peer->greedy_last_good_download_rate = $useful_rate;
                    $outcome = "STABLE_RESPONSE";
                }
            }
        }

        $peer->greedy_price_sample_count++;
        $peer->last_greedy_price_sample_at = $now;
        $observation = runtime_greedy_record_price_observation(
            $peer,
            $now,
            $state_before,
            $peer->greedy_price_state,
            $allocation_before,
            $peer->greedy_upload_allocation,
            $metric,
            $outcome,
            $price_state
        );
        $latest[$peer_key] = $observation;
    }

    $price_state["latest"] = $latest;
    $price_state["last_update_at"] = $now;
    $price_state["update_count"]++;

    return $latest;
}

function prepare_runtime_greedy_price_allocations($selected_upload_connections, $now = null) {
    if(!is_array($selected_upload_connections))
        throw new InvalidArgumentException("Greedy price allocation preparation requires selected peers.");

    $now = normalise_peer_time($now);
    $selected_count = count($selected_upload_connections);

    if($selected_count === 0)
        return [];

    $per_peer_cap = UPLOAD_LIMIT_BYTES_PER_SECOND / $selected_count;
    $allocations = [];

    foreach($selected_upload_connections as $peer_key => $connection) {
        if(!($connection instanceof PeerConnection))
            throw new InvalidArgumentException("Greedy price allocation requires peer connections.");

        $peer = $connection->peer;

        if($peer->is_seed)
            continue;

        if($peer->greedy_price_state === "UNKNOWN") {
            $peer->greedy_price_state = "PROBING";
            $peer->greedy_upload_allocation = min(
                $per_peer_cap,
                floatval(UPLOAD_ALLOCATION_QUANTUM)
            );
            $peer->greedy_last_price_change_at = $now;
        } elseif($peer->greedy_upload_allocation <= 0.0 && ($peer->greedy_upload_price ?? 0.0) > 0.0) {
            $peer->greedy_upload_allocation = min(
                $per_peer_cap,
                max(floatval(UPLOAD_ALLOCATION_QUANTUM), floatval($peer->greedy_upload_price))
            );
        }

        $peer->greedy_upload_allocation = min(
            $per_peer_cap,
            max(0.0, $peer->greedy_upload_allocation)
        );

        if($peer->greedy_upload_allocation <= 0.0 && !$connection->local_choking)
            $connection->set_local_choking(true);

        $allocations[$peer_key] = $peer->greedy_upload_allocation;
    }

    return $allocations;
}

function log_runtime_greedy_price_estimates($observations, $log_stream) {
    if(!is_array($observations))
        throw new InvalidArgumentException("Greedy price log requires an observation list.");

    if($observations === [])
        return;

    foreach($observations as $observation) {
        $price = $observation["estimated_upload_price"] === null
            ? "n/a"
            : sprintf("%.2f KiB/s", $observation["estimated_upload_price"] / 1024);

        log_message(
            sprintf(
                "Upload price %s: %s -> %s, allocation %.2f -> %.2f KiB/s; actual up %.2f KiB/s, useful down %.2f KiB/s; %s; estimated price %s.",
                $observation["peer_key"],
                $observation["state_before"],
                $observation["state_after"],
                $observation["allocation_before"] / 1024,
                $observation["allocation_after"] / 1024,
                $observation["actual_upload_rate"] / 1024,
                $observation["useful_download_rate"] / 1024,
                $observation["outcome"],
                $price
            ),
            $log_stream
        );
    }
}

// Historical upload-vs-download marginal-return estimation.
function create_runtime_greedy_marginal_return_state($now = null) {
    $now = normalise_peer_time($now);

    return [
        "last_update_at" => $now,
        "latest" => [],
        "update_count" => 0,
        "sample_count" => 0,
    ];
}

function runtime_greedy_marginal_return_point($peer, $observation) {
    if(!($peer instanceof Peer))
        throw new InvalidArgumentException("Greedy marginal-return point requires a peer.");

    if(!is_array($observation))
        throw new InvalidArgumentException("Greedy marginal-return point requires an observation.");

    return [
        "timestamp" => normalise_peer_time($observation["timestamp"] ?? microtime(true)),
        "peer_key" => $peer->endpoint->key,
        "allocation" => max(0.0, floatval($observation["allocation_before"] ?? 0.0)),
        "actual_upload_rate" => max(0.0, floatval($observation["actual_upload_rate"] ?? 0.0)),
        "useful_download_rate" => max(0.0, floatval($observation["useful_download_rate"] ?? 0.0)),
        "uploaded_delta" => max(0, intval($observation["uploaded_delta"] ?? 0)),
        "useful_downloaded_delta" => max(0, intval($observation["useful_downloaded_delta"] ?? 0)),
        "price_state" => strval($observation["state_after"] ?? $peer->greedy_price_state),
        "price_outcome" => strval($observation["outcome"] ?? "UNKNOWN"),
    ];
}

function runtime_greedy_record_marginal_return($peer, $observation, &$return_state) {
    if(!($peer instanceof Peer))
        throw new InvalidArgumentException("Greedy marginal-return recording requires a peer.");

    if(!is_array($return_state) || !isset(
        $return_state["latest"],
        $return_state["update_count"],
        $return_state["sample_count"]
    ))
        throw new InvalidArgumentException("Greedy marginal-return state is invalid.");

    $point = runtime_greedy_marginal_return_point($peer, $observation);
    $previous = $peer->greedy_return_history === []
        ? null
        : $peer->greedy_return_history[count($peer->greedy_return_history) - 1];
    $peer->greedy_return_history[] = $point;

    if($previous === null)
        return null;

    $delta_upload = $point["actual_upload_rate"] - $previous["actual_upload_rate"];
    $delta_download = $point["useful_download_rate"] - $previous["useful_download_rate"];

    if(abs($delta_upload) < MARGINAL_RETURN_MIN_UPLOAD_CHANGE)
        return null;

    $sample = $delta_download / $delta_upload;

    if(!is_finite($sample))
        return null;

    $peer->greedy_marginal_return = update_runtime_ewma(
        $peer->greedy_marginal_return,
        $sample,
        MARGINAL_RETURN_EWMA_ALPHA
    );
    $peer->greedy_marginal_return_sample_count++;
    $peer->last_greedy_marginal_return_sample_at = $point["timestamp"];

    $record = [
        "timestamp" => $point["timestamp"],
        "peer_key" => $peer->endpoint->key,
        "previous_upload_rate" => $previous["actual_upload_rate"],
        "current_upload_rate" => $point["actual_upload_rate"],
        "delta_upload_rate" => $delta_upload,
        "previous_useful_download_rate" => $previous["useful_download_rate"],
        "current_useful_download_rate" => $point["useful_download_rate"],
        "delta_useful_download_rate" => $delta_download,
        "sample_marginal_return" => $sample,
        "expected_marginal_return" => $peer->greedy_marginal_return,
        "sample_count" => $peer->greedy_marginal_return_sample_count,
    ];
    $peer->greedy_marginal_return_history[] = $record;
    $return_state["sample_count"]++;

    return $record;
}

function update_runtime_greedy_marginal_returns($price_observations, $metrics, &$return_state, $now = null) {
    if(!is_array($price_observations) || !is_array($metrics))
        throw new InvalidArgumentException("Greedy marginal-return estimation requires price observations and peer metrics.");

    if(!is_array($return_state) || !isset(
        $return_state["last_update_at"],
        $return_state["latest"],
        $return_state["update_count"],
        $return_state["sample_count"]
    ))
        throw new InvalidArgumentException("Greedy marginal-return state is invalid.");

    $now = normalise_peer_time($now);
    $latest = [];

    foreach($price_observations as $peer_key => $observation) {
        if(!is_array($observation))
            throw new InvalidArgumentException("Greedy marginal-return estimation received an invalid observation.");

        $peer = $metrics[$peer_key]["peer"] ?? null;

        if(!($peer instanceof Peer))
            throw new InvalidArgumentException("Greedy marginal-return estimation received an observation without peer metrics.");

        $record = runtime_greedy_record_marginal_return($peer, $observation, $return_state);

        if($record !== null)
            $latest[$peer_key] = $record;
    }

    $return_state["latest"] = $latest;
    $return_state["last_update_at"] = $now;
    $return_state["update_count"]++;

    return $latest;
}

function log_runtime_greedy_marginal_returns($records, $log_stream) {
    if(!is_array($records))
        throw new InvalidArgumentException("Greedy marginal-return log requires a record list.");

    foreach($records as $record) {
        log_message(
            sprintf(
                "Marginal return %s: delta useful down %.2f KiB/s for delta up %.2f KiB/s; sample %.3f, expected %.3f after %d sample%s.",
                $record["peer_key"],
                $record["delta_useful_download_rate"] / 1024,
                $record["delta_upload_rate"] / 1024,
                $record["sample_marginal_return"],
                $record["expected_marginal_return"],
                $record["sample_count"],
                $record["sample_count"] === 1 ? "" : "s"
            ),
            $log_stream
        );
    }
}

// Greedy upload allocator.
function create_runtime_greedy_upload_allocator_state($now = null) {
    $now = normalise_peer_time($now);

    return [
        "last_update_at" => $now,
        "latest" => [],
        "update_count" => 0,
    ];
}

function runtime_greedy_compare_upload_allocator_connections($left, $right) {
    if(!($left instanceof PeerConnection) || !($right instanceof PeerConnection))
        throw new InvalidArgumentException("Greedy upload allocator ranking requires peer connections.");

    $left_return = $left->peer->greedy_marginal_return;
    $right_return = $right->peer->greedy_marginal_return;
    $left_known = $left_return !== null && is_finite($left_return);
    $right_known = $right_return !== null && is_finite($right_return);

    if($left_known !== $right_known)
        return $left_known ? -1 : 1;

    if($left_known && $right_known && $left_return !== $right_return)
        return $left_return > $right_return ? -1 : 1;

    return runtime_greedy_compare_connections($left, $right);
}

function select_runtime_greedy_upload_connections($candidates, $slot_limit, $exploration_cursor = 0) {
    if(!is_array($candidates))
        throw new InvalidArgumentException("Greedy upload candidate set must be an array.");

    if(!is_int($slot_limit) || $slot_limit < 0)
        throw new InvalidArgumentException("Greedy upload slot limit must be a non-negative integer.");

    if(!is_int($exploration_cursor) || $exploration_cursor < 0)
        throw new InvalidArgumentException("Greedy upload exploration cursor must be a non-negative integer.");

    $ranked = array_values($candidates);

    foreach($ranked as $connection) {
        if(!($connection instanceof PeerConnection))
            throw new InvalidArgumentException("Greedy upload candidates must contain peer connections.");
    }

    usort($ranked, "runtime_greedy_compare_upload_allocator_connections");
    $candidate_count = count($ranked);

    if($candidate_count === 0 || $slot_limit === 0) {
        return [
            "selected" => [],
            "exploit" => [],
            "explore" => [],
            "candidate_count" => $candidate_count,
            "exploit_slot_count" => 0,
            "explore_slot_count" => 0,
        ];
    }

    $positive = array_values(array_filter(
        $ranked,
        static function($connection) {
            $return = $connection->peer->greedy_marginal_return;

            return $return !== null && is_finite($return) && $return > 0.0;
        }
    ));
    $explore_target = runtime_greedy_exploration_slot_count($slot_limit, $candidate_count);
    $exploit_target = min(
        count($positive),
        max(0, min($slot_limit, $candidate_count) - $explore_target)
    );
    $exploit_connections = array_slice($positive, 0, $exploit_target);
    $exploit_keys = [];

    foreach($exploit_connections as $connection)
        $exploit_keys[$connection->peer->endpoint->key] = true;

    $remaining = array_values(array_filter(
        $ranked,
        static fn($connection) => !isset($exploit_keys[$connection->peer->endpoint->key])
    ));

    if($exploit_target === 0 && $remaining !== [] && $explore_target === 0)
        $explore_target = 1;

    $explore_target = min(
        $explore_target,
        max(0, $slot_limit - $exploit_target),
        count($remaining)
    );
    $explore_connections = [];

    if($explore_target > 0) {
        $remaining_count = count($remaining);
        $start = $exploration_cursor % $remaining_count;

        for($offset = 0; $offset < $explore_target; $offset++)
            $explore_connections[] = $remaining[($start + $offset) % $remaining_count];
    }

    $exploit = [];
    $explore = [];
    $selected = [];

    foreach($exploit_connections as $connection) {
        $peer_key = $connection->peer->endpoint->key;
        $exploit[$peer_key] = $connection;
        $selected[$peer_key] = $connection;
    }

    foreach($explore_connections as $connection) {
        $peer_key = $connection->peer->endpoint->key;
        $explore[$peer_key] = $connection;
        $selected[$peer_key] = $connection;
    }

    return [
        "selected" => $selected,
        "exploit" => $exploit,
        "explore" => $explore,
        "candidate_count" => $candidate_count,
        "exploit_slot_count" => count($exploit),
        "explore_slot_count" => count($explore),
    ];
}

function runtime_greedy_upload_rate_quanta($rate, $round_up = false) {
    if((!is_int($rate) && !is_float($rate)) || !is_finite(floatval($rate)) || $rate < 0)
        throw new InvalidArgumentException("Greedy upload allocator rate must be finite and non-negative.");

    if(!is_bool($round_up))
        throw new InvalidArgumentException("Greedy upload allocator rounding flag must be boolean.");

    if($rate <= 0)
        return 0;

    $quanta = floatval($rate) / floatval(UPLOAD_ALLOCATION_QUANTUM);

    return $round_up
        ? (int)ceil($quanta - 1e-12)
        : (int)floor($quanta + 1e-12);
}

function runtime_greedy_projected_marginal_return($peer, $projected_upload_rate) {
    if(!($peer instanceof Peer))
        throw new InvalidArgumentException("Greedy projected marginal return requires a peer.");

    if(
        (!is_int($projected_upload_rate) && !is_float($projected_upload_rate))
        || !is_finite(floatval($projected_upload_rate))
        || $projected_upload_rate < 0
    )
        throw new InvalidArgumentException("Projected upload rate must be finite and non-negative.");

    $points = $peer->greedy_return_history;

    if(count($points) >= 2) {
        usort(
            $points,
            static function($left, $right) {
                $left_rate = floatval($left["actual_upload_rate"] ?? 0.0);
                $right_rate = floatval($right["actual_upload_rate"] ?? 0.0);

                if($left_rate === $right_rate)
                    return floatval($left["timestamp"] ?? 0.0) <=> floatval($right["timestamp"] ?? 0.0);

                return $left_rate <=> $right_rate;
            }
        );
        $segments = [];

        for($index = 1; $index < count($points); $index++) {
            $previous = $points[$index - 1];
            $current = $points[$index];
            $previous_upload = max(0.0, floatval($previous["actual_upload_rate"] ?? 0.0));
            $current_upload = max(0.0, floatval($current["actual_upload_rate"] ?? 0.0));
            $delta_upload = $current_upload - $previous_upload;

            if(abs($delta_upload) < MARGINAL_RETURN_MIN_UPLOAD_CHANGE)
                continue;

            $previous_download = max(0.0, floatval($previous["useful_download_rate"] ?? 0.0));
            $current_download = max(0.0, floatval($current["useful_download_rate"] ?? 0.0));
            $slope = ($current_download - $previous_download) / $delta_upload;

            if(!is_finite($slope))
                continue;

            $segments[] = [
                "upper_upload_rate" => max($previous_upload, $current_upload),
                "marginal_return" => $slope,
            ];
        }

        if($segments !== []) {
            foreach($segments as $segment) {
                if($projected_upload_rate <= $segment["upper_upload_rate"])
                    return $segment["marginal_return"];
            }

            return $segments[count($segments) - 1]["marginal_return"];
        }
    }

    $fallback = $peer->greedy_marginal_return;

    if($fallback === null || !is_finite($fallback))
        return null;

    return floatval($fallback);
}

function allocate_runtime_greedy_upload_bandwidth($upload_selection, &$allocator_state, $now = null) {
    if(!is_array($upload_selection) || !isset(
        $upload_selection["selected"],
        $upload_selection["exploit"],
        $upload_selection["explore"]
    ))
        throw new InvalidArgumentException("Greedy upload allocator requires a valid upload selection.");

    if(!is_array($allocator_state) || !isset(
        $allocator_state["last_update_at"],
        $allocator_state["latest"],
        $allocator_state["update_count"]
    ))
        throw new InvalidArgumentException("Greedy upload allocator state is invalid.");

    $now = normalise_peer_time($now);
    $selected = $upload_selection["selected"];
    $exploit = $upload_selection["exploit"];
    $explore = $upload_selection["explore"];
    $price_targets = prepare_runtime_greedy_price_allocations($selected, $now);
    $quantum = floatval(UPLOAD_ALLOCATION_QUANTUM);
    $total_quanta = runtime_greedy_upload_rate_quanta(UPLOAD_LIMIT_BYTES_PER_SECOND);
    $explore_budget_quanta = $explore === []
        ? 0
        : min(
            $total_quanta,
            max(
                count($explore),
                (int)floor($total_quanta * EXPLORE_RATIO)
            )
        );
    $exploit_budget_quanta = max(0, $total_quanta - $explore_budget_quanta);
    $allocation_quanta = [];

    foreach($selected as $peer_key => $connection)
        $allocation_quanta[$peer_key] = 0;

    $remaining_explore = $explore_budget_quanta;

    foreach($explore as $peer_key => $connection) {
        if($remaining_explore <= 0)
            break;

        $target = max(
            $quantum,
            floatval($price_targets[$peer_key] ?? 0.0)
        );
        $wanted_quanta = max(1, runtime_greedy_upload_rate_quanta($target, true));
        $granted = min($wanted_quanta, $remaining_explore);
        $allocation_quanta[$peer_key] += $granted;
        $remaining_explore -= $granted;
    }

    $ranked_exploit = array_values($exploit);
    usort($ranked_exploit, "runtime_greedy_compare_upload_allocator_connections");
    $remaining_exploit = $exploit_budget_quanta;

    foreach($ranked_exploit as $connection) {
        if($remaining_exploit <= 0)
            break;

        $peer_key = $connection->peer->endpoint->key;
        $target = max(
            $quantum,
            floatval($price_targets[$peer_key] ?? 0.0)
        );
        $floor_quanta = max(1, runtime_greedy_upload_rate_quanta($target, true));
        $granted = min($floor_quanta, $remaining_exploit);
        $allocation_quanta[$peer_key] += $granted;
        $remaining_exploit -= $granted;
    }

    while($remaining_exploit > 0 && $ranked_exploit !== []) {
        $best_connection = null;
        $best_return = null;

        foreach($ranked_exploit as $connection) {
            $peer_key = $connection->peer->endpoint->key;
            $projected_rate = (($allocation_quanta[$peer_key] ?? 0) + 1) * $quantum;
            $projected_return = runtime_greedy_projected_marginal_return(
                $connection->peer,
                $projected_rate
            );

            if($projected_return === null || !is_finite($projected_return) || $projected_return <= 0.0)
                continue;

            if(
                $best_connection === null
                || $projected_return > $best_return
                || (
                    $projected_return === $best_return
                    && runtime_greedy_compare_upload_allocator_connections(
                        $connection,
                        $best_connection
                    ) < 0
                )
            ) {
                $best_connection = $connection;
                $best_return = $projected_return;
            }
        }

        if($best_connection === null)
            break;

        $best_key = $best_connection->peer->endpoint->key;
        $allocation_quanta[$best_key]++;
        $remaining_exploit--;
    }

    $allocations = [];
    $allocated_exploit_rate = 0.0;
    $allocated_explore_rate = 0.0;
    $active_peer_count = 0;

    foreach($selected as $peer_key => $connection) {
        $rate = min(
            floatval(UPLOAD_LIMIT_BYTES_PER_SECOND),
            ($allocation_quanta[$peer_key] ?? 0) * $quantum
        );
        $allocations[$peer_key] = $rate;
        $connection->peer->greedy_allocator_rate = $rate;
        $connection->peer->greedy_allocator_role = isset($exploit[$peer_key])
            ? "EXPLOIT"
            : "EXPLORE";

        if($rate > 0.0) {
            $active_peer_count++;

            if($connection->local_choking)
                $connection->set_local_choking(false);
        } elseif(!$connection->local_choking) {
            $connection->set_local_choking(true);
        }

        if(isset($exploit[$peer_key]))
            $allocated_exploit_rate += $rate;
        else
            $allocated_explore_rate += $rate;
    }

    $latest = [
        "timestamp" => $now,
        "allocations" => $allocations,
        "active_peer_count" => $active_peer_count,
        "exploit_peer_count" => count($exploit),
        "explore_peer_count" => count($explore),
        "exploit_budget_rate" => $exploit_budget_quanta * $quantum,
        "explore_budget_rate" => $explore_budget_quanta * $quantum,
        "allocated_exploit_rate" => $allocated_exploit_rate,
        "allocated_explore_rate" => $allocated_explore_rate,
        "allocated_total_rate" => $allocated_exploit_rate + $allocated_explore_rate,
        "unused_rate" => max(
            0.0,
            floatval(UPLOAD_LIMIT_BYTES_PER_SECOND) - $allocated_exploit_rate - $allocated_explore_rate
        ),
    ];

    if(($allocator_state["latest"]["allocations"] ?? null) !== $allocations)
        $allocator_state["update_count"]++;

    $allocator_state["latest"] = $latest;
    $allocator_state["last_update_at"] = $now;

    return $latest;
}

function log_runtime_greedy_upload_allocator($allocation, $upload_selection, $log_stream) {
    if(!is_array($allocation) || !is_array($upload_selection))
        throw new InvalidArgumentException("Greedy upload allocator log requires allocation and selection data.");

    $top = [];

    foreach(array_slice(array_values($upload_selection["exploit"] ?? []), 0, 3) as $connection) {
        $peer = $connection->peer;
        $top[] = sprintf(
            "%s mr %.3f @ %.1f KiB/s",
            $peer->endpoint->key,
            $peer->greedy_marginal_return ?? 0.0,
            ($allocation["allocations"][$peer->endpoint->key] ?? 0.0) / 1024
        );
    }

    log_message(
        sprintf(
            "Greedy upload allocator: %.2f/%.2f MiB/s allocated; exploit %.2f/%.2f MiB/s across %d peer%s, explore %.2f/%.2f MiB/s across %d peer%s%s.",
            $allocation["allocated_total_rate"] / 1048576,
            UPLOAD_LIMIT_BYTES_PER_SECOND / 1048576,
            $allocation["allocated_exploit_rate"] / 1048576,
            $allocation["exploit_budget_rate"] / 1048576,
            $allocation["exploit_peer_count"],
            $allocation["exploit_peer_count"] === 1 ? "" : "s",
            $allocation["allocated_explore_rate"] / 1048576,
            $allocation["explore_budget_rate"] / 1048576,
            $allocation["explore_peer_count"],
            $allocation["explore_peer_count"] === 1 ? "" : "s",
            $top === [] ? "" : "; top " . implode(", ", $top)
        ),
        $log_stream
    );
}

// Live torrent downloading with policy-aware peer selection, upload-price discovery,
// marginal-return estimation, greedy upload allocation and connection optimisation.
function fill_runtime_piece_connections_live(
    &$connections,
    $peer_pool,
    &$attempted_peer_keys,
    &$peer_retry_after,
    $metadata_exchange,
    $piece_manager,
    $magnet,
    $local_peer_id,
    $connector = null,
    $local_dht_port = null,
    $peer_listener = null,
    $research_policy_mode = null
) {
    if(!($peer_pool instanceof PeerPool))
        throw new InvalidArgumentException("Live piece connection filling requires a peer pool.");

    if(!($metadata_exchange instanceof MetadataExchange) || !$metadata_exchange->is_complete())
        throw new InvalidArgumentException("Live piece downloading requires complete metadata.");

    if(!($piece_manager instanceof PieceManager))
        throw new InvalidArgumentException("Live piece downloading requires a piece manager.");

    $active_connection_count = 0;
    $outbound_connection_count = 0;
    $outbound_connection_limit = runtime_outbound_connection_limit($peer_listener, $connections);

    foreach($connections as $connection) {
        if($connection->is_terminal())
            continue;

        $active_connection_count++;

        if(!$connection->inbound)
            $outbound_connection_count++;
    }

    if(
        $active_connection_count >= DESIRED_CONNECTED_PEERS
        || $outbound_connection_count >= $outbound_connection_limit
    )
        return $active_connection_count;

    $now = microtime(true);
    $research_policy_mode = normalise_runtime_research_policy($research_policy_mode);
    $available_peer_market = $peer_pool->get_available_peers($now);
    $available_peers = $research_policy_mode === RESEARCH_POLICY_GREEDY
        ? rank_runtime_connection_candidates($available_peer_market, $now)
        : rank_runtime_standard_connection_candidates($available_peer_market, $now);

    foreach($available_peers as $peer) {
        if(
            $active_connection_count >= DESIRED_CONNECTED_PEERS
            || $outbound_connection_count >= $outbound_connection_limit
        )
            break;

        if(!runtime_peer_is_outbound_connectable($peer))
            continue;

        if(isset($peer_retry_after[$peer->endpoint->key]) && $now < $peer_retry_after[$peer->endpoint->key])
            continue;

        unset($peer_retry_after[$peer->endpoint->key]);
        $attempted_peer_keys[$peer->endpoint->key] = true;
        $connection = new PeerConnection(
            $peer,
            $magnet->info_hash,
            $local_peer_id,
            CONNECT_TIMEOUT,
            null,
            null,
            $metadata_exchange,
            $piece_manager,
            $local_dht_port
        );

        try {
            if(!$connection->start(null, $connector))
                continue;
        } catch(Throwable) {
            continue;
        }

        $connections[] = $connection;
        $active_connection_count++;
        $outbound_connection_count++;
    }

    return $active_connection_count;
}

function close_runtime_piece_connection($connection, &$peer_retry_after, $now) {
    if(!($connection instanceof PeerConnection))
        throw new InvalidArgumentException("Runtime piece connection close requires a peer connection.");

    $now = normalise_peer_time($now);
    $cooldown_until = $now + PEER_COOLDOWN;
    $peer_retry_after[$connection->peer->endpoint->key] = max(
        $peer_retry_after[$connection->peer->endpoint->key] ?? 0.0,
        $cooldown_until
    );
    $connection->close();
    $connection->peer->defer_connection_until($cooldown_until);
}

function adjust_runtime_piece_availability_from_bitfield(&$availability, $bitfield, $piece_count, $delta) {
    if(!is_array($availability) || !is_string($bitfield) || !is_int($piece_count))
        throw new InvalidArgumentException("Piece availability bitfield adjustment is invalid.");

    if(!is_int($delta) || !in_array($delta, [-1, 1], true))
        throw new InvalidArgumentException("Piece availability adjustment delta must be -1 or 1.");

    for($piece_index = 0; $piece_index < $piece_count; $piece_index++) {
        if(!peer_piece_bitfield_has_piece($bitfield, $piece_index, $piece_count))
            continue;

        $availability[$piece_index] = max(0, $availability[$piece_index] + $delta);
    }
}

function adjust_runtime_piece_availability_bitfield_difference(
    &$availability,
    $previous_bitfield,
    $current_bitfield,
    $piece_count
) {
    if(!is_array($availability) || !is_string($previous_bitfield) || !is_string($current_bitfield))
        throw new InvalidArgumentException("Piece availability bitfield difference is invalid.");

    $byte_count = intdiv($piece_count + 7, 8);
    $zero_bitfield = build_empty_peer_piece_bitfield($piece_count);

    if(strlen($previous_bitfield) !== $byte_count)
        $previous_bitfield = $zero_bitfield;

    if(strlen($current_bitfield) !== $byte_count)
        $current_bitfield = $zero_bitfield;

    for($byte_index = 0; $byte_index < $byte_count; $byte_index++) {
        $previous_byte = ord($previous_bitfield[$byte_index]);
        $current_byte = ord($current_bitfield[$byte_index]);
        $changed = $previous_byte ^ $current_byte;

        if($changed === 0)
            continue;

        for($bit_index = 0; $bit_index < 8; $bit_index++) {
            $piece_index = ($byte_index * 8) + $bit_index;

            if($piece_index >= $piece_count)
                break;

            $mask = 1 << (7 - $bit_index);

            if(($changed & $mask) === 0)
                continue;

            if(($current_byte & $mask) !== 0)
                $availability[$piece_index]++;
            else
                $availability[$piece_index] = max(0, $availability[$piece_index] - 1);
        }
    }
}

function calculate_runtime_piece_availability($connections, $piece_manager) {
    if(!is_array($connections))
        throw new InvalidArgumentException("Piece availability requires a connection list.");

    if(!($piece_manager instanceof PieceManager))
        throw new InvalidArgumentException("Piece availability requires a piece manager.");

    $availability = array_fill(0, $piece_manager->piece_count, 0);

    foreach($connections as $connection) {
        if(
            $connection->is_terminal()
            || $connection->state !== PeerConnection::STATE_ESTABLISHED
            || !$connection->remote_piece_information_received
        )
            continue;

        adjust_runtime_piece_availability_from_bitfield(
            $availability,
            $connection->get_remote_piece_bitfield(),
            $piece_manager->piece_count,
            1
        );
    }

    return $availability;
}

function select_runtime_rarest_piece($connections, $piece_manager) {
    if(!($piece_manager instanceof PieceManager))
        throw new InvalidArgumentException("Rarest-first piece selection requires a piece manager.");

    $availability = calculate_runtime_piece_availability($connections, $piece_manager);
    $selected_piece_index = null;
    $selected_availability = null;
    $selected_complete_block_count = -1;

    foreach($availability as $piece_index => $peer_count) {
        if($peer_count < 1 || $piece_manager->is_piece_complete($piece_index))
            continue;

        $complete_block_count = count($piece_manager->completed_blocks[$piece_index] ?? []);

        if(
            $selected_piece_index === null
            || $peer_count < $selected_availability
            || (
                $peer_count === $selected_availability
                && $complete_block_count > $selected_complete_block_count
            )
            || (
                $peer_count === $selected_availability
                && $complete_block_count === $selected_complete_block_count
                && $piece_index < $selected_piece_index
            )
        ) {
            $selected_piece_index = $piece_index;
            $selected_availability = $peer_count;
            $selected_complete_block_count = $complete_block_count;
        }
    }

    if($selected_piece_index === null)
        return null;

    return [
        "piece_index" => $selected_piece_index,
        "availability" => $selected_availability,
        "complete_block_count" => $selected_complete_block_count,
    ];
}

function build_runtime_rarest_piece_priority_from_availability($availability, $piece_manager) {
    if(!is_array($availability) || !($piece_manager instanceof PieceManager))
        throw new InvalidArgumentException("Rarest-first priority cache input is invalid.");

    $buckets = [];

    foreach($availability as $piece_index => $peer_count) {
        if($peer_count < 1 || isset($piece_manager->verified_pieces[$piece_index]))
            continue;

        $complete_block_count = count($piece_manager->completed_blocks[$piece_index] ?? []);
        $buckets[$peer_count][$complete_block_count][] = $piece_index;
    }

    if($buckets === [])
        return [];

    ksort($buckets, SORT_NUMERIC);
    $priority = [];

    foreach($buckets as $peer_count => $completion_buckets) {
        krsort($completion_buckets, SORT_NUMERIC);

        foreach($completion_buckets as $complete_block_count => $piece_indexes) {
            foreach($piece_indexes as $piece_index) {
                $priority[] = [
                    "piece_index" => $piece_index,
                    "availability" => (int)$peer_count,
                    "complete_block_count" => (int)$complete_block_count,
                ];
            }
        }
    }

    return $priority;
}

function build_runtime_rarest_piece_priority($connections, $piece_manager) {
    if(!($piece_manager instanceof PieceManager))
        throw new InvalidArgumentException("Rarest-first priority requires a piece manager.");

    return build_runtime_rarest_piece_priority_from_availability(
        calculate_runtime_piece_availability($connections, $piece_manager),
        $piece_manager
    );
}

final class RuntimePiecePriorityCache {
    private array $availability;
    private array $connection_snapshots = [];
    private array $priority = [];
    private array $peer_wanted_cache = [];
    private int $piece_manager_priority_state_version = -1;
    private int $generation = 0;
    private float $last_rebuild_at = 0.0;
    private bool $dirty = true;

    public function __construct($piece_manager) {
        if(!($piece_manager instanceof PieceManager))
            throw new InvalidArgumentException("Piece priority cache requires a piece manager.");

        $this->availability = array_fill(0, $piece_manager->piece_count, 0);
        $this->piece_manager_priority_state_version = $piece_manager->priority_state_version;
    }

    private function remove_snapshot($object_id, $piece_count) {
        if(!isset($this->connection_snapshots[$object_id]))
            return false;

        adjust_runtime_piece_availability_bitfield_difference(
            $this->availability,
            $this->connection_snapshots[$object_id]["bitfield"],
            build_empty_peer_piece_bitfield($piece_count),
            $piece_count
        );
        unset($this->connection_snapshots[$object_id], $this->peer_wanted_cache[$object_id]);
        $this->dirty = true;

        return true;
    }

    private function sync_connections($connections, $piece_manager) {
        $seen = [];
        $availability_changed = false;

        foreach($connections as $connection) {
            if(!($connection instanceof PeerConnection))
                continue;

            $object_id = spl_object_id($connection);
            $eligible = !$connection->is_terminal()
                && $connection->state === PeerConnection::STATE_ESTABLISHED
                && $connection->remote_piece_information_received;

            if(!$eligible) {
                if($this->remove_snapshot($object_id, $piece_manager->piece_count))
                    $availability_changed = true;

                continue;
            }

            $seen[$object_id] = true;
            $snapshot = $this->connection_snapshots[$object_id] ?? null;

            if($snapshot !== null && $snapshot["version"] === $connection->remote_piece_version)
                continue;

            $bitfield = $connection->get_remote_piece_bitfield();
            adjust_runtime_piece_availability_bitfield_difference(
                $this->availability,
                $snapshot["bitfield"] ?? build_empty_peer_piece_bitfield($piece_manager->piece_count),
                $bitfield,
                $piece_manager->piece_count
            );
            $this->connection_snapshots[$object_id] = [
                "version" => $connection->remote_piece_version,
                "bitfield" => $bitfield,
            ];
            unset($this->peer_wanted_cache[$object_id]);
            $this->dirty = true;
            $availability_changed = true;
        }

        foreach(array_keys($this->connection_snapshots) as $object_id) {
            if(isset($seen[$object_id]))
                continue;

            if($this->remove_snapshot($object_id, $piece_manager->piece_count))
                $availability_changed = true;
        }

        return $availability_changed;
    }

    public function get_priority($connections, $piece_manager, $now = null) {
        if(!is_array($connections) || !($piece_manager instanceof PieceManager))
            throw new InvalidArgumentException("Piece priority cache refresh input is invalid.");

        $now = normalise_peer_time($now);
        $availability_changed = $this->sync_connections($connections, $piece_manager);

        if($piece_manager->priority_state_version !== $this->piece_manager_priority_state_version) {
            $this->piece_manager_priority_state_version = $piece_manager->priority_state_version;
            $this->dirty = true;
            $this->peer_wanted_cache = [];
        }

        $rebuild_due = $this->priority === []
            || $availability_changed
            || ($this->dirty && $now - $this->last_rebuild_at >= PIECE_PRIORITY_REBUILD_MIN_INTERVAL);

        if($this->dirty && $rebuild_due) {
            $this->priority = build_runtime_rarest_piece_priority_from_availability(
                $this->availability,
                $piece_manager
            );
            $this->generation++;
            $this->last_rebuild_at = $now;
            $this->dirty = false;
        }

        return $this->priority;
    }

    public function find_peer_candidate($connection, $piece_manager, $excluded_piece_indexes = []) {
        if(!($connection instanceof PeerConnection) || !($piece_manager instanceof PieceManager))
            throw new InvalidArgumentException("Peer piece candidate lookup input is invalid.");

        if(!is_array($excluded_piece_indexes))
            throw new InvalidArgumentException("Peer piece exclusion set must be an array.");

        foreach($this->priority as $candidate) {
            $piece_index = $candidate["piece_index"];

            if(isset($excluded_piece_indexes[$piece_index]))
                continue;

            if(isset($piece_manager->verified_pieces[$piece_index]))
                continue;

            if($connection->has_remote_piece($piece_index))
                return $candidate;
        }

        return null;
    }

    public function get_peer_priority($connection, $piece_manager) {
        if(!($connection instanceof PeerConnection) || !($piece_manager instanceof PieceManager))
            throw new InvalidArgumentException("Peer piece priority cache input is invalid.");

        $priority = [];

        foreach($this->priority as $candidate) {
            $piece_index = $candidate["piece_index"];

            if(isset($piece_manager->verified_pieces[$piece_index]))
                continue;

            if($connection->has_remote_piece($piece_index))
                $priority[] = $candidate;
        }

        return $priority;
    }

    public function peer_has_wanted_piece($connection, $piece_manager) {
        if(!($connection instanceof PeerConnection) || !($piece_manager instanceof PieceManager))
            throw new InvalidArgumentException("Wanted-piece cache input is invalid.");

        $object_id = spl_object_id($connection);
        $cache = $this->peer_wanted_cache[$object_id] ?? null;

        if(
            $cache !== null
            && $cache["priority_state_version"] === $piece_manager->priority_state_version
            && $cache["remote_piece_version"] === $connection->remote_piece_version
        )
            return $cache["wanted"];

        $wanted = false;
        $bitfield = $connection->get_remote_piece_bitfield();
        $byte_count = strlen($bitfield);

        for($byte_index = 0; $byte_index < $byte_count && !$wanted; $byte_index++) {
            $remote_byte = ord($bitfield[$byte_index]);

            if($remote_byte === 0)
                continue;

            for($bit_index = 0; $bit_index < 8; $bit_index++) {
                $piece_index = ($byte_index * 8) + $bit_index;

                if($piece_index >= $piece_manager->piece_count)
                    break;

                if(($remote_byte & (1 << (7 - $bit_index))) === 0)
                    continue;

                if(!isset($piece_manager->verified_pieces[$piece_index])) {
                    $wanted = true;
                    break;
                }
            }
        }

        $this->peer_wanted_cache[$object_id] = [
            "priority_state_version" => $piece_manager->priority_state_version,
            "remote_piece_version" => $connection->remote_piece_version,
            "wanted" => $wanted,
        ];

        return $wanted;
    }

    public function get_generation() {
        return $this->generation;
    }

    public function get_profile_stats() {
        return [
            "generation" => $this->generation,
            "tracked_connections" => count($this->connection_snapshots),
            "wanted_cache_entries" => count($this->peer_wanted_cache),
            "priority_entries" => count($this->priority),
        ];
    }
}

function runtime_peer_has_wanted_piece($connection, $piece_priority, $piece_manager = null) {
    if(!($connection instanceof PeerConnection))
        throw new InvalidArgumentException("Wanted-piece inspection requires a peer connection.");

    if($piece_manager !== null && !($piece_manager instanceof PieceManager))
        throw new InvalidArgumentException("Wanted-piece inspection received an invalid piece manager.");

    foreach($piece_priority as $candidate) {
        $piece_index = $candidate["piece_index"];

        if($piece_manager !== null && isset($piece_manager->verified_pieces[$piece_index]))
            continue;

        if($connection->has_remote_piece($piece_index))
            return true;
    }

    return false;
}

function create_runtime_endgame_state() {
    return [
        "active" => false,
        "activated_at" => null,
        "activation_count" => 0,
        "duplicate_requests_queued" => 0,
        "duplicate_cancels_sent" => 0,
        "duplicate_blocks_won" => 0,
        "latest_unfinished_blocks" => null,
    ];
}

function runtime_endgame_status($piece_manager, $target_verified_piece_count) {
    if(!($piece_manager instanceof PieceManager))
        throw new InvalidArgumentException("Endgame status requires a piece manager.");

    if(!is_int($target_verified_piece_count) || $target_verified_piece_count < 0)
        throw new InvalidArgumentException("Endgame target piece count must be a non-negative integer.");

    if(ENDGAME_UNFINISHED_BLOCK_THRESHOLD < 1)
        throw new LogicException("ENDGAME_UNFINISHED_BLOCK_THRESHOLD must be positive.");

    if(ENDGAME_MAX_REQUEST_COPIES_PER_BLOCK < 2)
        throw new LogicException("ENDGAME_MAX_REQUEST_COPIES_PER_BLOCK must allow at least two request copies.");

    if(ENDGAME_DUPLICATE_MIN_AGE_SECONDS < 0.0)
        throw new LogicException("ENDGAME_DUPLICATE_MIN_AGE_SECONDS must not be negative.");

    if($target_verified_piece_count !== $piece_manager->piece_count) {
        return [
            "active" => false,
            "unfinished_blocks" => null,
        ];
    }

    $unfinished_blocks = $piece_manager->get_unfinished_block_count(
        ENDGAME_UNFINISHED_BLOCK_THRESHOLD
    );

    return [
        "active" => $unfinished_blocks > 0
            && $unfinished_blocks <= ENDGAME_UNFINISHED_BLOCK_THRESHOLD,
        "unfinished_blocks" => $unfinished_blocks,
    ];
}

function runtime_endgame_block_request_key($piece_index, $begin) {
    if(!is_int($piece_index) || $piece_index < 0 || !is_int($begin) || $begin < 0)
        throw new InvalidArgumentException("Endgame block request key requires non-negative indexes.");

    return "{$piece_index}:{$begin}";
}

function collect_runtime_endgame_request_copies($connections) {
    if(!is_array($connections))
        throw new InvalidArgumentException("Endgame request-copy collection requires connections.");

    $copies = [];

    foreach($connections as $connection) {
        if(!($connection instanceof PeerConnection))
            throw new InvalidArgumentException("Endgame request-copy collection received an invalid connection.");

        if($connection->is_terminal())
            continue;

        foreach($connection->get_outstanding_block_requests() as $request) {
            $key = runtime_endgame_block_request_key($request["piece_index"], $request["begin"]);
            $copies[$key][$connection->peer->endpoint->key] = true;
        }
    }

    return $copies;
}

function schedule_runtime_endgame_duplicates(
    $connections,
    $piece_manager,
    $selected_download_peer_keys,
    &$endgame_state,
    $target_verified_piece_count,
    $now = null
) {
    if(!is_array($connections) || !is_array($selected_download_peer_keys) || !is_array($endgame_state))
        throw new InvalidArgumentException("Endgame scheduling requires connection, selection and state arrays.");

    if(!($piece_manager instanceof PieceManager))
        throw new InvalidArgumentException("Endgame scheduling requires a piece manager.");

    $now = normalise_peer_time($now);
    $status = runtime_endgame_status($piece_manager, $target_verified_piece_count);
    $activated_now = false;
    $queued = [];
    $endgame_state["latest_unfinished_blocks"] = $status["unfinished_blocks"];

    if(!$status["active"]) {
        $endgame_state["active"] = false;

        return [
            "active" => false,
            "activated_now" => false,
            "unfinished_blocks" => $status["unfinished_blocks"],
            "queued" => [],
        ];
    }

    if(!$endgame_state["active"]) {
        $endgame_state["active"] = true;
        $endgame_state["activated_at"] = $now;
        $endgame_state["activation_count"]++;
        $activated_now = true;
    }

    $request_copies = collect_runtime_endgame_request_copies($connections);
    $primary_requests = $piece_manager->get_all_outstanding_requests();
    usort(
        $primary_requests,
        static function($left, $right) {
            if(abs($left["requested_at"] - $right["requested_at"]) > 0.000001)
                return $left["requested_at"] <=> $right["requested_at"];

            if($left["piece_index"] !== $right["piece_index"])
                return $left["piece_index"] <=> $right["piece_index"];

            return $left["begin"] <=> $right["begin"];
        }
    );

    $eligible_connections = [];

    foreach($connections as $connection) {
        if(
            $connection->is_terminal()
            || $connection->state !== PeerConnection::STATE_ESTABLISHED
            || $connection->remote_choking
            || !$connection->local_interested
            || !$connection->remote_piece_information_received
            || !isset($selected_download_peer_keys[$connection->peer->endpoint->key])
        )
            continue;

        $eligible_connections[] = $connection;
    }

    usort($eligible_connections, "runtime_greedy_compare_connections");

    foreach($primary_requests as $primary_request) {
        if($now - $primary_request["requested_at"] < ENDGAME_DUPLICATE_MIN_AGE_SECONDS)
            continue;

        $piece_index = $primary_request["piece_index"];
        $begin = $primary_request["begin"];
        $key = runtime_endgame_block_request_key($piece_index, $begin);
        $copy_count = count($request_copies[$key] ?? []);

        if($copy_count >= ENDGAME_MAX_REQUEST_COPIES_PER_BLOCK)
            continue;

        foreach($eligible_connections as $connection) {
            if($copy_count >= ENDGAME_MAX_REQUEST_COPIES_PER_BLOCK)
                break;

            if(!$connection->has_remote_piece($piece_index) || $connection->has_block_request($piece_index, $begin))
                continue;

            $pipeline = calculate_runtime_adaptive_request_pipeline($connection, $now);

            if($connection->get_outstanding_block_request_count() >= $pipeline["depth"])
                continue;

            try {
                $request = $connection->queue_endgame_duplicate_block_request(
                    $piece_index,
                    $begin,
                    $now
                );
            } catch(Throwable) {
                continue;
            }

            if($request === false)
                continue;

            $request_copies[$key][$connection->peer->endpoint->key] = true;
            $copy_count++;
            $endgame_state["duplicate_requests_queued"]++;
            $queued[] = [
                "piece_index" => $piece_index,
                "begin" => $begin,
                "peer_key" => $connection->peer->endpoint->key,
                "copy_count" => $copy_count,
            ];
        }
    }

    return [
        "active" => true,
        "activated_now" => $activated_now,
        "unfinished_blocks" => $status["unfinished_blocks"],
        "queued" => $queued,
    ];
}

function cancel_runtime_redundant_block_requests(
    $connections,
    $piece_index,
    $begin,
    $winner_connection,
    &$endgame_state
) {
    if(!is_array($connections) || !is_array($endgame_state))
        throw new InvalidArgumentException("Endgame cancellation requires connection and state arrays.");

    if(!($winner_connection instanceof PeerConnection))
        throw new InvalidArgumentException("Endgame cancellation requires a winning peer connection.");

    $cancelled = 0;

    foreach($connections as $connection) {
        if(!($connection instanceof PeerConnection))
            throw new InvalidArgumentException("Endgame cancellation received an invalid connection.");

        if($connection === $winner_connection || $connection->is_terminal())
            continue;

        if($connection->state !== PeerConnection::STATE_ESTABLISHED)
            continue;

        if(!$connection->has_block_request($piece_index, $begin))
            continue;

        if($connection->cancel_block_request($piece_index, $begin))
            $cancelled++;
    }

    $endgame_state["duplicate_cancels_sent"] += $cancelled;

    return $cancelled;
}

function cancel_runtime_piece_block_requests($connections, $piece_index) {
    if(!is_array($connections) || !is_int($piece_index) || $piece_index < 0)
        throw new InvalidArgumentException("Piece-request cancellation requires connections and a non-negative piece index.");

    $cancelled = 0;

    foreach($connections as $connection) {
        if(!($connection instanceof PeerConnection))
            throw new InvalidArgumentException("Piece-request cancellation received an invalid connection.");

        if($connection->is_terminal() || $connection->state !== PeerConnection::STATE_ESTABLISHED)
            continue;

        foreach($connection->get_outstanding_block_requests() as $request) {
            if($request["piece_index"] !== $piece_index)
                continue;

            if($connection->cancel_block_request($request["piece_index"], $request["begin"]))
                $cancelled++;
        }
    }

    return $cancelled;
}

function log_runtime_endgame_activation($result, $log_stream) {
    if(!is_array($result))
        throw new InvalidArgumentException("Endgame activation log requires a result array.");

    log_message(
        sprintf(
            "Endgame activated: %d unfinished block%s remain; allowing up to %d request copies per block after %.2fs, with redundant copies cancelled on first valid response.",
            $result["unfinished_blocks"],
            $result["unfinished_blocks"] === 1 ? "" : "s",
            ENDGAME_MAX_REQUEST_COPIES_PER_BLOCK,
            ENDGAME_DUPLICATE_MIN_AGE_SECONDS
        ),
        $log_stream
    );
}

function create_runtime_completion_shutdown_state() {
    return [
        "complete" => false,
        "download_requests_stopped" => false,
        "uploads_stopped" => false,
        "discovery_stopped" => false,
        "tracker_completed_announce_attempted" => false,
        "tracker_completed_announce_succeeded" => 0,
        "tracker_completed_announce_failed" => 0,
        "trackers_stopped" => false,
        "dht_stopped" => false,
        "pex_stopped" => false,
        "listener_stopped" => false,
        "port_mapping_removed" => false,
        "port_mapping_delete_attempted" => false,
        "peer_connections_closed" => 0,
        "storage_files_flushed" => 0,
        "logs_flushed" => false,
        "shutdown_at" => null,
    ];
}

function flush_runtime_log_stream($log_stream) {
    $target = runtime_resolve_log_stream($log_stream);

    if(!fflush($target))
        throw new RuntimeException("Runtime log stream could not be flushed.");

    return true;
}

function shutdown_runtime_completed_torrent(
    $connections,
    $piece_manager,
    $tracker_discovery,
    $dht_discovery,
    $log_stream,
    $now = null,
    $peer_listener = null,
    $port_mapping = null,
    $peer_pool = null
) {
    if(!is_array($connections))
        throw new InvalidArgumentException("Completion shutdown requires a connection list.");

    if(!($piece_manager instanceof PieceManager))
        throw new InvalidArgumentException("Completion shutdown requires a piece manager.");

    if($tracker_discovery !== null && !($tracker_discovery instanceof RuntimeTrackerDiscovery))
        throw new InvalidArgumentException("Completion shutdown received an invalid tracker discovery object.");

    if($dht_discovery !== null && !($dht_discovery instanceof RuntimeDhtDiscovery))
        throw new InvalidArgumentException("Completion shutdown received an invalid DHT discovery object.");

    if($peer_listener !== null && !($peer_listener instanceof RuntimePeerListener))
        throw new InvalidArgumentException("Completion shutdown received an invalid inbound peer listener.");

    if($port_mapping !== null && !($port_mapping instanceof RuntimePortMapping))
        throw new InvalidArgumentException("Completion shutdown received an invalid router port mapping.");

    if($peer_pool !== null && !($peer_pool instanceof PeerPool))
        throw new InvalidArgumentException("Completion shutdown received an invalid peer pool.");

    if(!is_resource($log_stream))
        throw new InvalidArgumentException("Completion shutdown requires an open log stream.");

    if(!$piece_manager->is_complete())
        throw new LogicException("Completion shutdown cannot run before every piece is verified.");

    $now = normalise_peer_time($now);
    $state = create_runtime_completion_shutdown_state();
    $state["complete"] = true;
    $state["download_requests_stopped"] = true;
    $state["uploads_stopped"] = true;
    $state["discovery_stopped"] = true;
    $state["pex_stopped"] = true;
    $state["shutdown_at"] = $now;

    $downloaded_bytes = runtime_downloaded_block_bytes($connections, $peer_pool);
    $uploaded_bytes = runtime_uploaded_block_bytes($connections, $peer_pool);

    if($tracker_discovery !== null) {
        $tracker_discovery->set_transfer_counters(
            $downloaded_bytes,
            $uploaded_bytes,
            0
        );
        $completed_announce = $tracker_discovery->announce_lifecycle_event("completed");
        $state["tracker_completed_announce_attempted"] = true;
        $state["tracker_completed_announce_succeeded"] = $completed_announce["succeeded"];
        $state["tracker_completed_announce_failed"] = $completed_announce["failed"];
        $tracker_discovery->close();
        $state["trackers_stopped"] = $tracker_discovery->is_closed();
    } else {
        $state["trackers_stopped"] = true;
    }

    if($dht_discovery !== null) {
        $dht_discovery->close();
        $state["dht_stopped"] = $dht_discovery->is_closed();
    } else {
        $state["dht_stopped"] = true;
    }

    if($peer_listener !== null) {
        $peer_listener->close();
        $state["listener_stopped"] = $peer_listener->is_closed();
    } else {
        $state["listener_stopped"] = true;
    }

    if($port_mapping !== null) {
        $mapping_was_active = $port_mapping->mapped;
        $port_mapping->close();
        $state["port_mapping_delete_attempted"] = $port_mapping->delete_attempted;
        $state["port_mapping_removed"] = !$mapping_was_active || $port_mapping->delete_succeeded === true;

        if($mapping_was_active && !$state["port_mapping_removed"])
            log_message("Router port mapping cleanup was attempted but could not be confirmed.", $log_stream);
    } else {
        $state["port_mapping_removed"] = true;
    }

    foreach($connections as $connection) {
        if(!($connection instanceof PeerConnection))
            throw new InvalidArgumentException("Completion shutdown received an invalid peer connection.");

        if(!$connection->is_terminal()) {
            $connection->close();
            $state["peer_connections_closed"]++;
        }
    }

    $state["storage_files_flushed"] = $piece_manager->storage->flush();

    log_message(
        sprintf(
            "Completion shutdown: COMPLETE; tracker completed announce %d successful/%d failed; download requests and uploads stopped; tracker, DHT, PEX and inbound listener stopped; router port mapping removed; %d peer connection%s closed; %d storage file%s flushed; no seeding state entered.",
            $state["tracker_completed_announce_succeeded"],
            $state["tracker_completed_announce_failed"],
            $state["peer_connections_closed"],
            $state["peer_connections_closed"] === 1 ? "" : "s",
            $state["storage_files_flushed"],
            $state["storage_files_flushed"] === 1 ? "" : "s"
        ),
        $log_stream
    );

    $state["logs_flushed"] = flush_runtime_log_stream($log_stream);

    return $state;
}

function runtime_adaptive_request_pipeline_rate($connection, $now = null) {
    if(!($connection instanceof PeerConnection))
        throw new InvalidArgumentException("Adaptive request-pipeline rate requires a peer connection.");

    $now = normalise_peer_time($now);
    $peer = $connection->peer;

    if($peer->performance_sample_count > 0) {
        $short_weight = (float)GREEDY_SHORT_RATE_WEIGHT;

        if($short_weight < 0.0 || $short_weight > 1.0)
            throw new LogicException("GREEDY_SHORT_RATE_WEIGHT must be between zero and one.");

        $short_rate = max(0.0, $peer->useful_download_rate_short);
        $long_rate = max(0.0, $peer->useful_download_rate_long);

        return ($short_rate * $short_weight)
            + ($long_rate * (1.0 - $short_weight));
    }

    if(
        $connection->successful_block_request_count < DOWNLOAD_PIPELINE_LIVE_MIN_COMPLETIONS
        || $connection->received_useful_block_bytes <= 0
        || $connection->handshake_completed_at === null
        || $now <= $connection->handshake_completed_at
    )
        return null;

    $elapsed = max(0.25, $now - $connection->handshake_completed_at);

    return $connection->received_useful_block_bytes / $elapsed;
}

function runtime_adaptive_request_pipeline_latency($connection) {
    if(!($connection instanceof PeerConnection))
        throw new InvalidArgumentException("Adaptive request-pipeline latency requires a peer connection.");

    $peer = $connection->peer;
    $short_latency = $peer->request_latency_short;
    $long_latency = $peer->request_latency_long;

    if($short_latency !== null || $long_latency !== null) {
        if($short_latency === null)
            return max(0.0, $long_latency);

        if($long_latency === null)
            return max(0.0, $short_latency);

        $short_weight = (float)GREEDY_SHORT_RATE_WEIGHT;

        return max(
            0.0,
            ($short_latency * $short_weight)
                + ($long_latency * (1.0 - $short_weight))
        );
    }

    if($connection->request_latency_count <= 0)
        return null;

    return max(
        0.0,
        $connection->request_latency_sum_seconds / $connection->request_latency_count
    );
}

function calculate_runtime_adaptive_request_pipeline($connection, $now = null) {
    if(!($connection instanceof PeerConnection))
        throw new InvalidArgumentException("Adaptive request pipeline requires a peer connection.");

    if(DOWNLOAD_PIPELINE_MIN_REQUESTS < 1)
        throw new LogicException("DOWNLOAD_PIPELINE_MIN_REQUESTS must be positive.");

    if(DOWNLOAD_PIPELINE_MAX_REQUESTS < DOWNLOAD_PIPELINE_MIN_REQUESTS)
        throw new LogicException("DOWNLOAD_PIPELINE_MAX_REQUESTS must not be below the minimum.");

    if(
        DOWNLOAD_PIPELINE_COLD_START_REQUESTS < DOWNLOAD_PIPELINE_MIN_REQUESTS
        || DOWNLOAD_PIPELINE_COLD_START_REQUESTS > DOWNLOAD_PIPELINE_MAX_REQUESTS
    )
        throw new LogicException("DOWNLOAD_PIPELINE_COLD_START_REQUESTS must lie within the pipeline bounds.");

    if(TARGET_PIPELINE_SECONDS <= 0.0 || !is_finite((float)TARGET_PIPELINE_SECONDS))
        throw new LogicException("TARGET_PIPELINE_SECONDS must be finite and positive.");

    if(DOWNLOAD_PIPELINE_LATENCY_HEADROOM < 1.0)
        throw new LogicException("DOWNLOAD_PIPELINE_LATENCY_HEADROOM must be at least one.");

    $now = normalise_peer_time($now);
    $estimated_rate = runtime_adaptive_request_pipeline_rate($connection, $now);
    $estimated_latency = runtime_adaptive_request_pipeline_latency($connection);
    $reason = "MEASURED";
    $target_seconds = (float)TARGET_PIPELINE_SECONDS;
    $target_bytes = 0.0;

    if($estimated_rate === null) {
        $depth = DOWNLOAD_PIPELINE_COLD_START_REQUESTS;
        $reason = "COLD_START";
    } elseif($estimated_rate <= 0.0) {
        $depth = DOWNLOAD_PIPELINE_MIN_REQUESTS;
        $reason = "NO_USEFUL_RATE";
    } else {
        if($estimated_latency !== null) {
            $target_seconds = max(
                $target_seconds,
                $estimated_latency * DOWNLOAD_PIPELINE_LATENCY_HEADROOM
            );
        }

        $target_bytes = $estimated_rate * $target_seconds;
        $depth = (int)ceil($target_bytes / PEER_BLOCK_MAX_LENGTH);
        $depth = max(
            DOWNLOAD_PIPELINE_MIN_REQUESTS,
            min(DOWNLOAD_PIPELINE_MAX_REQUESTS, $depth)
        );
    }

    $peer = $connection->peer;

    if($peer->request_pipeline_depth !== $depth) {
        $peer->request_pipeline_depth = $depth;
        $peer->request_pipeline_change_count++;
        $peer->last_request_pipeline_update_at = $now;
    } elseif($peer->last_request_pipeline_update_at === null) {
        $peer->last_request_pipeline_update_at = $now;
    }

    $peer->request_pipeline_max_depth = max($peer->request_pipeline_max_depth, $depth);
    $peer->request_pipeline_estimated_rate = max(0.0, $estimated_rate ?? 0.0);
    $peer->request_pipeline_latency = $estimated_latency;
    $peer->request_pipeline_target_bytes = max(0.0, $target_bytes);
    $peer->request_pipeline_target_seconds = $target_seconds;

    return [
        "depth" => $depth,
        "estimated_rate" => $estimated_rate,
        "estimated_latency" => $estimated_latency,
        "target_seconds" => $target_seconds,
        "target_bytes" => $target_bytes,
        "reason" => $reason,
    ];
}

function collect_runtime_adaptive_request_pipeline_statistics(
    $connections,
    $selected_download_peer_keys,
    $now = null
) {
    if(!is_array($connections) || !is_array($selected_download_peer_keys))
        throw new InvalidArgumentException("Adaptive request-pipeline statistics require arrays.");

    $now = normalise_peer_time($now);
    $depths = [];
    $total_target_requests = 0;
    $total_outstanding_requests = 0;

    foreach($connections as $connection) {
        if(!($connection instanceof PeerConnection))
            throw new InvalidArgumentException("Adaptive request-pipeline connections are invalid.");

        if(
            $connection->is_terminal()
            || $connection->state !== PeerConnection::STATE_ESTABLISHED
            || $connection->remote_choking
            || !isset($selected_download_peer_keys[$connection->peer->endpoint->key])
        )
            continue;

        $pipeline = calculate_runtime_adaptive_request_pipeline($connection, $now);
        $depths[] = $pipeline["depth"];
        $total_target_requests += $pipeline["depth"];
        $total_outstanding_requests += $connection->get_outstanding_block_request_count();
    }

    if($depths === []) {
        return [
            "peer_count" => 0,
            "minimum_depth" => 0,
            "maximum_depth" => 0,
            "average_depth" => 0.0,
            "target_requests" => 0,
            "outstanding_requests" => 0,
        ];
    }

    return [
        "peer_count" => count($depths),
        "minimum_depth" => min($depths),
        "maximum_depth" => max($depths),
        "average_depth" => array_sum($depths) / count($depths),
        "target_requests" => $total_target_requests,
        "outstanding_requests" => $total_outstanding_requests,
    ];
}

function log_runtime_adaptive_request_pipeline($statistics, $log_stream) {
    if(!is_array($statistics))
        throw new InvalidArgumentException("Adaptive request-pipeline log requires statistics.");

    log_message(
        sprintf(
            "Adaptive request pipeline: %d active selected peer%s; depth %d-%d requests, %.1f average; %d/%d requests currently outstanding/target; %.2fs target window, %d-request cap.",
            $statistics["peer_count"],
            $statistics["peer_count"] === 1 ? "" : "s",
            $statistics["minimum_depth"],
            $statistics["maximum_depth"],
            $statistics["average_depth"],
            $statistics["outstanding_requests"],
            $statistics["target_requests"],
            TARGET_PIPELINE_SECONDS,
            DOWNLOAD_PIPELINE_MAX_REQUESTS
        ),
        $log_stream
    );
}

function count_runtime_adaptive_pipeline_changes($peer_pool) {
    if(!($peer_pool instanceof PeerPool))
        throw new InvalidArgumentException("Adaptive pipeline change counting requires a peer pool.");

    $count = 0;

    foreach($peer_pool->get_peers() as $peer)
        $count += $peer->request_pipeline_change_count;

    return $count;
}

function maximum_runtime_adaptive_pipeline_depth($peer_pool) {
    if(!($peer_pool instanceof PeerPool))
        throw new InvalidArgumentException("Adaptive pipeline maximum-depth inspection requires a peer pool.");

    $maximum = DOWNLOAD_PIPELINE_COLD_START_REQUESTS;

    foreach($peer_pool->get_peers() as $peer)
        $maximum = max($maximum, $peer->request_pipeline_max_depth);

    return $maximum;
}

function log_runtime_peer_frontier($peer_pool, $log_stream, $now = null) {
    if(!($peer_pool instanceof PeerPool))
        throw new InvalidArgumentException("Peer frontier logging requires a peer pool.");

    $state = runtime_supplier_market_state($peer_pool, $now);
    $top_failures = $state["failure_categories"];
    arsort($top_failures);
    $failure_parts = [];

    foreach(array_slice($top_failures, 0, 4, true) as $category => $count)
        $failure_parts[] = strtolower($category) . "=" . $count;

    log_message(
        sprintf(
            "Peer frontier: %d known; %d connecting, %d established, %d unchoked, %d recently useful at %.2f MiB/s aggregate; %d immediately available, %d cooling down; %d connection failures%s.",
            $state["known_peers"],
            $state["connecting_peers"],
            $state["established_peers"],
            $state["unchoked_peers"],
            $state["useful_peers"],
            $state["aggregate_useful_rate"] / 1048576,
            $state["available_peers"],
            $state["cooldown_peers"],
            $state["connection_failures"],
            $failure_parts === [] ? "" : " (" . implode(", ", $failure_parts) . ")"
        ),
        $log_stream
    );
}

function runtime_download_connection_statistics($connections) {
    $statistics = [
        "active" => 0,
        "established" => 0,
        "unchoked" => 0,
        "interested" => 0,
        "local_unchoked" => 0,
        "pending_upload_requests" => 0,
        "outstanding_requests" => 0,
        "inbound_active" => 0,
        "inbound_established" => 0,
        "inbound_unchoked" => 0,
    ];

    foreach($connections as $connection) {
        if($connection->is_terminal())
            continue;

        $statistics["active"]++;

        if($connection->inbound)
            $statistics["inbound_active"]++;

        if($connection->state !== PeerConnection::STATE_ESTABLISHED)
            continue;

        $statistics["established"]++;

        if($connection->inbound)
            $statistics["inbound_established"]++;

        if(!$connection->remote_choking) {
            $statistics["unchoked"]++;

            if($connection->inbound)
                $statistics["inbound_unchoked"]++;
        }

        if($connection->local_interested)
            $statistics["interested"]++;

        if(!$connection->local_choking)
            $statistics["local_unchoked"]++;

        $statistics["pending_upload_requests"] += $connection->get_pending_upload_request_count();
        $statistics["outstanding_requests"] += $connection->get_outstanding_block_request_count();
    }

    return $statistics;
}

function select_runtime_basic_piece($connections, $piece_manager) {
    $selection = select_runtime_rarest_piece($connections, $piece_manager);

    return $selection["piece_index"] ?? null;
}

function runtime_piece_has_committed_progress($piece_manager, $piece_index) {
    if(!($piece_manager instanceof PieceManager))
        throw new InvalidArgumentException("Piece progress inspection requires a piece manager.");

    $piece_state = $piece_manager->get_piece_state($piece_index);

    if($piece_state["complete_block_count"] > 0)
        return true;

    return isset($piece_manager->outstanding_requests[$piece_index])
        && $piece_manager->outstanding_requests[$piece_index] !== [];
}

function runtime_piece_has_live_provider($connections, $piece_index) {
    foreach($connections as $connection) {
        if($connection->is_terminal() || $connection->state !== PeerConnection::STATE_ESTABLISHED)
            continue;

        if($connection->has_remote_piece($piece_index))
            return true;
    }

    return false;
}

function prune_runtime_empty_terminal_connections(&$connections) {
    if(!is_array($connections))
        throw new InvalidArgumentException("Empty terminal connection pruning requires a connection list.");

    $retained = [];
    $pruned = 0;

    foreach($connections as $connection) {
        if(!($connection instanceof PeerConnection))
            throw new InvalidArgumentException("Empty terminal connection pruning received an invalid connection.");

        $has_metric_activity = $connection->received_block_bytes > 0
            || $connection->received_useful_block_bytes > 0
            || $connection->uploaded_block_bytes > 0
            || $connection->successful_block_request_count > 0
            || $connection->failed_block_request_count > 0
            || $connection->request_latency_count > 0;

        if($connection->is_terminal() && !$has_metric_activity) {
            $pruned++;
            continue;
        }

        $retained[] = $connection;
    }

    if($pruned > 0)
        $connections = $retained;

    return $pruned;
}

function archive_runtime_terminal_connection_metrics($connection) {
    if(!($connection instanceof PeerConnection))
        throw new InvalidArgumentException("Terminal connection metric archival requires a peer connection.");

    if(!$connection->is_terminal() || $connection->metrics_archived)
        return false;

    $peer = $connection->peer;
    $peer->archived_downloaded_payload_bytes += $connection->received_block_bytes;
    $peer->archived_useful_downloaded_payload_bytes += $connection->received_useful_block_bytes;
    $peer->archived_uploaded_payload_bytes += $connection->uploaded_block_bytes;
    $peer->archived_successful_block_requests += $connection->successful_block_request_count;
    $peer->archived_failed_block_requests += $connection->failed_block_request_count;
    $peer->archived_request_latency_sum_seconds += $connection->request_latency_sum_seconds;
    $peer->archived_request_latency_count += $connection->request_latency_count;
    $connection->metrics_archived = true;

    return true;
}

function prune_runtime_terminal_connections(&$connections) {
    if(!is_array($connections))
        throw new InvalidArgumentException("Terminal connection pruning requires a connection list.");

    $retained = [];
    $pruned = 0;

    foreach($connections as $connection) {
        if(!($connection instanceof PeerConnection))
            throw new InvalidArgumentException("Terminal connection pruning received an invalid connection.");

        if(!$connection->is_terminal()) {
            $retained[] = $connection;
            continue;
        }

        archive_runtime_terminal_connection_metrics($connection);
        $pruned++;
    }

    if($pruned > 0)
        $connections = $retained;

    return $pruned;
}

function runtime_downloaded_block_bytes($connections, $peer_pool = null) {
    if($peer_pool !== null && !($peer_pool instanceof PeerPool))
        throw new InvalidArgumentException("Downloaded byte accounting received an invalid peer pool.");

    $downloaded_bytes = 0;

    if($peer_pool !== null) {
        foreach($peer_pool->get_peers() as $peer)
            $downloaded_bytes += $peer->archived_downloaded_payload_bytes;
    }

    foreach($connections as $connection) {
        if($peer_pool !== null && $connection->metrics_archived)
            continue;

        $downloaded_bytes += $connection->received_block_bytes;
    }

    return $downloaded_bytes;
}

function runtime_uploaded_block_bytes($connections, $peer_pool = null) {
    if($peer_pool !== null && !($peer_pool instanceof PeerPool))
        throw new InvalidArgumentException("Uploaded byte accounting received an invalid peer pool.");

    $uploaded_bytes = 0;

    if($peer_pool !== null) {
        foreach($peer_pool->get_peers() as $peer)
            $uploaded_bytes += $peer->archived_uploaded_payload_bytes;
    }

    foreach($connections as $connection) {
        if($peer_pool !== null && $connection->metrics_archived)
            continue;

        $uploaded_bytes += $connection->uploaded_block_bytes;
    }

    return $uploaded_bytes;
}

function create_runtime_upload_limiter($now = null) {
    $now = normalise_peer_time($now);
    $global_capacity = max(
        PEER_BLOCK_MAX_LENGTH,
        UPLOAD_LIMIT_BYTES_PER_SECOND * BASIC_UPLOAD_BURST_SECONDS
    );

    return [
        "global_tokens" => floatval($global_capacity),
        "global_capacity" => floatval($global_capacity),
        "global_updated_at" => $now,
        "peers" => [],
    ];
}

function runtime_upload_limiter_consume(
    &$limiter,
    $peer_key,
    $bytes,
    $peer_rate_bytes_per_second,
    $now = null
) {
    if(!is_array($limiter) || !isset(
        $limiter["global_tokens"],
        $limiter["global_capacity"],
        $limiter["global_updated_at"],
        $limiter["peers"]
    ))
        throw new InvalidArgumentException("Runtime upload limiter state is invalid.");

    if(!is_string($peer_key) || $peer_key === "")
        throw new InvalidArgumentException("Runtime upload limiter peer key must be non-empty.");

    if(!is_int($bytes) || $bytes < 1)
        throw new InvalidArgumentException("Runtime upload limiter byte count must be positive.");

    if(
        (!is_int($peer_rate_bytes_per_second) && !is_float($peer_rate_bytes_per_second))
        || !is_finite(floatval($peer_rate_bytes_per_second))
        || $peer_rate_bytes_per_second <= 0
    )
        throw new InvalidArgumentException("Runtime per-peer upload rate must be positive.");

    $now = normalise_peer_time($now);
    $global_elapsed = max(0.0, $now - $limiter["global_updated_at"]);
    $limiter["global_tokens"] = min(
        $limiter["global_capacity"],
        $limiter["global_tokens"] + ($global_elapsed * UPLOAD_LIMIT_BYTES_PER_SECOND)
    );
    $limiter["global_updated_at"] = $now;
    $peer_capacity = max(
        PEER_BLOCK_MAX_LENGTH,
        $peer_rate_bytes_per_second * BASIC_UPLOAD_BURST_SECONDS
    );

    if(!isset($limiter["peers"][$peer_key])) {
        $limiter["peers"][$peer_key] = [
            "tokens" => floatval($peer_capacity),
            "capacity" => floatval($peer_capacity),
            "rate" => floatval($peer_rate_bytes_per_second),
            "updated_at" => $now,
        ];
    } else {
        $peer_state = $limiter["peers"][$peer_key];
        $peer_elapsed = max(0.0, $now - $peer_state["updated_at"]);
        $peer_state["tokens"] = min(
            floatval($peer_capacity),
            $peer_state["tokens"] + ($peer_elapsed * $peer_state["rate"])
        );
        $peer_state["capacity"] = floatval($peer_capacity);
        $peer_state["rate"] = floatval($peer_rate_bytes_per_second);
        $peer_state["updated_at"] = $now;
        $limiter["peers"][$peer_key] = $peer_state;
    }

    if(
        $limiter["global_tokens"] + 0.000001 < $bytes
        || $limiter["peers"][$peer_key]["tokens"] + 0.000001 < $bytes
    )
        return false;

    $limiter["global_tokens"] -= $bytes;
    $limiter["peers"][$peer_key]["tokens"] -= $bytes;

    return true;
}

function normalise_runtime_research_policy($policy_mode = null) {
    if($policy_mode === null)
        $policy_mode = RESEARCH_POLICY_MODE;

    if(!is_string($policy_mode))
        throw new InvalidArgumentException("Research policy mode must be a string.");

    $policy_mode = strtoupper(trim($policy_mode));

    if(!in_array($policy_mode, [RESEARCH_POLICY_STANDARD, RESEARCH_POLICY_GREEDY], true))
        throw new InvalidArgumentException("Research policy mode must be STANDARD or GREEDY.");

    return $policy_mode;
}

function runtime_research_policy_is_greedy($policy_mode = null) {
    return normalise_runtime_research_policy($policy_mode) === RESEARCH_POLICY_GREEDY;
}

function rank_runtime_standard_connection_candidates($peers, $now = null) {
    if(!is_array($peers))
        throw new InvalidArgumentException("Standard connection candidate ranking requires a peer list.");

    normalise_peer_time($now);
    $ranked = array_values($peers);

    foreach($ranked as $peer) {
        if(!($peer instanceof Peer))
            throw new InvalidArgumentException("Standard connection candidate ranking requires Peer objects.");
    }

    usort(
        $ranked,
        static function($left, $right) {
            $left_untested = $left->connection_attempts === 0;
            $right_untested = $right->connection_attempts === 0;

            if($left_untested !== $right_untested)
                return $left_untested ? -1 : 1;

            if($left->consecutive_failures !== $right->consecutive_failures)
                return $left->consecutive_failures <=> $right->consecutive_failures;

            $left_sources = count($left->sources);
            $right_sources = count($right->sources);

            if($left_sources !== $right_sources)
                return $right_sources <=> $left_sources;

            if(abs($left->last_discovered_at - $right->last_discovered_at) > 0.000001)
                return $left->last_discovered_at > $right->last_discovered_at ? -1 : 1;

            return strcmp($left->endpoint->key, $right->endpoint->key);
        }
    );

    return $ranked;
}

function update_runtime_basic_upload_slots($connections) {
    $candidates = [];

    foreach($connections as $connection) {
        if(
            $connection->is_terminal()
            || $connection->state !== PeerConnection::STATE_ESTABLISHED
            || !$connection->remote_interested
            || $connection->peer->is_seed
            || $connection->piece_manager === null
            || $connection->piece_manager->verified_piece_count === 0
        )
            continue;

        $candidates[] = $connection;
    }

    usort(
        $candidates,
        static function($left, $right) {
            if($left->local_choking !== $right->local_choking)
                return $left->local_choking ? 1 : -1;

            return strcmp($left->peer->endpoint->key, $right->peer->endpoint->key);
        }
    );

    $selected = [];

    foreach(array_slice($candidates, 0, DESIRED_UPLOAD_PEERS) as $connection)
        $selected[spl_object_id($connection)] = true;

    foreach($connections as $connection) {
        if($connection->is_terminal() || $connection->state !== PeerConnection::STATE_ESTABLISHED)
            continue;

        $should_unchoke = isset($selected[spl_object_id($connection)]);

        if($connection->local_choking === $should_unchoke)
            $connection->set_local_choking(!$should_unchoke);
    }

    return count($selected);
}

function create_runtime_standard_choking_state($now = null) {
    $now = normalise_peer_time($now);

    return [
        "last_evaluation_at" => $now - PEER_EVALUATION_INTERVAL,
        "last_optimistic_at" => $now - STANDARD_OPTIMISTIC_UNCHOKE_INTERVAL,
        "last_refresh_at" => $now - STANDARD_POLICY_REFRESH_INTERVAL,
        "previous_downloaded_bytes" => [],
        "recent_download_rates" => [],
        "optimistic_peer_key" => null,
        "latest_result" => null,
    ];
}

function runtime_standard_choking_candidates($connections) {
    $candidates = [];

    foreach($connections as $connection) {
        if(
            $connection->is_terminal()
            || $connection->state !== PeerConnection::STATE_ESTABLISHED
            || !$connection->remote_interested
            || $connection->peer->is_seed
            || $connection->piece_manager === null
            || $connection->piece_manager->verified_piece_count === 0
        )
            continue;

        $candidates[$connection->peer->endpoint->key] = $connection;
    }

    return $candidates;
}

function select_runtime_standard_optimistic_peer($candidates, $regular_selected, $random_int_generator = null) {
    $eligible = [];

    foreach($candidates as $peer_key => $connection) {
        if(!isset($regular_selected[$peer_key]))
            $eligible[$peer_key] = $connection;
    }

    if($eligible === [])
        return null;

    $keys = array_keys($eligible);

    if($random_int_generator === null)
        $random_int_generator = "random_int";

    if(!is_callable($random_int_generator))
        throw new InvalidArgumentException("Optimistic unchoke random generator must be callable.");

    $selected_index = $random_int_generator(0, count($keys) - 1);

    if(!is_int($selected_index) || $selected_index < 0 || $selected_index >= count($keys))
        throw new RuntimeException("Optimistic unchoke random generator returned an invalid index.");

    return $keys[$selected_index];
}

function update_runtime_standard_upload_slots(
    $connections,
    &$choking_state,
    $now = null,
    $random_int_generator = null
) {
    if(!is_array($choking_state) || !array_key_exists("latest_result", $choking_state) || !isset(
        $choking_state["last_evaluation_at"],
        $choking_state["last_optimistic_at"],
        $choking_state["last_refresh_at"],
        $choking_state["previous_downloaded_bytes"],
        $choking_state["recent_download_rates"]
    ))
        throw new InvalidArgumentException("Standard choking state is invalid.");

    $now = normalise_peer_time($now);
    $evaluation_due = $now - $choking_state["last_evaluation_at"] >= PEER_EVALUATION_INTERVAL;
    $optimistic_due = $now - $choking_state["last_optimistic_at"] >= STANDARD_OPTIMISTIC_UNCHOKE_INTERVAL;
    $refresh_due = $choking_state["latest_result"] === null
        || $evaluation_due
        || $optimistic_due
        || $now - $choking_state["last_refresh_at"] >= STANDARD_POLICY_REFRESH_INTERVAL;

    if(!$refresh_due) {
        $result = $choking_state["latest_result"];
        $result["evaluation_due"] = false;
        $result["optimistic_due"] = false;
        $result["refresh_due"] = false;

        return $result;
    }

    $candidates = runtime_standard_choking_candidates($connections);

    if($evaluation_due) {
        $elapsed = max(0.001, $now - $choking_state["last_evaluation_at"]);
        $recent_download_rates = [];
        $current_downloaded_bytes = [];

        foreach($connections as $connection) {
            if($connection->is_terminal() || $connection->state !== PeerConnection::STATE_ESTABLISHED)
                continue;

            $peer_key = $connection->peer->endpoint->key;
            $current_downloaded_bytes[$peer_key] = $connection->received_block_bytes;
        }

        foreach($candidates as $peer_key => $connection) {
            $current = $current_downloaded_bytes[$peer_key] ?? 0;
            $previous = $choking_state["previous_downloaded_bytes"][$peer_key] ?? 0;
            $recent_download_rates[$peer_key] = max(0, $current - $previous) / $elapsed;
        }

        $choking_state["recent_download_rates"] = $recent_download_rates;
        $choking_state["previous_downloaded_bytes"] = $current_downloaded_bytes;
        $choking_state["last_evaluation_at"] = $now;
    }

    $ranked = array_values($candidates);
    usort(
        $ranked,
        static function($left, $right) use ($choking_state) {
            $left_key = $left->peer->endpoint->key;
            $right_key = $right->peer->endpoint->key;
            $left_rate = $choking_state["recent_download_rates"][$left_key] ?? 0.0;
            $right_rate = $choking_state["recent_download_rates"][$right_key] ?? 0.0;

            if(abs($left_rate - $right_rate) > 0.000001)
                return $left_rate > $right_rate ? -1 : 1;

            if($left->local_choking !== $right->local_choking)
                return $left->local_choking ? 1 : -1;

            return strcmp($left_key, $right_key);
        }
    );

    $regular_slot_count = max(0, DESIRED_UPLOAD_PEERS - 1);
    $regular_selected = [];

    foreach(array_slice($ranked, 0, $regular_slot_count) as $connection)
        $regular_selected[$connection->peer->endpoint->key] = true;

    $optimistic_peer_key = $choking_state["optimistic_peer_key"] ?? null;

    if(
        $optimistic_peer_key === null
        || !isset($candidates[$optimistic_peer_key])
        || isset($regular_selected[$optimistic_peer_key])
        || $optimistic_due
    ) {
        $optimistic_peer_key = select_runtime_standard_optimistic_peer(
            $candidates,
            $regular_selected,
            $random_int_generator
        );
        $choking_state["optimistic_peer_key"] = $optimistic_peer_key;
        $choking_state["last_optimistic_at"] = $now;
    }

    $selected = $regular_selected;

    if($optimistic_peer_key !== null)
        $selected[$optimistic_peer_key] = true;

    foreach($connections as $connection) {
        if($connection->is_terminal() || $connection->state !== PeerConnection::STATE_ESTABLISHED)
            continue;

        $peer_key = $connection->peer->endpoint->key;
        $should_unchoke = isset($selected[$peer_key]);

        if(isset($regular_selected[$peer_key]))
            $connection->peer->policy_state = "STANDARD_REGULAR";
        elseif($optimistic_peer_key !== null && $peer_key === $optimistic_peer_key)
            $connection->peer->policy_state = "STANDARD_OPTIMISTIC";
        else
            $connection->peer->policy_state = "STANDARD_CHOKED";

        if($connection->local_choking === $should_unchoke)
            $connection->set_local_choking(!$should_unchoke);
    }

    $result = [
        "slot_count" => count($selected),
        "regular_slot_count" => count($regular_selected),
        "optimistic_peer_key" => $optimistic_peer_key,
        "selected_peer_keys" => $selected,
        "candidate_count" => count($candidates),
        "evaluation_due" => $evaluation_due,
        "optimistic_due" => $optimistic_due,
        "refresh_due" => true,
    ];
    $choking_state["last_refresh_at"] = $now;
    $choking_state["latest_result"] = $result;

    return $result;
}

function select_runtime_standard_download_peer_keys(
    $connections,
    $piece_manager,
    $piece_priority_cache
) {
    if(!is_array($connections) || !($piece_manager instanceof PieceManager))
        throw new InvalidArgumentException("Standard download selection input is invalid.");

    if(!($piece_priority_cache instanceof RuntimePiecePriorityCache))
        throw new InvalidArgumentException("Standard download selection requires a piece priority cache.");

    $selected = [];

    foreach($connections as $connection) {
        if(
            !($connection instanceof PeerConnection)
            || $connection->is_terminal()
            || $connection->state !== PeerConnection::STATE_ESTABLISHED
            || $connection->fresh_turnover_retiring
            || !$connection->remote_piece_information_received
            || !$piece_priority_cache->peer_has_wanted_piece($connection, $piece_manager)
        )
            continue;

        $selected[$connection->peer->endpoint->key] = true;
    }

    return $selected;
}

function log_runtime_standard_policy_decision(
    $selected_download_peer_keys,
    $upload_result,
    $log_stream
) {
    if(!is_array($selected_download_peer_keys) || !is_array($upload_result))
        throw new InvalidArgumentException("Standard policy decision log input is invalid.");

    log_message(
        sprintf(
            "Standard control: %d download peer%s eligible; %d/%d upload slot%s active (%d regular, optimistic %s).",
            count($selected_download_peer_keys),
            count($selected_download_peer_keys) === 1 ? "" : "s",
            $upload_result["slot_count"],
            DESIRED_UPLOAD_PEERS,
            $upload_result["slot_count"] === 1 ? "" : "s",
            $upload_result["regular_slot_count"],
            $upload_result["optimistic_peer_key"] ?? "none"
        ),
        $log_stream
    );
}

function create_runtime_greedy_policy_state($now = null) {
    $now = normalise_peer_time($now);

    return [
        "last_evaluation_at" => $now - OPTIMISER_INTERVAL,
        "last_refresh_at" => $now - GREEDY_POLICY_REFRESH_INTERVAL,
        "download_exploration_cursor" => 0,
        "upload_exploration_cursor" => 0,
        "latest_result" => null,
    ];
}

function runtime_greedy_peer_value($peer) {
    if(!($peer instanceof Peer))
        throw new InvalidArgumentException("Greedy peer value requires a peer.");

    $short_weight = (float)GREEDY_SHORT_RATE_WEIGHT;

    if($short_weight < 0.0 || $short_weight > 1.0)
        throw new LogicException("GREEDY_SHORT_RATE_WEIGHT must be between zero and one.");

    $short_rate = max(0.0, $peer->useful_download_rate_short);
    $long_rate = max(0.0, $peer->useful_download_rate_long);

    return ($short_rate * $short_weight)
        + ($long_rate * (1.0 - $short_weight));
}

function runtime_greedy_compare_connections($left, $right) {
    if(!($left instanceof PeerConnection) || !($right instanceof PeerConnection))
        throw new InvalidArgumentException("Greedy peer comparison requires peer connections.");

    $left_value = runtime_greedy_peer_value($left->peer);
    $right_value = runtime_greedy_peer_value($right->peer);

    if(abs($left_value - $right_value) > 0.000001)
        return $left_value > $right_value ? -1 : 1;

    $left_reliability = $left->peer->reliability_long ?? -1.0;
    $right_reliability = $right->peer->reliability_long ?? -1.0;

    if(abs($left_reliability - $right_reliability) > 0.000001)
        return $left_reliability > $right_reliability ? -1 : 1;

    $left_latency = $left->peer->request_latency_long ?? INF;
    $right_latency = $right->peer->request_latency_long ?? INF;

    if($left_latency !== $right_latency)
        return $left_latency < $right_latency ? -1 : 1;

    if($left->peer->performance_sample_count !== $right->peer->performance_sample_count)
        return $left->peer->performance_sample_count <=> $right->peer->performance_sample_count;

    return strcmp($left->peer->endpoint->key, $right->peer->endpoint->key);
}

function runtime_greedy_exploration_slot_count($slot_limit, $candidate_count) {
    if(!is_int($slot_limit) || $slot_limit < 0)
        throw new InvalidArgumentException("Greedy slot limit must be a non-negative integer.");

    if(!is_int($candidate_count) || $candidate_count < 0)
        throw new InvalidArgumentException("Greedy candidate count must be a non-negative integer.");

    $selected_count = min($slot_limit, $candidate_count);

    if($selected_count <= 1 || EXPLORE_RATIO <= 0.0)
        return 0;

    return min(
        $selected_count,
        max(1, (int)ceil($slot_limit * EXPLORE_RATIO))
    );
}

function select_runtime_greedy_connections($candidates, $slot_limit, $exploration_cursor = 0) {
    if(!is_array($candidates))
        throw new InvalidArgumentException("Greedy candidate set must be an array.");

    if(!is_int($slot_limit) || $slot_limit < 0)
        throw new InvalidArgumentException("Greedy slot limit must be a non-negative integer.");

    if(!is_int($exploration_cursor) || $exploration_cursor < 0)
        throw new InvalidArgumentException("Greedy exploration cursor must be a non-negative integer.");

    $ranked = array_values($candidates);

    foreach($ranked as $connection) {
        if(!($connection instanceof PeerConnection))
            throw new InvalidArgumentException("Greedy candidates must contain peer connections.");
    }

    usort($ranked, "runtime_greedy_compare_connections");
    $selected_count = min($slot_limit, count($ranked));

    if($selected_count === 0) {
        return [
            "selected" => [],
            "exploit" => [],
            "explore" => [],
            "candidate_count" => count($ranked),
            "exploit_slot_count" => 0,
            "explore_slot_count" => 0,
        ];
    }

    $explore_slot_count = runtime_greedy_exploration_slot_count(
        $slot_limit,
        count($ranked)
    );
    $exploit_slot_count = max(0, $selected_count - $explore_slot_count);
    $exploit_connections = array_slice($ranked, 0, $exploit_slot_count);
    $remaining = array_slice($ranked, $exploit_slot_count);
    $explore_connections = [];

    if($explore_slot_count > 0 && $remaining !== []) {
        $remaining_count = count($remaining);
        $start = $exploration_cursor % $remaining_count;

        for($offset = 0; $offset < $explore_slot_count; $offset++)
            $explore_connections[] = $remaining[($start + $offset) % $remaining_count];
    }

    $exploit = [];
    $explore = [];
    $selected = [];

    foreach($exploit_connections as $connection) {
        $peer_key = $connection->peer->endpoint->key;
        $exploit[$peer_key] = $connection;
        $selected[$peer_key] = $connection;
    }

    foreach($explore_connections as $connection) {
        $peer_key = $connection->peer->endpoint->key;

        if(isset($selected[$peer_key]))
            continue;

        $explore[$peer_key] = $connection;
        $selected[$peer_key] = $connection;
    }

    return [
        "selected" => $selected,
        "exploit" => $exploit,
        "explore" => $explore,
        "candidate_count" => count($ranked),
        "exploit_slot_count" => count($exploit),
        "explore_slot_count" => count($explore),
    ];
}

function runtime_greedy_download_candidates(
    $connections,
    $piece_priority,
    $piece_priority_cache = null,
    $piece_manager = null
) {
    if(!is_array($piece_priority))
        throw new InvalidArgumentException("Greedy download selection requires piece priority.");

    if($piece_priority_cache !== null && !($piece_priority_cache instanceof RuntimePiecePriorityCache))
        throw new InvalidArgumentException("Greedy download selection received an invalid piece-priority cache.");

    if($piece_manager !== null && !($piece_manager instanceof PieceManager))
        throw new InvalidArgumentException("Greedy download selection received an invalid piece manager.");

    $candidates = [];

    foreach($connections as $connection) {
        if(
            $connection->is_terminal()
            || $connection->state !== PeerConnection::STATE_ESTABLISHED
            || $connection->fresh_turnover_retiring
            || !$connection->remote_piece_information_received
            || $connection->remote_choking
        )
            continue;

        $has_wanted_piece = $piece_priority_cache !== null && $piece_manager !== null
            ? $piece_priority_cache->peer_has_wanted_piece($connection, $piece_manager)
            : runtime_peer_has_wanted_piece($connection, $piece_priority, $piece_manager);

        if(!$has_wanted_piece)
            continue;

        $candidates[$connection->peer->endpoint->key] = $connection;
    }

    return $candidates;
}

function runtime_greedy_upload_candidates($connections) {
    $candidates = runtime_standard_choking_candidates($connections);

    foreach($candidates as $peer_key => $connection) {
        if($connection->fresh_turnover_retiring) {
            unset($candidates[$peer_key]);

            continue;
        }

        if(
            $connection->peer->greedy_price_state === "STABLE"
            && $connection->peer->greedy_upload_price === 0.0
        )
            unset($candidates[$peer_key]);
    }

    return $candidates;
}

function update_runtime_greedy_policy(
    $connections,
    $piece_priority,
    &$policy_state,
    $now = null,
    $piece_priority_cache = null,
    $piece_manager = null
) {
    if(!is_array($policy_state) || !array_key_exists("latest_result", $policy_state) || !isset(
        $policy_state["last_evaluation_at"],
        $policy_state["last_refresh_at"],
        $policy_state["download_exploration_cursor"],
        $policy_state["upload_exploration_cursor"]
    ))
        throw new InvalidArgumentException("Greedy policy state is invalid.");

    $now = normalise_peer_time($now);
    $evaluation_due = $now - $policy_state["last_evaluation_at"] >= OPTIMISER_INTERVAL;
    $refresh_due = $evaluation_due
        || $policy_state["latest_result"] === null
        || $now - $policy_state["last_refresh_at"] >= GREEDY_POLICY_REFRESH_INTERVAL;

    if(!$refresh_due) {
        $result = $policy_state["latest_result"];
        $result["evaluation_due"] = false;

        return $result;
    }

    $policy_state["last_refresh_at"] = $now;
    $download_candidates = runtime_greedy_download_candidates(
        $connections,
        $piece_priority,
        $piece_priority_cache,
        $piece_manager
    );
    $upload_candidates = runtime_greedy_upload_candidates($connections);
    $download_selection = select_runtime_greedy_connections(
        $download_candidates,
        DESIRED_DOWNLOAD_PEERS,
        $policy_state["download_exploration_cursor"]
    );
    $upload_selection = select_runtime_greedy_upload_connections(
        $upload_candidates,
        DESIRED_UPLOAD_PEERS,
        $policy_state["upload_exploration_cursor"]
    );

    if($evaluation_due) {
        $policy_state["last_evaluation_at"] = $now;
        $policy_state["download_exploration_cursor"] += max(
            1,
            $download_selection["explore_slot_count"]
        );
        $policy_state["upload_exploration_cursor"] += max(
            1,
            $upload_selection["explore_slot_count"]
        );
    }

    foreach($connections as $connection) {
        if($connection->is_terminal() || $connection->state !== PeerConnection::STATE_ESTABLISHED)
            continue;

        $peer_key = $connection->peer->endpoint->key;
        $is_explore = isset($download_selection["explore"][$peer_key])
            || isset($upload_selection["explore"][$peer_key]);
        $is_exploit = isset($download_selection["exploit"][$peer_key])
            || isset($upload_selection["exploit"][$peer_key]);

        if($is_explore)
            $connection->peer->policy_state = "GREEDY_EXPLORE";
        elseif($is_exploit)
            $connection->peer->policy_state = "GREEDY_EXPLOIT";
        else
            $connection->peer->policy_state = "GREEDY_IDLE";

        $should_unchoke = isset($upload_selection["selected"][$peer_key]);

        if(!$should_unchoke) {
            $connection->peer->greedy_allocator_rate = 0.0;
            $connection->peer->greedy_allocator_role = "IDLE";
        }

        if($connection->local_choking === $should_unchoke)
            $connection->set_local_choking(!$should_unchoke);
    }

    $result = [
        "evaluation_due" => $evaluation_due,
        "download" => $download_selection,
        "upload" => $upload_selection,
        "selected_download_peer_keys" => array_fill_keys(
            array_keys($download_selection["selected"]),
            true
        ),
        "upload_slot_count" => count($upload_selection["selected"]),
    ];
    $policy_state["latest_result"] = $result;

    return $result;
}

function log_runtime_greedy_policy_decision($result, $log_stream) {
    if(!is_array($result) || !isset($result["download"], $result["upload"]))
        throw new InvalidArgumentException("Greedy policy log requires a policy result.");

    $download = $result["download"];
    $upload = $result["upload"];
    $top_download = [];

    foreach(array_slice(array_values($download["exploit"]), 0, 3) as $connection) {
        $top_download[] = sprintf(
            "%s %.1f KiB/s",
            $connection->peer->endpoint->key,
            runtime_greedy_peer_value($connection->peer) / 1024
        );
    }

    log_message(
        sprintf(
            "Greedy baseline: download %d/%d candidates (%d exploit, %d explore; target %d), upload %d/%d candidates (%d exploit, %d explore; target %d)%s.",
            count($download["selected"]),
            $download["candidate_count"],
            $download["exploit_slot_count"],
            $download["explore_slot_count"],
            DESIRED_DOWNLOAD_PEERS,
            count($upload["selected"]),
            $upload["candidate_count"],
            $upload["exploit_slot_count"],
            $upload["explore_slot_count"],
            DESIRED_UPLOAD_PEERS,
            $top_download === []
                ? ""
                : "; top useful peers " . implode(", ", $top_download)
        ),
        $log_stream
    );
}

function service_runtime_upload_requests(
    $connections,
    &$upload_limiter,
    $upload_slot_count,
    $now = null,
    $peer_rates = null
) {
    if(!is_int($upload_slot_count) || $upload_slot_count < 0)
        throw new InvalidArgumentException("Runtime upload slot count must be non-negative.");

    if($peer_rates !== null && !is_array($peer_rates))
        throw new InvalidArgumentException("Runtime upload peer rates must be null or an array.");

    if($upload_slot_count === 0)
        return 0;

    $now = normalise_peer_time($now);
    $served_requests = 0;
    $equal_peer_rate = UPLOAD_LIMIT_BYTES_PER_SECOND / $upload_slot_count;

    foreach($connections as $connection) {
        if(
            $connection->is_terminal()
            || $connection->state !== PeerConnection::STATE_ESTABLISHED
            || $connection->local_choking
        )
            continue;

        $request = $connection->peek_pending_upload_request();

        if($request === null)
            continue;

        $peer_key = $connection->peer->endpoint->key;
        $peer_rate = $peer_rates === null
            ? $equal_peer_rate
            : floatval($peer_rates[$peer_key] ?? 0.0);

        if($peer_rate <= 0.0)
            continue;

        if(!runtime_upload_limiter_consume(
            $upload_limiter,
            $peer_key,
            $request["length"],
            $peer_rate,
            $now
        ))
            continue;

        if($connection->serve_next_upload_request() !== null)
            $served_requests++;
    }

    return $served_requests;
}


function create_runtime_connection_optimizer_state($now = null) {
    $now = normalise_peer_time($now);

    return [
        "last_evaluation_at" => $now - OPTIMISER_INTERVAL,
        "evaluation_count" => 0,
        "replacement_count" => 0,
        "exploration_replacement_count" => 0,
        "fresh_turnover_mark_count" => 0,
        "fresh_turnover_completion_count" => 0,
        "latest" => [],
    ];
}

function runtime_connection_optimizer_peer_score($peer) {
    if(!($peer instanceof Peer))
        throw new InvalidArgumentException("Connection optimiser score requires a peer.");

    return runtime_greedy_peer_value($peer);
}

function rank_runtime_connection_candidates($peers, $now = null) {
    if(!is_array($peers))
        throw new InvalidArgumentException("Connection candidate ranking requires a peer list.");

    $now = normalise_peer_time($now);
    $ranked = array_values($peers);

    foreach($ranked as $peer) {
        if(!($peer instanceof Peer))
            throw new InvalidArgumentException("Connection candidate ranking requires Peer objects.");
    }

    usort(
        $ranked,
        static function($left, $right) use ($now) {
            $left_priority = $left->connection_optimizer_priority_until > $now;
            $right_priority = $right->connection_optimizer_priority_until > $now;

            if($left_priority !== $right_priority)
                return $left_priority ? -1 : 1;

            $left_score = runtime_connection_optimizer_peer_score($left);
            $right_score = runtime_connection_optimizer_peer_score($right);
            $left_has_history = $left->performance_sample_count > 0 && $left_score > 0.0;
            $right_has_history = $right->performance_sample_count > 0 && $right_score > 0.0;

            if($left_has_history !== $right_has_history)
                return $left_has_history ? -1 : 1;

            if($left_has_history && abs($left_score - $right_score) > 0.000001)
                return $left_score > $right_score ? -1 : 1;

            $left_untested = $left->connection_attempts === 0;
            $right_untested = $right->connection_attempts === 0;

            if($left_untested !== $right_untested)
                return $left_untested ? -1 : 1;

            if($left->consecutive_failures !== $right->consecutive_failures)
                return $left->consecutive_failures <=> $right->consecutive_failures;

            $left_sources = count($left->sources);
            $right_sources = count($right->sources);

            if($left_sources !== $right_sources)
                return $right_sources <=> $left_sources;

            if(abs($left->last_discovered_at - $right->last_discovered_at) > 0.000001)
                return $left->last_discovered_at > $right->last_discovered_at ? -1 : 1;

            return strcmp($left->endpoint->key, $right->endpoint->key);
        }
    );

    return $ranked;
}

function runtime_connection_optimizer_compare_active_connections($left, $right) {
    if(!($left instanceof PeerConnection) || !($right instanceof PeerConnection))
        throw new InvalidArgumentException("Connection optimiser active ranking requires peer connections.");

    $left_score = runtime_connection_optimizer_peer_score($left->peer);
    $right_score = runtime_connection_optimizer_peer_score($right->peer);

    if(abs($left_score - $right_score) > 0.000001)
        return $left_score < $right_score ? -1 : 1;

    $left_reliability = $left->peer->reliability_long ?? -1.0;
    $right_reliability = $right->peer->reliability_long ?? -1.0;

    if(abs($left_reliability - $right_reliability) > 0.000001)
        return $left_reliability < $right_reliability ? -1 : 1;

    $left_latency = $left->peer->request_latency_long ?? INF;
    $right_latency = $right->peer->request_latency_long ?? INF;

    if($left_latency !== $right_latency)
        return $left_latency > $right_latency ? -1 : 1;

    return strcmp($left->peer->endpoint->key, $right->peer->endpoint->key);
}

function runtime_connection_optimizer_connection_is_replaceable(
    $connection,
    $selected_download_peer_keys,
    $selected_upload_peer_keys,
    $now
) {
    if(!($connection instanceof PeerConnection))
        throw new InvalidArgumentException("Connection optimiser replacement check requires a peer connection.");

    $now = normalise_peer_time($now);

    if(
        $connection->is_terminal()
        || $connection->state !== PeerConnection::STATE_ESTABLISHED
        || $connection->handshake_completed_at === null
        || $now - $connection->handshake_completed_at < CONNECTION_OPTIMISER_MIN_CONNECTION_AGE
    )
        return false;

    $peer_key = $connection->peer->endpoint->key;

    if(isset($selected_download_peer_keys[$peer_key]) || isset($selected_upload_peer_keys[$peer_key]))
        return false;

    if($connection->get_outstanding_block_requests() !== [] || $connection->pending_upload_requests !== [])
        return false;

    return true;
}

function runtime_connection_optimizer_history_candidate_is_better($candidate, $active_connection) {
    if(!($candidate instanceof Peer) || !($active_connection instanceof PeerConnection))
        throw new InvalidArgumentException("Connection optimiser comparison requires a candidate peer and active connection.");

    $candidate_score = runtime_connection_optimizer_peer_score($candidate);
    $active_score = runtime_connection_optimizer_peer_score($active_connection->peer);

    if($candidate->performance_sample_count <= 0 || $candidate_score <= 0.0)
        return false;

    $required_score = max(
        $active_score * (1.0 + CONNECTION_OPTIMISER_HYSTERESIS_RATIO),
        $active_score + CONNECTION_OPTIMISER_MIN_RATE_GAIN
    );

    return $candidate_score >= $required_score;
}

function runtime_connection_optimizer_active_is_poor_for_exploration($connection, $now) {
    if(!($connection instanceof PeerConnection))
        throw new InvalidArgumentException("Connection optimiser exploration check requires a peer connection.");

    $now = normalise_peer_time($now);
    $score = runtime_connection_optimizer_peer_score($connection->peer);

    if($score > CONNECTION_OPTIMISER_POOR_RATE_THRESHOLD)
        return false;

    if(
        $connection->peer->last_piece_received_at !== null
        && $now - $connection->peer->last_piece_received_at < CONNECTION_OPTIMISER_MIN_CONNECTION_AGE
    )
        return false;

    return true;
}

function collect_runtime_fresh_service_statistics($connections, $now = null, $peer_pool = null) {
    if(!is_array($connections))
        throw new InvalidArgumentException("Fresh-service statistics require a connection list.");

    if($peer_pool !== null && !($peer_pool instanceof PeerPool))
        throw new InvalidArgumentException("Fresh-service statistics received an invalid peer pool.");

    $now = normalise_peer_time($now);
    $statistics = [
        "established_connections" => 0,
        "matured_connections" => 0,
        "fresh_unchokes" => 0,
        "fresh_useful_bytes" => 0,
        "seed_fresh_unchokes" => 0,
        "leecher_fresh_unchokes" => 0,
        "retiring_connections" => 0,
    ];

    if($peer_pool !== null) {
        foreach($peer_pool->get_peers() as $peer) {
            $statistics["established_connections"] += $peer->fresh_service_connection_count;
            $statistics["fresh_unchokes"] += $peer->fresh_service_unchoke_count;
            $statistics["fresh_useful_bytes"] += $peer->fresh_service_useful_bytes;

            if($peer->is_seed)
                $statistics["seed_fresh_unchokes"] += $peer->fresh_service_unchoke_count;
            else
                $statistics["leecher_fresh_unchokes"] += $peer->fresh_service_unchoke_count;
        }

        $currently_fresh = 0;

        foreach($connections as $connection) {
            if(!($connection instanceof PeerConnection))
                continue;

            if(
                !$connection->is_terminal()
                && $connection->handshake_completed_at !== null
                && $now - $connection->handshake_completed_at < FRESH_SERVICE_WINDOW
            )
                $currently_fresh++;

            if($connection->fresh_turnover_retiring && !$connection->is_terminal())
                $statistics["retiring_connections"]++;
        }

        $statistics["matured_connections"] = max(
            0,
            $statistics["established_connections"] - $currently_fresh
        );

        return $statistics;
    }

    foreach($connections as $connection) {
        if(!($connection instanceof PeerConnection))
            continue;

        if($connection->handshake_completed_at !== null) {
            $statistics["established_connections"]++;

            if(
                $connection->is_terminal()
                || $now - $connection->handshake_completed_at >= FRESH_SERVICE_WINDOW
            )
                $statistics["matured_connections"]++;
        }

        if($connection->fresh_service_unchoke_recorded) {
            $statistics["fresh_unchokes"]++;

            if($connection->peer->is_seed)
                $statistics["seed_fresh_unchokes"]++;
            else
                $statistics["leecher_fresh_unchokes"]++;
        }

        $statistics["fresh_useful_bytes"] += $connection->fresh_service_useful_bytes;

        if($connection->fresh_turnover_retiring && !$connection->is_terminal())
            $statistics["retiring_connections"]++;
    }

    return $statistics;
}

function runtime_connection_optimizer_fresh_turnover_candidate_is_eligible(
    $connection,
    $selected_upload_peer_keys,
    $now
) {
    if(!($connection instanceof PeerConnection))
        throw new InvalidArgumentException("Fresh-service turnover requires a peer connection.");

    if(!is_array($selected_upload_peer_keys))
        throw new InvalidArgumentException("Fresh-service turnover upload selection must be a peer map.");

    $now = normalise_peer_time($now);

    if(
        $connection->is_terminal()
        || $connection->state !== PeerConnection::STATE_ESTABLISHED
        || $connection->fresh_turnover_retiring
        || $connection->handshake_completed_at === null
        || $now - $connection->handshake_completed_at < FRESH_SERVICE_TURNOVER_MIN_AGE
        || $connection->peer->policy_state !== "GREEDY_EXPLORE"
        || runtime_connection_optimizer_peer_score($connection->peer) > FRESH_SERVICE_TURNOVER_MAX_PEER_VALUE
        || isset($selected_upload_peer_keys[$connection->peer->endpoint->key])
        || $connection->pending_upload_requests !== []
    )
        return false;

    return true;
}

function runtime_connection_optimizer_compare_fresh_turnover_connections($left, $right) {
    if(!($left instanceof PeerConnection) || !($right instanceof PeerConnection))
        throw new InvalidArgumentException("Fresh-service turnover ranking requires peer connections.");

    $left_score = runtime_connection_optimizer_peer_score($left->peer);
    $right_score = runtime_connection_optimizer_peer_score($right->peer);

    if(abs($left_score - $right_score) > 0.000001)
        return $left_score < $right_score ? -1 : 1;

    $left_bytes = $left->fresh_service_useful_bytes;
    $right_bytes = $right->fresh_service_useful_bytes;

    if($left_bytes !== $right_bytes)
        return $left_bytes <=> $right_bytes;

    $left_age = $left->handshake_completed_at ?? INF;
    $right_age = $right->handshake_completed_at ?? INF;

    if($left_age !== $right_age)
        return $left_age < $right_age ? -1 : 1;

    return strcmp($left->peer->endpoint->key, $right->peer->endpoint->key);
}

function complete_runtime_fresh_turnover_retirements(
    $connections,
    &$peer_retry_after,
    &$optimizer_state,
    $now = null
) {
    if(!is_array($connections) || !is_array($peer_retry_after) || !is_array($optimizer_state))
        throw new InvalidArgumentException("Fresh-service turnover completion state is invalid.");

    if(!isset(
        $optimizer_state["replacement_count"],
        $optimizer_state["exploration_replacement_count"],
        $optimizer_state["fresh_turnover_completion_count"]
    ))
        throw new InvalidArgumentException("Fresh-service turnover optimiser state is invalid.");

    $now = normalise_peer_time($now);
    $completed = [];

    foreach($connections as $connection) {
        if(!($connection instanceof PeerConnection) || !$connection->fresh_turnover_retiring)
            continue;

        if($connection->is_terminal()) {
            $connection->fresh_turnover_retiring = false;
            $connection->fresh_turnover_candidate_peer_key = null;

            continue;
        }

        if(
            $connection->state !== PeerConnection::STATE_ESTABLISHED
            || $connection->get_outstanding_block_requests() !== []
            || $connection->pending_upload_requests !== []
        )
            continue;

        $peer = $connection->peer;
        $peer_key = $peer->endpoint->key;
        $candidate_key = $connection->fresh_turnover_candidate_peer_key;
        $score = runtime_connection_optimizer_peer_score($peer);
        $connection->fresh_turnover_retiring = false;
        $connection->fresh_turnover_candidate_peer_key = null;
        $peer->fresh_turnover_count++;
        $peer->last_fresh_turnover_at = $now;
        $peer->connection_optimizer_replacements++;
        $peer->last_connection_optimizer_replacement_at = $now;
        close_runtime_piece_connection($connection, $peer_retry_after, $now);
        $optimizer_state["replacement_count"]++;
        $optimizer_state["exploration_replacement_count"]++;
        $optimizer_state["fresh_turnover_completion_count"]++;
        $completed[] = [
            "closed_peer_key" => $peer_key,
            "candidate_peer_key" => $candidate_key,
            "closed_score" => $score,
            "cooldown_until" => $peer_retry_after[$peer_key],
        ];
    }

    return $completed;
}

function log_runtime_fresh_turnover_completions($completed, $log_stream) {
    if(!is_array($completed))
        throw new InvalidArgumentException("Fresh-service turnover completion log requires an array.");

    foreach($completed as $decision) {
        log_message(
            sprintf(
                "Fresh-service turnover closed %s after drain; candidate %s was prioritised; displaced value %.2f KiB/s and cooldown %ds.",
                $decision["closed_peer_key"],
                $decision["candidate_peer_key"] ?? "n/a",
                $decision["closed_score"] / 1024,
                PEER_COOLDOWN
            ),
            $log_stream
        );
    }
}

function optimise_runtime_connections(
    $connections,
    $peer_pool,
    &$peer_retry_after,
    $selected_download_peer_keys,
    $selected_upload_peer_keys,
    &$optimizer_state,
    $now = null,
    $connection_limit = DESIRED_CONNECTED_PEERS
) {
    if(!is_array($connections))
        throw new InvalidArgumentException("Connection optimiser requires a connection list.");

    if(!($peer_pool instanceof PeerPool))
        throw new InvalidArgumentException("Connection optimiser requires a peer pool.");

    if(!is_array($peer_retry_after) || !is_array($selected_download_peer_keys) || !is_array($selected_upload_peer_keys))
        throw new InvalidArgumentException("Connection optimiser peer maps are invalid.");

    if(!is_array($optimizer_state) || !isset(
        $optimizer_state["last_evaluation_at"],
        $optimizer_state["evaluation_count"],
        $optimizer_state["replacement_count"],
        $optimizer_state["exploration_replacement_count"],
        $optimizer_state["fresh_turnover_mark_count"],
        $optimizer_state["fresh_turnover_completion_count"],
        $optimizer_state["latest"]
    ))
        throw new InvalidArgumentException("Connection optimiser state is invalid.");

    if(!is_int($connection_limit) || $connection_limit < 1)
        throw new InvalidArgumentException("Connection optimiser connection limit must be positive.");

    $now = normalise_peer_time($now);
    $evaluation_due = $now - $optimizer_state["last_evaluation_at"] >= OPTIMISER_INTERVAL;

    if(!$evaluation_due && $optimizer_state["latest"] !== []) {
        $result = $optimizer_state["latest"];
        $result["evaluation_due"] = false;
        $result["replacement_count"] = 0;
        $result["history_replacement_count"] = 0;
        $result["exploration_replacement_count"] = 0;
        $result["decisions"] = [];

        return $result;
    }

    $active_connection_count = count(array_filter(
        $connections,
        static fn($connection) => $connection instanceof PeerConnection && !$connection->is_terminal()
    ));
    $supplier_state = runtime_supplier_market_state($peer_pool, $now);
    $available_candidates = [];

    foreach($peer_pool->get_available_peers($now) as $candidate) {
        $peer_key = $candidate->endpoint->key;

        if(isset($peer_retry_after[$peer_key]) && $now < $peer_retry_after[$peer_key])
            continue;

        $available_candidates[] = $candidate;
    }

    $available_candidates = rank_runtime_connection_candidates($available_candidates, $now);
    $result = [
        "evaluation_due" => $evaluation_due,
        "active_connection_count" => $active_connection_count,
        "available_candidate_count" => count($available_candidates),
        "supplier_state" => $supplier_state,
        "replaceable_connection_count" => 0,
        "replacement_limit" => 0,
        "replacement_count" => 0,
        "history_replacement_count" => 0,
        "exploration_replacement_count" => 0,
        "fresh_turnover_marked_count" => 0,
        "fresh_turnover_retiring_count" => 0,
        "fresh_turnover_decisions" => [],
        "fresh_service" => collect_runtime_fresh_service_statistics($connections, $now, $peer_pool),
        "decisions" => [],
    ];

    if(!$evaluation_due)
        return $result;

    $optimizer_state["last_evaluation_at"] = $now;
    $optimizer_state["evaluation_count"]++;

    if($active_connection_count < $connection_limit) {
        $optimizer_state["latest"] = $result;

        return $result;
    }

    if($available_candidates === []) {
        $optimizer_state["latest"] = $result;

        return $result;
    }

    $replaceable_connections = [];

    foreach($connections as $connection) {
        if(!($connection instanceof PeerConnection))
            continue;

        if(runtime_connection_optimizer_connection_is_replaceable(
            $connection,
            $selected_download_peer_keys,
            $selected_upload_peer_keys,
            $now
        ))
            $replaceable_connections[] = $connection;
    }

    usort($replaceable_connections, "runtime_connection_optimizer_compare_active_connections");
    $result["replaceable_connection_count"] = count($replaceable_connections);
    $used_candidate_keys = [];
    $replacement_limit = CONNECTION_OPTIMISER_MAX_REPLACEMENTS_PER_INTERVAL;

    if(
        count($available_candidates) >= CONNECTION_OPTIMISER_DYNAMIC_MARKET_THRESHOLD
        && count($replaceable_connections) >= CONNECTION_OPTIMISER_DYNAMIC_REPLACEABLE_THRESHOLD
    ) {
        $replacement_limit = min(
            CONNECTION_OPTIMISER_DYNAMIC_MAX_REPLACEMENTS_PER_INTERVAL,
            max(
                CONNECTION_OPTIMISER_MAX_REPLACEMENTS_PER_INTERVAL,
                (int)ceil(
                    count($replaceable_connections)
                    / CONNECTION_OPTIMISER_DYNAMIC_REPLACEABLE_THRESHOLD
                )
            )
        );
    }

    $max_replacements = min($replacement_limit, count($replaceable_connections));
    $result["replacement_limit"] = $max_replacements;
    $max_exploration_replacements = $max_replacements > 0 && EXPLORE_RATIO > 0.0
        ? min(
            $max_replacements,
            max(1, (int)ceil($max_replacements * EXPLORE_RATIO))
        )
        : 0;

    foreach($replaceable_connections as $active_connection) {
        if($result["replacement_count"] >= $max_replacements)
            break;

        $active_score = runtime_connection_optimizer_peer_score($active_connection->peer);
        $replacement_candidate = null;
        $replacement_reason = null;

        foreach($available_candidates as $candidate) {
            $candidate_key = $candidate->endpoint->key;

            if(isset($used_candidate_keys[$candidate_key]))
                continue;

            if(runtime_connection_optimizer_history_candidate_is_better($candidate, $active_connection)) {
                $replacement_candidate = $candidate;
                $replacement_reason = "HISTORY";

                break;
            }
        }

        if(
            $replacement_candidate === null
            && $result["exploration_replacement_count"] < $max_exploration_replacements
            && runtime_connection_optimizer_active_is_poor_for_exploration($active_connection, $now)
        ) {
            foreach($available_candidates as $candidate) {
                $candidate_key = $candidate->endpoint->key;

                if(isset($used_candidate_keys[$candidate_key]))
                    continue;

                if($candidate->performance_sample_count === 0 && $candidate->connection_attempts === 0) {
                    $replacement_candidate = $candidate;
                    $replacement_reason = "EXPLORE";

                    break;
                }
            }
        }

        if($replacement_candidate === null)
            continue;

        $candidate_key = $replacement_candidate->endpoint->key;
        $active_key = $active_connection->peer->endpoint->key;
        $candidate_score = runtime_connection_optimizer_peer_score($replacement_candidate);
        $replacement_candidate->connection_optimizer_priority_until = max(
            $replacement_candidate->connection_optimizer_priority_until,
            $now + OPTIMISER_INTERVAL
        );
        $active_connection->peer->connection_optimizer_replacements++;
        $active_connection->peer->last_connection_optimizer_replacement_at = $now;
        close_runtime_piece_connection($active_connection, $peer_retry_after, $now);
        $used_candidate_keys[$candidate_key] = true;
        $result["replacement_count"]++;

        if($replacement_reason === "HISTORY")
            $result["history_replacement_count"]++;
        else
            $result["exploration_replacement_count"]++;

        $result["decisions"][] = [
            "closed_peer_key" => $active_key,
            "candidate_peer_key" => $candidate_key,
            "reason" => $replacement_reason,
            "closed_score" => $active_score,
            "candidate_score" => $candidate_score,
            "cooldown_until" => $peer_retry_after[$active_key],
        ];
    }

    $untested_candidates = array_values(array_filter(
        $available_candidates,
        static fn($candidate) => $candidate instanceof Peer && $candidate->connection_attempts === 0
    ));
    $fresh_turnover_connections = [];

    $minimum_warm_established = min(
        MINIMUM_ESTABLISHED_DOWNLOAD_PEERS,
        max(0, $connection_limit - 1)
    );

    if(
        $untested_candidates !== []
        && FRESH_SERVICE_TURNOVER_MAX_PER_INTERVAL > 0
        && $supplier_state["established_peers"] > $minimum_warm_established
    ) {
        foreach($connections as $connection) {
            if(
                $connection instanceof PeerConnection
                && runtime_connection_optimizer_fresh_turnover_candidate_is_eligible(
                    $connection,
                    $selected_upload_peer_keys,
                    $now
                )
            )
                $fresh_turnover_connections[] = $connection;
        }

        usort(
            $fresh_turnover_connections,
            "runtime_connection_optimizer_compare_fresh_turnover_connections"
        );
        $turnover_limit = min(
            FRESH_SERVICE_TURNOVER_MAX_PER_INTERVAL,
            count($fresh_turnover_connections),
            count($untested_candidates)
        );
        $candidate_cursor = 0;

        for($index = 0; $index < $turnover_limit; $index++) {
            while(
                $candidate_cursor < count($untested_candidates)
                && isset($used_candidate_keys[$untested_candidates[$candidate_cursor]->endpoint->key])
            )
                $candidate_cursor++;

            if($candidate_cursor >= count($untested_candidates))
                break;

            $connection = $fresh_turnover_connections[$index];
            $candidate = $untested_candidates[$candidate_cursor++];
            $candidate_key = $candidate->endpoint->key;
            $candidate->connection_optimizer_priority_until = max(
                $candidate->connection_optimizer_priority_until,
                $now + FRESH_SERVICE_CANDIDATE_PRIORITY_SECONDS
            );
            $connection->fresh_turnover_retiring = true;
            $connection->fresh_turnover_retire_marked_at = $now;
            $connection->fresh_turnover_candidate_peer_key = $candidate_key;
            $used_candidate_keys[$candidate_key] = true;
            $result["fresh_turnover_marked_count"]++;
            $result["fresh_turnover_decisions"][] = [
                "retiring_peer_key" => $connection->peer->endpoint->key,
                "candidate_peer_key" => $candidate_key,
                "retiring_score" => runtime_connection_optimizer_peer_score($connection->peer),
                "outstanding_requests" => $connection->get_outstanding_block_request_count(),
            ];
        }
    }

    $result["fresh_turnover_retiring_count"] = count(array_filter(
        $connections,
        static fn($connection) => $connection instanceof PeerConnection
            && $connection->fresh_turnover_retiring
            && !$connection->is_terminal()
    ));
    $optimizer_state["fresh_turnover_mark_count"] += $result["fresh_turnover_marked_count"];

    $optimizer_state["replacement_count"] += $result["replacement_count"];
    $optimizer_state["exploration_replacement_count"] += $result["exploration_replacement_count"];
    $optimizer_state["latest"] = $result;

    return $result;
}

function log_runtime_connection_optimizer($result, $log_stream) {
    if(!is_array($result) || !isset(
        $result["active_connection_count"],
        $result["available_candidate_count"],
        $result["replaceable_connection_count"],
        $result["replacement_count"],
        $result["fresh_turnover_marked_count"],
        $result["fresh_turnover_retiring_count"],
        $result["fresh_turnover_decisions"],
        $result["fresh_service"],
        $result["supplier_state"],
        $result["decisions"]
    ))
        throw new InvalidArgumentException("Connection optimiser log result is invalid.");

    log_message(
        sprintf(
            "Connection optimiser: %d active, %d available candidates, %d replaceable; %d immediate replacement%s; fresh-service turnover marked %d, retiring %d; suppliers %d established/%d unchoked/%d recently useful, %d cooling; early unreciprocated service %d/%d established connections, %.2f MiB useful.",
            $result["active_connection_count"],
            $result["available_candidate_count"],
            $result["replaceable_connection_count"],
            $result["replacement_count"],
            $result["replacement_count"] === 1 ? "" : "s",
            $result["fresh_turnover_marked_count"],
            $result["fresh_turnover_retiring_count"],
            $result["supplier_state"]["established_peers"],
            $result["supplier_state"]["unchoked_peers"],
            $result["supplier_state"]["useful_peers"],
            $result["supplier_state"]["cooldown_peers"],
            $result["fresh_service"]["fresh_unchokes"],
            $result["fresh_service"]["established_connections"],
            $result["fresh_service"]["fresh_useful_bytes"] / 1048576
        ),
        $log_stream
    );

    foreach($result["fresh_turnover_decisions"] as $decision) {
        log_message(
            sprintf(
                "Fresh-service turnover marked %s -> %s: exploration peer value %.2f KiB/s, %d outstanding request%s draining before close.",
                $decision["retiring_peer_key"],
                $decision["candidate_peer_key"],
                $decision["retiring_score"] / 1024,
                $decision["outstanding_requests"],
                $decision["outstanding_requests"] === 1 ? "" : "s"
            ),
            $log_stream
        );
    }

    foreach($result["decisions"] as $decision) {
        log_message(
            sprintf(
                "Connection replacement %s -> %s (%s): useful value %.2f -> %.2f KiB/s; displaced peer cooling down for %ds.",
                $decision["closed_peer_key"],
                $decision["candidate_peer_key"],
                strtolower($decision["reason"]),
                $decision["closed_score"] / 1024,
                $decision["candidate_score"] / 1024,
                PEER_COOLDOWN
            ),
            $log_stream
        );
    }
}

function announce_runtime_verified_piece($connections, $piece_index) {
    foreach($connections as $connection) {
        if($connection->is_terminal() || $connection->state !== PeerConnection::STATE_ESTABLISHED)
            continue;

        try {
            $connection->announce_local_piece($piece_index);
        } catch(Throwable) {
            continue;
        }
    }
}

function runtime_block_request_has_timed_out($connection, $now) {
    foreach($connection->get_outstanding_block_requests() as $request) {
        if($now - $request["requested_at"] >= BASIC_DOWNLOAD_REQUEST_TIMEOUT)
            return true;
    }

    return false;
}

function download_runtime_pieces(
    $peer_pool,
    $magnet,
    $local_peer_id,
    $metadata_exchange,
    $piece_manager,
    $log_stream,
    $tracker_discovery = null,
    $connector = null,
    $verified_piece_target = null,
    $dht_discovery = null,
    $peer_listener = null,
    $port_mapping = null,
    $progress_stream = null,
    $research_policy_mode = null
) {
    if(!($peer_pool instanceof PeerPool))
        throw new InvalidArgumentException("Basic piece downloading requires a peer pool.");

    if(!($magnet instanceof MagnetUri))
        throw new InvalidArgumentException("Basic piece downloading requires a parsed magnet URI.");

    if(!is_string($local_peer_id) || strlen($local_peer_id) !== 20)
        throw new InvalidArgumentException("Basic piece downloading requires a 20-byte peer ID.");

    if(!($metadata_exchange instanceof MetadataExchange) || !$metadata_exchange->is_complete())
        throw new InvalidArgumentException("Basic piece downloading requires complete metadata.");

    if(!($piece_manager instanceof PieceManager))
        throw new InvalidArgumentException("Basic piece downloading requires a piece manager.");

    if($tracker_discovery !== null && !($tracker_discovery instanceof RuntimeTrackerDiscovery))
        throw new InvalidArgumentException("Basic piece downloading received an invalid tracker discovery object.");

    if($dht_discovery !== null && !($dht_discovery instanceof RuntimeDhtDiscovery))
        throw new InvalidArgumentException("Basic piece downloading received an invalid DHT discovery object.");

    if($peer_listener !== null && !($peer_listener instanceof RuntimePeerListener))
        throw new InvalidArgumentException("Basic piece downloading received an invalid inbound peer listener.");

    if($port_mapping !== null && !($port_mapping instanceof RuntimePortMapping))
        throw new InvalidArgumentException("Basic piece downloading received an invalid router port mapping.");

    if($progress_stream !== null && !is_resource($progress_stream))
        throw new InvalidArgumentException("Basic piece downloading progress output requires an open stream or null.");

    if($connector !== null && !is_callable($connector))
        throw new InvalidArgumentException("Basic piece downloader connector must be callable.");

    if($verified_piece_target !== null && (!is_int($verified_piece_target) || $verified_piece_target < 1))
        throw new InvalidArgumentException("Verified piece target must be null or a positive integer.");

    $connections = [];
    $attempted_peer_keys = [];
    $peer_retry_after = [];
    $logged_active_pieces = [];
    $logged_verified_pieces = [];
    $downloaded_bytes = 0;
    $initial_verified_piece_count = $piece_manager->verified_piece_count;
    $initial_verified_piece_indexes = $piece_manager->get_verified_piece_indexes();
    $initial_verified_piece_lookup = array_fill_keys($initial_verified_piece_indexes, true);
    $target_verified_piece_count = $verified_piece_target === null
        ? $piece_manager->piece_count
        : min(
            $piece_manager->piece_count,
            $initial_verified_piece_count + $verified_piece_target
        );
    $last_status_at = microtime(true);
    $download_started_at = $last_status_at;
    $last_status_downloaded_bytes = 0;
    $last_status_uploaded_bytes = 0;
    $initial_verified_byte_count = $piece_manager->get_verified_byte_count();
    $last_status_verified_bytes = $initial_verified_byte_count;
    $progress_rate_bytes_per_second = 0.0;
    $progress_updates = 0;
    $upload_limiter = create_runtime_upload_limiter($last_status_at);
    $research_policy_mode = normalise_runtime_research_policy($research_policy_mode);
    $standard_choking_state = create_runtime_standard_choking_state($last_status_at);
    $greedy_policy_state = create_runtime_greedy_policy_state($last_status_at);
    $greedy_price_state = create_runtime_greedy_price_state($last_status_at);
    $greedy_marginal_return_state = create_runtime_greedy_marginal_return_state($last_status_at);
    $greedy_upload_allocator_state = create_runtime_greedy_upload_allocator_state($last_status_at);
    $connection_optimizer_state = create_runtime_connection_optimizer_state($last_status_at);
    $endgame_state = create_runtime_endgame_state();
    $completion_shutdown_state = create_runtime_completion_shutdown_state();
    $peer_metrics_state = create_runtime_peer_metrics_state($last_status_at);
    $piece_priority_cache = new RuntimePiecePriorityCache($piece_manager);
    $runtime_profiler = new RuntimeProfiler($last_status_at);
    $last_empty_terminal_prune_at = $last_status_at;
    $last_pex_service_at = $last_status_at - PEX_SERVICE_CHECK_INTERVAL;
    $last_tracker_transfer_counter_at = $last_status_at - TRACKER_TRANSFER_COUNTER_REFRESH_INTERVAL;

    if($verified_piece_target === null) {
        log_message(
            sprintf(
                "Downloading with active policy %s; discovery, metadata, storage, rarest-first piece selection, adaptive request pipelining, endgame, metrics and completion shutdown are shared; STANDARD uses conventional reciprocity-ranked regular upload slots plus optimistic unchoke and no greedy connection replacement, while GREEDY uses useful-download peer selection, upload-price/marginal-return allocation and connection optimisation; %d/%d pieces already verified; up to %d concurrent peer connections.",
                $research_policy_mode,
                $piece_manager->verified_piece_count,
                $piece_manager->piece_count,
                DESIRED_CONNECTED_PEERS
            ),
            $log_stream
        );
    } else {
        log_message(
            sprintf(
                "Rarest-first downloading: target %d newly verified piece%s; up to %d concurrent peer connections; live discovery remains active.",
                $verified_piece_target,
                $verified_piece_target === 1 ? "" : "s",
                DESIRED_CONNECTED_PEERS
            ),
            $log_stream
        );
    }

    try {
        while($piece_manager->verified_piece_count < $target_verified_piece_count) {
            $runtime_profiler->record_loop_iteration();

            if($tracker_discovery !== null) {
                $tracker_counter_now = microtime(true);

                if($tracker_counter_now - $last_tracker_transfer_counter_at >= TRACKER_TRANSFER_COUNTER_REFRESH_INTERVAL) {
                    $verified_bytes = $piece_manager->get_verified_byte_count();
                    $downloaded_bytes = runtime_downloaded_block_bytes($connections, $peer_pool);
                    $uploaded_bytes = runtime_uploaded_block_bytes($connections, $peer_pool);
                    $tracker_discovery->set_transfer_counters(
                        $downloaded_bytes,
                        $uploaded_bytes,
                        max(0, $piece_manager->metadata->total_length - $verified_bytes)
                    );
                    $last_tracker_transfer_counter_at = $tracker_counter_now;
                }

                $tracker_discovery->poll(0);
            }

            if($dht_discovery !== null)
                $dht_discovery->poll(0);

            if($port_mapping !== null)
                $port_mapping->poll();

            accept_runtime_inbound_peer_connections(
                $connections,
                $peer_listener,
                $peer_pool,
                $magnet->info_hash,
                $local_peer_id,
                $metadata_exchange,
                $piece_manager,
                $dht_discovery?->get_port()
            );

            $turnover_completed = complete_runtime_fresh_turnover_retirements(
                $connections,
                $peer_retry_after,
                $connection_optimizer_state,
                microtime(true)
            );

            if(LOG_OPTIMISER_DECISIONS && $turnover_completed !== [])
                log_runtime_fresh_turnover_completions($turnover_completed, $log_stream);

            fill_runtime_piece_connections_live(
                $connections,
                $peer_pool,
                $attempted_peer_keys,
                $peer_retry_after,
                $metadata_exchange,
                $piece_manager,
                $magnet,
                $local_peer_id,
                $connector,
                $dht_discovery?->get_port(),
                $peer_listener,
                $research_policy_mode
            );

            $now = microtime(true);

            foreach($connections as $connection) {
                if($connection->is_terminal())
                    continue;

                $connection->check_timeout($now);

                if($connection->is_terminal())
                    continue;

                $received_messages = $connection->take_received_messages();
                $received_piece_message_count = 0;

                foreach($received_messages as $message) {
                    $message_type = $message["type"] ?? null;

                    if($message_type === "piece")
                        $received_piece_message_count++;

                    if($message_type === "port" && $dht_discovery !== null) {
                        $dht_discovery->add_peer_node(
                            $connection->peer->endpoint->host,
                            $message["port"],
                            $now
                        );

                        continue;
                    }

                    if(
                        $message_type === "extended"
                        && ($message["extension_name"] ?? null) === UT_PEX_EXTENSION_NAME
                        && isset($message["pex_message"])
                    ) {
                        $pex_result = add_runtime_pex_peers(
                            $peer_pool,
                            $connection,
                            $message["pex_message"],
                            $now
                        );

                        if($pex_result["added"] > 0) {
                            log_message(
                                sprintf(
                                    "PEX from %s: added %d peer%s from %d advertised contact%s; %d known.",
                                    $connection->peer->endpoint->key,
                                    $pex_result["added"],
                                    $pex_result["added"] === 1 ? "" : "s",
                                    $pex_result["advertised"],
                                    $pex_result["advertised"] === 1 ? "" : "s",
                                    $peer_pool->get_count()
                                ),
                                $log_stream
                            );
                        }

                        continue;
                    }

                    if($message_type === "request") {
                        try {
                            $connection->queue_upload_request(
                                $message["piece_index"],
                                $message["begin"],
                                $message["length"]
                            );
                        } catch(InvalidArgumentException|RuntimeException) {
                            close_runtime_piece_connection($connection, $peer_retry_after, $now);

                            break;
                        }

                        continue;
                    }

                    if($message_type === "cancel") {
                        $connection->cancel_upload_request(
                            $message["piece_index"],
                            $message["begin"],
                            $message["length"]
                        );

                        continue;
                    }

                    if($message_type !== "piece")
                        continue;

                    if(
                        in_array($message["piece_result"] ?? null, ["accepted", "verified"], true)
                    ) {
                        $cancelled_copies = cancel_runtime_redundant_block_requests(
                            $connections,
                            $message["piece_index"],
                            $message["begin"],
                            $connection,
                            $endgame_state
                        );

                        if(($message["endgame_duplicate"] ?? false) === true)
                            $endgame_state["duplicate_blocks_won"]++;
                    }

                    if(($message["piece_result"] ?? null) === "verified") {
                        $verified_bytes = $piece_manager->get_verified_byte_count();
                        $progress_percent = calculate_runtime_progress_percent(
                            $verified_bytes,
                            $piece_manager->metadata->total_length,
                            $piece_manager->is_complete()
                        );
                        log_message(
                            sprintf(
                                "Piece %d verified after receiving block at offset %d from %s; %d/%d pieces, %.2f%% complete.",
                                $message["piece_index"],
                                $message["begin"],
                                $connection->peer->endpoint->key,
                                $piece_manager->verified_piece_count,
                                $piece_manager->piece_count,
                                $progress_percent
                            ),
                            $log_stream
                        );
                        $logged_verified_pieces[$message["piece_index"]] = true;

                        foreach($connections as $candidate_connection)
                            $candidate_connection->finalise_piece_download_contribution($message["piece_index"]);

                        announce_runtime_verified_piece($connections, $message["piece_index"]);
                    } elseif(($message["piece_result"] ?? null) === "verification_failed") {
                        foreach($connections as $candidate_connection)
                            $candidate_connection->invalidate_piece_download_contribution($message["piece_index"]);

                        cancel_runtime_piece_block_requests($connections, $message["piece_index"]);

                        log_message(
                            "Piece {$message["piece_index"]} failed SHA-1 verification and was reset for redownload.",
                            $log_stream
                        );
                    }
                }

                $runtime_profiler->record_messages(count($received_messages), $received_piece_message_count);

                if($piece_manager->verified_piece_count >= $target_verified_piece_count)
                    break;

                if(runtime_block_request_has_timed_out($connection, $now)) {
                    $connection->record_block_request_timeouts(
                        $now,
                        BASIC_DOWNLOAD_REQUEST_TIMEOUT
                    );
                    close_runtime_piece_connection($connection, $peer_retry_after, $now);

                    continue;
                }

                if($connection->state !== PeerConnection::STATE_ESTABLISHED)
                    continue;

                if(
                    !$connection->remote_piece_information_received
                    && $connection->handshake_completed_at !== null
                    && $now - $connection->handshake_completed_at >= BASIC_PEER_AVAILABILITY_TIMEOUT
                ) {
                    close_runtime_piece_connection($connection, $peer_retry_after, $now);

                    continue;
                }
            }

            if($now - $last_empty_terminal_prune_at >= TERMINAL_CONNECTION_PRUNE_INTERVAL) {
                $empty_pruned = prune_runtime_empty_terminal_connections($connections);
                $runtime_profiler->record_pruned_connections($empty_pruned, 0);
                $last_empty_terminal_prune_at = $now;
            }

            if($piece_manager->verified_piece_count >= $target_verified_piece_count)
                break;

            if($now - $last_pex_service_at >= PEX_SERVICE_CHECK_INTERVAL) {
                service_runtime_pex($connections, $now);
                $last_pex_service_at = $now;
            }

            $peer_metrics = sample_runtime_peer_metrics(
                $connections,
                $peer_metrics_state,
                $now
            );

            if(LOG_PEER_METRICS && $peer_metrics !== null)
                log_runtime_peer_metrics($peer_metrics, $log_stream);

            if($peer_metrics !== null && $research_policy_mode === RESEARCH_POLICY_GREEDY) {
                $price_observations = update_runtime_greedy_price_estimates(
                    $peer_metrics,
                    $greedy_price_state,
                    $now
                );

                if(LOG_OPTIMISER_DECISIONS && $price_observations !== null)
                    log_runtime_greedy_price_estimates($price_observations, $log_stream);

                if($price_observations !== null) {
                    $marginal_return_records = update_runtime_greedy_marginal_returns(
                        $price_observations,
                        $peer_metrics,
                        $greedy_marginal_return_state,
                        $now
                    );

                    if(LOG_OPTIMISER_DECISIONS && $marginal_return_records !== [])
                        log_runtime_greedy_marginal_returns($marginal_return_records, $log_stream);
                }
            }

            if($peer_metrics !== null) {
                $terminal_pruned = prune_runtime_terminal_connections($connections);
                $runtime_profiler->record_pruned_connections(0, $terminal_pruned);
            }

            $piece_priority = $piece_priority_cache->get_priority(
                $connections,
                $piece_manager,
                $now
            );

            if($research_policy_mode === RESEARCH_POLICY_GREEDY) {
                $greedy_result = update_runtime_greedy_policy(
                    $connections,
                    $piece_priority,
                    $greedy_policy_state,
                    $now,
                    $piece_priority_cache,
                    $piece_manager
                );
                $selected_download_peer_keys = $greedy_result["selected_download_peer_keys"];
                $greedy_upload_allocation = allocate_runtime_greedy_upload_bandwidth(
                    $greedy_result["upload"],
                    $greedy_upload_allocator_state,
                    $now
                );
                $upload_slot_count = $greedy_upload_allocation["active_peer_count"];
                $upload_peer_rates = $greedy_upload_allocation["allocations"];
                $selected_upload_peer_keys = array_fill_keys(
                    array_keys($greedy_result["upload"]["selected"]),
                    true
                );
                $standard_upload_result = null;
            } else {
                $standard_upload_result = update_runtime_standard_upload_slots(
                    $connections,
                    $standard_choking_state,
                    $now
                );
                $selected_download_peer_keys = select_runtime_standard_download_peer_keys(
                    $connections,
                    $piece_manager,
                    $piece_priority_cache
                );
                $upload_slot_count = $standard_upload_result["slot_count"];
                $upload_peer_rates = null;
                $selected_upload_peer_keys = $standard_upload_result["selected_peer_keys"];
                $greedy_result = null;
                $greedy_upload_allocation = null;
            }

            $adaptive_pipeline_statistics = null;
            $inbound_rebalance_result = rebalance_runtime_inbound_connection_overflow(
                $connections,
                $peer_retry_after,
                $selected_download_peer_keys,
                $selected_upload_peer_keys,
                $now
            );
            if(
                LOG_OPTIMISER_DECISIONS
                && $inbound_rebalance_result["closed_outbound"] > 0
            ) {
                log_message(
                    sprintf(
                        "Inbound headroom rebalance: %d connection%s above target; %d recent established inbound peer%s; displaced %d outbound connection%s.",
                        $inbound_rebalance_result["overflow"],
                        $inbound_rebalance_result["overflow"] === 1 ? "" : "s",
                        $inbound_rebalance_result["recent_established_inbound"],
                        $inbound_rebalance_result["recent_established_inbound"] === 1 ? "" : "s",
                        $inbound_rebalance_result["closed_outbound"],
                        $inbound_rebalance_result["closed_outbound"] === 1 ? "" : "s"
                    ),
                    $log_stream
                );
            }

            if($research_policy_mode === RESEARCH_POLICY_GREEDY) {
                $connection_optimizer_result = optimise_runtime_connections(
                    $connections,
                    $peer_pool,
                    $peer_retry_after,
                    $selected_download_peer_keys,
                    $selected_upload_peer_keys,
                    $connection_optimizer_state,
                    $now
                );

                if($connection_optimizer_result["replacement_count"] > 0) {
                    fill_runtime_piece_connections_live(
                        $connections,
                        $peer_pool,
                        $attempted_peer_keys,
                        $peer_retry_after,
                        $metadata_exchange,
                        $piece_manager,
                        $magnet,
                        $local_peer_id,
                        $connector,
                        $dht_discovery?->get_port(),
                        $peer_listener,
                        $research_policy_mode
                    );
                }

                if(LOG_OPTIMISER_DECISIONS && $greedy_result["evaluation_due"]) {
                    $adaptive_pipeline_statistics = collect_runtime_adaptive_request_pipeline_statistics(
                        $connections,
                        $selected_download_peer_keys,
                        $now
                    );
                    log_runtime_greedy_policy_decision($greedy_result, $log_stream);
                    log_runtime_greedy_upload_allocator(
                        $greedy_upload_allocation,
                        $greedy_result["upload"],
                        $log_stream
                    );
                    log_runtime_adaptive_request_pipeline(
                        $adaptive_pipeline_statistics,
                        $log_stream
                    );
                }

                if(LOG_OPTIMISER_DECISIONS && $connection_optimizer_result["evaluation_due"])
                    log_runtime_connection_optimizer($connection_optimizer_result, $log_stream);
            } else {
                $connection_optimizer_result = null;

                if(
                    LOG_OPTIMISER_DECISIONS
                    && ($standard_upload_result["evaluation_due"] || $standard_upload_result["optimistic_due"])
                ) {
                    $adaptive_pipeline_statistics = collect_runtime_adaptive_request_pipeline_statistics(
                        $connections,
                        $selected_download_peer_keys,
                        $now
                    );
                    log_runtime_standard_policy_decision(
                        $selected_download_peer_keys,
                        $standard_upload_result,
                        $log_stream
                    );
                    log_runtime_adaptive_request_pipeline(
                        $adaptive_pipeline_statistics,
                        $log_stream
                    );
                }
            }

            service_runtime_upload_requests(
                $connections,
                $upload_limiter,
                $upload_slot_count,
                $now,
                $upload_peer_rates
            );

            foreach($connections as $connection) {
                if(
                    $connection->is_terminal()
                    || $connection->state !== PeerConnection::STATE_ESTABLISHED
                    || $connection->fresh_turnover_retiring
                    || !$connection->remote_piece_information_received
                )
                    continue;

                $peer_has_wanted_piece = $piece_priority_cache->peer_has_wanted_piece(
                    $connection,
                    $piece_manager
                );

                if($peer_has_wanted_piece && !$connection->local_interested)
                    $connection->set_local_interested(true);

                if(
                    !$peer_has_wanted_piece
                    || $connection->remote_choking
                    || !isset($selected_download_peer_keys[$connection->peer->endpoint->key])
                )
                    continue;

                $adaptive_pipeline = calculate_runtime_adaptive_request_pipeline(
                    $connection,
                    $now
                );
                $request_limit = $adaptive_pipeline["depth"]
                    - $connection->get_outstanding_block_request_count();

                if($request_limit <= 0)
                    continue;

                $excluded_piece_indexes = [];

                while($request_limit > 0) {
                    $candidate = $piece_priority_cache->find_peer_candidate(
                        $connection,
                        $piece_manager,
                        $excluded_piece_indexes
                    );

                    if($candidate === null)
                        break;

                    $piece_index = $candidate["piece_index"];

                    try {
                        $requests = $connection->queue_block_requests(
                            $piece_index,
                            $request_limit,
                            $now
                        );
                    } catch(Throwable) {
                        close_runtime_piece_connection($connection, $peer_retry_after, $now);

                        break;
                    }

                    if($requests === []) {
                        $excluded_piece_indexes[$piece_index] = true;

                        continue;
                    }

                    if(!isset($logged_active_pieces[$piece_index])) {
                        $logged_active_pieces[$piece_index] = true;
                        log_message(
                            sprintf(
                                "Rarest-first activated piece %d; availability %d peer%s; %d block request%s queued.",
                                $piece_index,
                                $candidate["availability"],
                                $candidate["availability"] === 1 ? "" : "s",
                                count($requests),
                                count($requests) === 1 ? "" : "s"
                            ),
                            $log_stream
                        );
                    }

                    $request_limit -= count($requests);
                }
            }

            $endgame_result = schedule_runtime_endgame_duplicates(
                $connections,
                $piece_manager,
                $selected_download_peer_keys,
                $endgame_state,
                $target_verified_piece_count,
                $now
            );

            if($endgame_result["activated_now"])
                log_runtime_endgame_activation($endgame_result, $log_stream);

            if($now - $last_status_at >= DOWNLOAD_STATUS_INTERVAL) {
                $current_downloaded_bytes = runtime_downloaded_block_bytes($connections, $peer_pool);
                $current_uploaded_bytes = runtime_uploaded_block_bytes($connections, $peer_pool);
                $elapsed = max(0.001, $now - $last_status_at);
                $interval_bytes = max(0, $current_downloaded_bytes - $last_status_downloaded_bytes);
                $interval_uploaded_bytes = max(0, $current_uploaded_bytes - $last_status_uploaded_bytes);
                $rate_mib = ($interval_bytes / $elapsed) / (1024 * 1024);
                $upload_rate_mib = ($interval_uploaded_bytes / $elapsed) / (1024 * 1024);
                $verified_bytes = $piece_manager->get_verified_byte_count();
                $interval_verified_bytes = max(0, $verified_bytes - $last_status_verified_bytes);
                $progress_percent = calculate_runtime_progress_percent(
                    $verified_bytes,
                    $piece_manager->metadata->total_length,
                    $piece_manager->is_complete()
                );
                $connection_statistics = runtime_download_connection_statistics($connections);
                $dynamic_inbound_headroom = runtime_dynamic_inbound_headroom($connections, $now);

                $status_message = sprintf(
                    "Download status: %d/%d pieces (%.2f%%); %.2f MiB/s down, %.2f MiB/s up; %d active, %d established, %d unchoked by remote (%d/%d/%d inbound active/established/unchoked; dynamic headroom %d); %d upload slots, %d upload requests pending; %d download requests outstanding; %d peers known; memory %.1f/%.1f MiB current/peak.",
                    $piece_manager->verified_piece_count,
                    $piece_manager->piece_count,
                    $progress_percent,
                    $rate_mib,
                    $upload_rate_mib,
                    $connection_statistics["active"],
                    $connection_statistics["established"],
                    $connection_statistics["unchoked"],
                    $connection_statistics["inbound_active"],
                    $connection_statistics["inbound_established"],
                    $connection_statistics["inbound_unchoked"],
                    $dynamic_inbound_headroom,
                    $connection_statistics["local_unchoked"],
                    $connection_statistics["pending_upload_requests"],
                    $connection_statistics["outstanding_requests"],
                    $peer_pool->get_count(),
                    memory_get_usage(true) / (1024 * 1024),
                    memory_get_peak_usage(true) / (1024 * 1024)
                );
                log_message($status_message, $log_stream);

                if($progress_stream !== null) {
                    $interval_verified_rate_bytes_per_second = $interval_verified_bytes / $elapsed;

                    if($interval_verified_rate_bytes_per_second > 0) {
                        $progress_rate_bytes_per_second = $progress_rate_bytes_per_second > 0
                            ? (PROGRESS_RATE_EWMA_ALPHA * $interval_verified_rate_bytes_per_second)
                                + ((1.0 - PROGRESS_RATE_EWMA_ALPHA) * $progress_rate_bytes_per_second)
                            : $interval_verified_rate_bytes_per_second;
                    }

                    $remaining_bytes = max(
                        0,
                        $piece_manager->metadata->total_length - $verified_bytes
                    );
                    $eta_seconds = estimate_runtime_eta_seconds(
                        $remaining_bytes,
                        $progress_rate_bytes_per_second
                    );
                    $eta_text = $eta_seconds === null
                        ? "--:--:--"
                        : format_runtime_duration($eta_seconds);

                    write_runtime_progress_line(
                        $progress_stream,
                        sprintf(
                            "Progress %6.2f%% | ETA %s | %5.2f MiB/s ⬇️ | %5.2f MiB/s ⬆️ | %4d/%d peers",
                            $progress_percent,
                            $eta_text,
                            $rate_mib,
                            $upload_rate_mib,
                            $connection_statistics["established"],
                            $peer_pool->get_count()
                        )
                    );
                    $progress_updates++;
                }

                log_runtime_peer_frontier($peer_pool, $log_stream, $now);

                if($runtime_profiler->is_enabled()) {
                    $profile_sample = $runtime_profiler->sample($now);
                    log_runtime_profile_sample(
                        $profile_sample,
                        $piece_priority_cache->get_generation(),
                        $connection_statistics,
                        $log_stream
                    );
                }

                $last_status_at = $now;
                $last_status_downloaded_bytes = $current_downloaded_bytes;
                $last_status_uploaded_bytes = $current_uploaded_bytes;
                $last_status_verified_bytes = $verified_bytes;
            }

            $read_sockets = [];
            $write_sockets = [];
            $socket_connections = [];

            foreach($connections as $connection) {
                if($connection->is_terminal() || !is_resource($connection->socket))
                    continue;

                $socket_id = get_resource_id($connection->socket);
                $socket_connections[$socket_id] = $connection;

                if($connection->wants_read())
                    $read_sockets[] = $connection->socket;

                if($connection->wants_write())
                    $write_sockets[] = $connection->socket;
            }

            if($read_sockets === [] && $write_sockets === []) {
                if($tracker_discovery !== null)
                    $tracker_discovery->poll(0);

                $idle_wait_started_at = hrtime(true);

                if($dht_discovery !== null)
                    $dht_discovery->poll(RUNTIME_SELECT_TIMEOUT_MICROSECONDS);
                else
                    usleep(RUNTIME_SELECT_TIMEOUT_MICROSECONDS);

                $runtime_profiler->record_idle_wait((hrtime(true) - $idle_wait_started_at) / 1000000000.0);

                continue;
            }

            $except_sockets = [];
            $select_started_at = hrtime(true);
            $selected = @stream_select(
                $read_sockets,
                $write_sockets,
                $except_sockets,
                0,
                RUNTIME_SELECT_TIMEOUT_MICROSECONDS
            );
            $select_elapsed_seconds = (hrtime(true) - $select_started_at) / 1000000000.0;

            if($selected !== false)
                $runtime_profiler->record_select($select_elapsed_seconds, $selected);

            if($selected === false)
                throw new RuntimeException("Peer socket selection failed during basic piece downloading.");

            $handled_at = microtime(true);

            // Drain readable peer data before attempting writes. A peer may send its final
            // requested blocks and close immediately. If a socket was readable in this select
            // cycle, defer its writes until the next cycle so queued HAVE/upload traffic cannot
            // trigger an EPIPE before all already-arrived piece frames have been drained.
            $read_socket_ids = [];
            $read_callback_count = 0;
            $write_callback_count = 0;

            foreach($read_sockets as $socket) {
                $socket_id = get_resource_id($socket);
                $read_socket_ids[$socket_id] = true;
                $connection = $socket_connections[$socket_id];

                if(!$connection->is_terminal() && $connection->wants_read()) {
                    $connection->handle_readable($handled_at);
                    $read_callback_count++;
                }
            }

            foreach($write_sockets as $socket) {
                $socket_id = get_resource_id($socket);

                if(isset($read_socket_ids[$socket_id]))
                    continue;

                $connection = $socket_connections[$socket_id];

                if(!$connection->is_terminal() && $connection->wants_write()) {
                    $connection->handle_writable($handled_at);
                    $write_callback_count++;
                }
            }

            $runtime_profiler->record_socket_callbacks($read_callback_count, $write_callback_count);
        }
    } finally {
        if($piece_manager->is_complete()) {
            $completion_shutdown_state = shutdown_runtime_completed_torrent(
                $connections,
                $piece_manager,
                $tracker_discovery,
                $dht_discovery,
                $log_stream,
                microtime(true),
                $peer_listener,
                $port_mapping,
                $peer_pool
            );
        } else {
            foreach($connections as $connection) {
                if(!$connection->is_terminal())
                    $connection->close();
            }
        }
    }

    $final_sample_at = microtime(true);
    $final_peer_metrics = sample_runtime_peer_metrics(
        $connections,
        $peer_metrics_state,
        $final_sample_at,
        true
    );
    $final_price_observations = null;
    $final_marginal_return_records = [];

    if($research_policy_mode === RESEARCH_POLICY_GREEDY) {
        $final_price_observations = update_runtime_greedy_price_estimates(
            $final_peer_metrics,
            $greedy_price_state,
            $final_sample_at,
            true
        );

        if(LOG_OPTIMISER_DECISIONS && $final_price_observations !== null)
            log_runtime_greedy_price_estimates($final_price_observations, $log_stream);

        $final_marginal_return_records = update_runtime_greedy_marginal_returns(
            $final_price_observations ?? [],
            $final_peer_metrics,
            $greedy_marginal_return_state,
            $final_sample_at
        );

        if(LOG_OPTIMISER_DECISIONS && $final_marginal_return_records !== [])
            log_runtime_greedy_marginal_returns($final_marginal_return_records, $log_stream);
    }

    $downloaded_bytes = runtime_downloaded_block_bytes($connections, $peer_pool);
    $verified_piece_indexes = $piece_manager->get_verified_piece_indexes();
    $new_verified_piece_indexes = array_values(array_filter(
        $verified_piece_indexes,
        static function($piece_index) use ($initial_verified_piece_lookup) {
            return !isset($initial_verified_piece_lookup[$piece_index]);
        }
    ));

    foreach($new_verified_piece_indexes as $piece_index) {
        if(isset($logged_verified_pieces[$piece_index]))
            continue;

        $verified_bytes = $piece_manager->get_verified_byte_count();
        $progress_percent = calculate_runtime_progress_percent(
            $verified_bytes,
            $piece_manager->metadata->total_length,
            $piece_manager->is_complete()
        );
        log_message(
            sprintf(
                "Piece %d verified; %d/%d pieces, %.2f%% complete.",
                $piece_index,
                $piece_manager->verified_piece_count,
                $piece_manager->piece_count,
                $progress_percent
            ),
            $log_stream
        );
    }

    $fresh_service_statistics = collect_runtime_fresh_service_statistics(
        $connections,
        $final_sample_at,
        $peer_pool
    );

    $download_elapsed_seconds = max(0.0, $final_sample_at - $download_started_at);
    $runtime_profile = $runtime_profiler->summary($final_sample_at);
    $piece_priority_profile = $piece_priority_cache->get_profile_stats();

    $verified_bytes = $piece_manager->get_verified_byte_count();
    $new_verified_bytes = max(0, $verified_bytes - $initial_verified_byte_count);

    return [
        "policy_mode" => $research_policy_mode,
        "downloaded_bytes" => $downloaded_bytes,
        "verified_bytes" => $verified_bytes,
        "new_verified_bytes" => $new_verified_bytes,
        "download_elapsed_seconds" => $download_elapsed_seconds,
        "overall_download_rate_bytes_per_second" => $download_elapsed_seconds > 0
            ? $downloaded_bytes / $download_elapsed_seconds
            : 0.0,
        "overall_verified_rate_bytes_per_second" => $download_elapsed_seconds > 0
            ? $new_verified_bytes / $download_elapsed_seconds
            : 0.0,
        "progress_updates" => $progress_updates,
        "uploaded_bytes" => runtime_uploaded_block_bytes($connections, $peer_pool),
        "verified_piece_indexes" => $new_verified_piece_indexes,
        "known_peers" => $peer_pool->get_count(),
        "attempted_peers" => count($attempted_peer_keys),
        "peer_metrics" => $final_peer_metrics,
        "peer_metric_samples" => $peer_metrics_state["sample_count"],
        "greedy_prices" => $greedy_price_state["latest"],
        "greedy_price_updates" => $greedy_price_state["update_count"],
        "greedy_price_observations" => $greedy_price_state["observation_count"],
        "greedy_marginal_returns" => $greedy_marginal_return_state["latest"],
        "greedy_marginal_return_updates" => $greedy_marginal_return_state["update_count"],
        "greedy_marginal_return_samples" => $greedy_marginal_return_state["sample_count"],
        "greedy_upload_allocator" => $greedy_upload_allocator_state["latest"],
        "greedy_upload_allocator_updates" => $greedy_upload_allocator_state["update_count"],
        "connection_optimizer" => $connection_optimizer_state["latest"],
        "connection_optimizer_evaluations" => $connection_optimizer_state["evaluation_count"],
        "connection_optimizer_replacements" => $connection_optimizer_state["replacement_count"],
        "connection_optimizer_exploration_replacements" => $connection_optimizer_state["exploration_replacement_count"],
        "fresh_turnover_marks" => $connection_optimizer_state["fresh_turnover_mark_count"],
        "fresh_turnover_completions" => $connection_optimizer_state["fresh_turnover_completion_count"],
        "fresh_service_connections" => $fresh_service_statistics["established_connections"],
        "fresh_service_matured_connections" => $fresh_service_statistics["matured_connections"],
        "fresh_service_unchokes" => $fresh_service_statistics["fresh_unchokes"],
        "fresh_service_seed_unchokes" => $fresh_service_statistics["seed_fresh_unchokes"],
        "fresh_service_leecher_unchokes" => $fresh_service_statistics["leecher_fresh_unchokes"],
        "fresh_service_useful_bytes" => $fresh_service_statistics["fresh_useful_bytes"],
        "adaptive_pipeline_changes" => count_runtime_adaptive_pipeline_changes($peer_pool),
        "adaptive_pipeline_max_depth" => maximum_runtime_adaptive_pipeline_depth($peer_pool),
        "endgame_activations" => $endgame_state["activation_count"],
        "endgame_duplicate_requests" => $endgame_state["duplicate_requests_queued"],
        "endgame_duplicate_cancels" => $endgame_state["duplicate_cancels_sent"],
        "endgame_duplicate_wins" => $endgame_state["duplicate_blocks_won"],
        "inbound_connections_accepted" => $peer_listener?->get_stats()["accepted_connections"] ?? 0,
        "port_mapping_method" => $port_mapping?->method ?? "none",
        "port_mapping_external_port" => $port_mapping?->external_port,
        "completion_shutdown" => $completion_shutdown_state,
        "runtime_profile" => $runtime_profile,
        "piece_priority_profile" => $piece_priority_profile,
    ];
}

function download_runtime_first_piece(
    $peer_pool,
    $magnet,
    $local_peer_id,
    $metadata_exchange,
    $piece_manager,
    $log_stream,
    $tracker_discovery = null,
    $connector = null,
    $dht_discovery = null,
    $peer_listener = null,
    $port_mapping = null,
    $progress_stream = null,
    $research_policy_mode = null
) {
    return download_runtime_pieces(
        $peer_pool,
        $magnet,
        $local_peer_id,
        $metadata_exchange,
        $piece_manager,
        $log_stream,
        $tracker_discovery,
        $connector,
        1,
        $dht_discovery,
        $peer_listener,
        $port_mapping,
        $progress_stream,
        $research_policy_mode
    );
}

function download_runtime_complete_torrent(
    $peer_pool,
    $magnet,
    $local_peer_id,
    $metadata_exchange,
    $piece_manager,
    $log_stream,
    $tracker_discovery = null,
    $connector = null,
    $dht_discovery = null,
    $peer_listener = null,
    $port_mapping = null,
    $progress_stream = null,
    $research_policy_mode = null
) {
    return download_runtime_pieces(
        $peer_pool,
        $magnet,
        $local_peer_id,
        $metadata_exchange,
        $piece_manager,
        $log_stream,
        $tracker_discovery,
        $connector,
        null,
        $dht_discovery,
        $peer_listener,
        $port_mapping,
        $progress_stream,
        $research_policy_mode
    );
}

// Client startup.
function run_client(
    $arguments,
    $error_stream,
    $tracker_list_fetcher = null,
    $log_stream = null,
    $tracker_announcer = null,
    $metadata_retriever = null,
    $storage_base_path = null,
    $output_stream = null
) {
    if($output_stream !== null)
        fwrite($output_stream, <<<BANNER
           _____                                                __  
         _|     |--.--.-----.-----.-----.----.-----.---.-.--.--|  |_ 
        |       |  |  |  _  |  _  |  -__|   _|     |  _  |  |  |   _|
        |_______|_____|___  |___  |_____|__| |__|__|___._|_____|____| v1
                      |_____|_____|                                  


        BANNER);

    if($output_stream !== null && !is_resource($output_stream))
        throw new InvalidArgumentException("Client output stream must be an open stream or null.");

    if(count($arguments) !== 2) {
        fwrite($error_stream, "Usage: php bittorrent.php <magnet link or infohash>\n");

        return 1;
    }

    try {
        $magnet = parse_torrent_input($arguments[1]);
    } catch(InvalidArgumentException) {
        fwrite($error_stream, "Error: expected a valid magnet link or 40-character hexadecimal info hash.\n");

        return 1;
    }

    if($tracker_list_fetcher === null)
        $tracker_list_fetcher = "download_public_tracker_list";

    $owns_log_stream = $log_stream === null;
    $runtime_log_path = null;

    if($log_stream === null)
        $log_stream = create_runtime_deferred_log_stream();

    $using_default_metadata_retriever = $metadata_retriever === null;

    if($metadata_retriever === null)
        $metadata_retriever = "retrieve_runtime_metadata";

    if(!is_callable($metadata_retriever)) {
        fwrite($error_stream, "Error: metadata retriever is not callable.\n");

        return 1;
    }

    log_message("Torrent input accepted: btih {$magnet->info_hash_hex}.", $log_stream);
    $public_trackers = refresh_public_tracker_list($tracker_list_fetcher, $log_stream);
    $tracker_discovery = null;
    $dht_discovery = null;
    $peer_listener = null;
    $port_mapping = null;

    try {
        $local_peer_id = generate_local_peer_id();
        $peer_pool = new PeerPool();
        $peer_listener = new RuntimePeerListener();
        $peer_listener->start();
        log_message(
            "Inbound peer listener started on TCP port {$peer_listener->get_port()}.",
            $log_stream
        );
        $port_mapping = attempt_runtime_tcp_port_mapping(
            $peer_listener->get_port(),
            $log_stream
        );
        $announce_port = $port_mapping->mapped
            ? $port_mapping->external_port
            : $peer_listener->get_port();
        log_message(
            "Tracker and DHT peer port advertised as TCP {$announce_port}; dynamic inbound headroom up to " . INBOUND_HEADROOM_MAX . " within " . sprintf("%.0f", INBOUND_HEADROOM_WINDOW) . "s, with up to " . INBOUND_HANDSHAKE_OVERFLOW . " temporary handshake-overflow connections above the " . DESIRED_CONNECTED_PEERS . "-peer target.",
            $log_stream
        );

        if($tracker_announcer === null) {
            try {
                $dht_discovery = new RuntimeDhtDiscovery(
                    $magnet,
                    $peer_pool,
                    $log_stream,
                    null,
                    null,
                    $announce_port
                );
                $dht_discovery->start();
            } catch(Throwable $exception) {
                if($dht_discovery !== null)
                    $dht_discovery->close();

                $dht_discovery = null;
                log_message(
                    "DHT discovery unavailable: {$exception->getMessage()}",
                    $log_stream
                );
            }
        }

        if($tracker_announcer !== null) {
            $tracker_discovery_result = discover_runtime_peers(
                $magnet,
                $public_trackers,
                $local_peer_id,
                $peer_pool,
                $log_stream,
                $tracker_announcer
            );
            log_message(
                sprintf(
                    "Peer discovery complete: %d known peers from %d tracker attempts.",
                    $tracker_discovery_result["known_peers"],
                    $tracker_discovery_result["tracker_attempts"]
                ),
                $log_stream
            );
        } else {
            $tracker_discovery = new RuntimeTrackerDiscovery(
                $magnet,
                $public_trackers,
                $local_peer_id,
                $peer_pool,
                $log_stream,
                $announce_port
            );
            $tracker_discovery->start();
            $discovery_stats = $tracker_discovery->get_stats();
            log_message(
                sprintf(
                    "Peer discovery running concurrently: %d tracker requests started; %d peers currently known.",
                    $discovery_stats["tracker_attempts"],
                    $discovery_stats["known_peers"]
                ),
                $log_stream
            );
        }
    } catch(Throwable $exception) {
        if($tracker_discovery !== null)
            $tracker_discovery->close();

        if($dht_discovery !== null)
            $dht_discovery->close();

        if($port_mapping !== null && !$port_mapping->is_closed())
            $port_mapping->close();

        if($peer_listener !== null && !$peer_listener->is_closed())
            $peer_listener->close();

        fwrite($error_stream, "Error: peer discovery failed: {$exception->getMessage()}\n");

        return 2;
    }

    if($tracker_discovery === null && $peer_pool->get_count() === 0) {
        if($port_mapping !== null && !$port_mapping->is_closed())
            $port_mapping->close();

        if($peer_listener !== null && !$peer_listener->is_closed())
            $peer_listener->close();

        fwrite($error_stream, "Error: no peers were discovered from the magnet or trackers.\n");

        return 2;
    }

    try {
        if($using_default_metadata_retriever) {
            $metadata_exchange = $metadata_retriever(
                $peer_pool,
                $magnet,
                $local_peer_id,
                $log_stream,
                null,
                $tracker_discovery,
                $dht_discovery,
                $peer_listener,
                $port_mapping
            );
        } else {
            $metadata_exchange = $metadata_retriever(
                $peer_pool,
                $magnet,
                $local_peer_id,
                $log_stream
            );
        }

        if(!($metadata_exchange instanceof MetadataExchange) || !$metadata_exchange->is_complete())
            throw new RuntimeException("Metadata retriever did not return complete, verified metadata.");

        if(!hash_equals($magnet->info_hash, $metadata_exchange->expected_info_hash))
            throw new RuntimeException("Verified metadata does not match the requested magnet info hash.");

        $verified_metadata = $metadata_exchange->get_torrent_metadata();

        if($owns_log_stream) {
            $runtime_log_path = build_runtime_log_path(
                $verified_metadata,
                $storage_base_path
            );
            redirect_runtime_log_stream_to_file($log_stream, $runtime_log_path);
            log_message("Detailed session log: {$runtime_log_path}", $log_stream);
        }

        log_message(
            "Active research policy: " . normalise_runtime_research_policy() . ".",
            $log_stream
        );

        if($output_stream !== null) {
            fwrite($output_stream, "Torrent: {$verified_metadata->name}\n");
            fwrite(
                $output_stream,
                sprintf(
                    "Size: %.2f MiB | %d file%s\n",
                    $verified_metadata->total_length / (1024 * 1024),
                    count($verified_metadata->files),
                    count($verified_metadata->files) === 1 ? "" : "s"
                )
            );
        }
    } catch(Throwable $exception) {
        if($tracker_discovery !== null)
            $tracker_discovery->close();

        if($dht_discovery !== null)
            $dht_discovery->close();

        if($port_mapping !== null && !$port_mapping->is_closed())
            $port_mapping->close();

        if($peer_listener !== null && !$peer_listener->is_closed())
            $peer_listener->close();

        fwrite($error_stream, "Error: metadata retrieval failed: {$exception->getMessage()}\n");

        return 2;
    }

    if($tracker_discovery !== null) {
        $discovery_stats = $tracker_discovery->get_stats();
        log_message(
            sprintf(
                "Peer discovery at metadata completion: %d known peers; %d tracker successes, %d failures, %d initial requests still pending.",
                $discovery_stats["known_peers"],
                $discovery_stats["tracker_successes"],
                $discovery_stats["tracker_failures"],
                $discovery_stats["pending_initial"]
            ),
            $log_stream
        );
    }

    if($dht_discovery !== null) {
        $dht_stats = $dht_discovery->get_stats();
        log_message(
            sprintf(
                "DHT at metadata completion: %d routing nodes in %d buckets; %d peers added; %d queries sent, %d responses, %d timeouts.",
                $dht_stats["routing_nodes"],
                $dht_stats["routing_buckets"],
                $dht_stats["peers_added"],
                $dht_stats["queries_sent"],
                $dht_stats["responses_received"],
                $dht_stats["query_timeouts"]
            ),
            $log_stream
        );
    }

    try {
        $runtime_storage = initialise_runtime_piece_manager(
            $metadata_exchange,
            $storage_base_path,
            $log_stream
        );
    } catch(Throwable $exception) {
        if($tracker_discovery !== null)
            $tracker_discovery->close();

        if($dht_discovery !== null)
            $dht_discovery->close();

        if($port_mapping !== null && !$port_mapping->is_closed())
            $port_mapping->close();

        if($peer_listener !== null && !$peer_listener->is_closed())
            $peer_listener->close();

        fwrite($error_stream, "Error: torrent storage initialisation failed: {$exception->getMessage()}\n");

        return 3;
    }

    try {
        $download_result = download_runtime_complete_torrent(
            $peer_pool,
            $magnet,
            $local_peer_id,
            $metadata_exchange,
            $runtime_storage["piece_manager"],
            $log_stream,
            $tracker_discovery,
            null,
            $dht_discovery,
            $peer_listener,
            $port_mapping,
            $output_stream
        );
    } catch(Throwable $exception) {
        if($tracker_discovery !== null)
            $tracker_discovery->close();

        if($dht_discovery !== null)
            $dht_discovery->close();

        if($port_mapping !== null && !$port_mapping->is_closed())
            $port_mapping->close();

        if($peer_listener !== null && !$peer_listener->is_closed())
            $peer_listener->close();

        if($output_stream !== null)
            finish_runtime_progress_line($output_stream);

        fwrite($error_stream, "Error: complete torrent download failed: {$exception->getMessage()}\n");

        return 4;
    }

    $final_dht_stats = $dht_discovery?->get_stats();
    $final_pex_peer_count = count_runtime_pex_peers($peer_pool);

    if($tracker_discovery !== null && !$tracker_discovery->is_closed()) {
        $tracker_discovery->set_transfer_counters(
            $download_result["downloaded_bytes"],
            $download_result["uploaded_bytes"],
            0
        );
        $tracker_discovery->close();
    }

    if($dht_discovery !== null && !$dht_discovery->is_closed())
        $dht_discovery->close();

    if($port_mapping !== null && !$port_mapping->is_closed())
        $port_mapping->close();

    if($peer_listener !== null && !$peer_listener->is_closed())
        $peer_listener->close();

    log_message(
        sprintf(
            "RESEARCH_RESULT policy=%s downloaded_bytes=%d uploaded_bytes=%d elapsed_seconds=%.6f overall_download_bytes_per_second=%.3f known_peers=%d attempted_peers=%d inbound_connections=%d metric_samples=%d adaptive_pipeline_changes=%d endgame_duplicate_requests=%d greedy_price_observations=%d greedy_marginal_return_samples=%d greedy_allocator_updates=%d connection_replacements=%d",
            $download_result["policy_mode"],
            $download_result["downloaded_bytes"],
            $download_result["uploaded_bytes"],
            $download_result["download_elapsed_seconds"],
            $download_result["overall_download_rate_bytes_per_second"],
            $download_result["known_peers"],
            $download_result["attempted_peers"],
            $download_result["inbound_connections_accepted"],
            $download_result["peer_metric_samples"],
            $download_result["adaptive_pipeline_changes"],
            $download_result["endgame_duplicate_requests"],
            $download_result["greedy_price_observations"],
            $download_result["greedy_marginal_return_samples"],
            $download_result["greedy_upload_allocator_updates"],
            $download_result["connection_optimizer_replacements"]
        ),
        $log_stream
    );

    $runtime_profile = $download_result["runtime_profile"];
    $priority_profile = $download_result["piece_priority_profile"];
    $cpu_percent = $runtime_profile["cpu_percent"] === null
        ? "n/a"
        : sprintf("%.3f", $runtime_profile["cpu_percent"]);
    $cpu_user_seconds = $runtime_profile["cpu_user_seconds"] === null
        ? "n/a"
        : sprintf("%.6f", $runtime_profile["cpu_user_seconds"]);
    $cpu_system_seconds = $runtime_profile["cpu_system_seconds"] === null
        ? "n/a"
        : sprintf("%.6f", $runtime_profile["cpu_system_seconds"]);
    log_message(
        sprintf(
            "RUNTIME_PROFILE policy=%s elapsed_seconds=%.6f cpu_percent=%s cpu_user_seconds=%s cpu_system_seconds=%s loop_iterations=%d loop_hz=%.3f select_calls=%d select_wait_seconds=%.6f select_wait_percent=%.3f idle_wait_seconds=%.6f read_callbacks=%d write_callbacks=%d messages=%d piece_messages=%d empty_connections_pruned=%d terminal_connections_pruned=%d memory_current_bytes=%d memory_peak_bytes=%d priority_generations=%d priority_entries=%d tracked_priority_connections=%d wanted_cache_entries=%d",
            $download_result["policy_mode"],
            $runtime_profile["elapsed_seconds"],
            $cpu_percent,
            $cpu_user_seconds,
            $cpu_system_seconds,
            $runtime_profile["loop_iterations"],
            $runtime_profile["loop_hz"],
            $runtime_profile["select_calls"],
            $runtime_profile["select_wait_seconds"],
            $runtime_profile["select_wait_percent"],
            $runtime_profile["idle_wait_seconds"],
            $runtime_profile["read_callbacks"],
            $runtime_profile["write_callbacks"],
            $runtime_profile["messages"],
            $runtime_profile["piece_messages"],
            $runtime_profile["empty_connections_pruned"],
            $runtime_profile["terminal_connections_pruned"],
            $runtime_profile["memory_current_bytes"],
            $runtime_profile["memory_peak_bytes"],
            $priority_profile["generation"],
            $priority_profile["priority_entries"],
            $priority_profile["tracked_connections"],
            $priority_profile["wanted_cache_entries"]
        ),
        $log_stream
    );

    log_message(
        sprintf(
            "Policy comparison complete: policy %s; %.2f MiB downloaded and %.2f MiB uploaded in %s; %.2f MiB/s overall download; %d peers known, %d attempted, %d inbound accepted; %d metric samples; adaptive pipeline max depth %d with %d changes; endgame %d activations, %d duplicate requests, %d redundant cancels and %d duplicate wins; greedy-only observations %d upload-price, %d marginal-return, %d allocator updates and %d connection replacements.",
            $download_result["policy_mode"],
            $download_result["downloaded_bytes"] / 1048576,
            $download_result["uploaded_bytes"] / 1048576,
            format_runtime_duration($download_result["download_elapsed_seconds"]),
            $download_result["overall_download_rate_bytes_per_second"] / 1048576,
            $download_result["known_peers"],
            $download_result["attempted_peers"],
            $download_result["inbound_connections_accepted"],
            $download_result["peer_metric_samples"],
            $download_result["adaptive_pipeline_max_depth"],
            $download_result["adaptive_pipeline_changes"],
            $download_result["endgame_activations"],
            $download_result["endgame_duplicate_requests"],
            $download_result["endgame_duplicate_cancels"],
            $download_result["endgame_duplicate_wins"],
            $download_result["greedy_price_observations"],
            $download_result["greedy_marginal_return_samples"],
            $download_result["greedy_upload_allocator_updates"],
            $download_result["connection_optimizer_replacements"]
        ),
        $log_stream
    );
    flush_runtime_log_stream($log_stream);

    if($output_stream !== null) {
        write_runtime_progress_line(
            $output_stream,
            sprintf(
                "Complete 100.00%% | %.2f MiB | %s elapsed | %.2f MiB/s overall",
                $download_result["verified_bytes"] / (1024 * 1024),
                format_runtime_duration($download_result["download_elapsed_seconds"]),
                $download_result["overall_verified_rate_bytes_per_second"] / (1024 * 1024)
            )
        );
        finish_runtime_progress_line($output_stream);
    }

    if($owns_log_stream) {
        close_runtime_log_stream($log_stream);
        fclose($log_stream);
    }

    return 0;
}

// CLI entrypoint.
if(realpath($_SERVER["SCRIPT_FILENAME"]) === realpath(__FILE__))
    exit(run_client($argv, STDERR, null, null, null, null, null, STDOUT));
