use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::mpsc::{self, Receiver, SyncSender, TryRecvError, TrySendError};
use std::time::{Duration, Instant};

use crate::audit::{DiscoveredFrom, PublicationPointAudit};
use crate::cir::CirInputAccumulator;
use crate::parallel::object_worker::{
    ObjectTaskExecutor, ObjectWorkerSubmitError, ScopedObjectWorkerPool,
};
use crate::parallel::repo_runtime::{RepoSyncRequestStatus, RepoSyncRuntimeOutcome};
use crate::parallel::types::RepoIdentity;
use crate::policy::SignedObjectFailurePolicy;
use crate::report::Warning;
use crate::storage::VcirReplaceTimingBreakdown;
use crate::validation::manifest::PublicationPointData;
use crate::validation::manifest::PublicationPointSource;
use crate::validation::objects::{
    ObjectsOutput, OwnedRoaTask, ParallelObjectsPrepare, ParallelObjectsStage,
    RoaValidationCacheInput, prepare_publication_point_for_parallel_roa_with_cache,
    reduce_parallel_roa_stage,
};
use crate::validation::tree::{
    CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult, PublicationPointRunner,
    TreeRunAuditOutput, TreeRunConfig, TreeRunError, TreeRunOutput, ca_depth_is_allowed,
    next_allowed_ca_depth, run_tree_serial_audit_multi_root,
};
use crate::validation::tree_runner::{
    FreshPublicationPointFinalizeOutput, FreshPublicationPointStage, Rpkiv1PublicationPointRunner,
};

#[derive(Clone, Debug)]
struct QueuedCaInstance {
    id: u64,
    handle: CaInstanceHandle,
    parent_id: Option<u64>,
    discovered_from: Option<DiscoveredFrom>,
}

#[derive(Clone, Debug)]
struct ReadyCaInstance {
    node: QueuedCaInstance,
    repo_outcome: RepoSyncRuntimeOutcome,
    ready_enqueued_at: Instant,
}

struct InflightPublicationPoint {
    node: QueuedCaInstance,
    fresh_stage: FreshPublicationPointStage,
    objects_prepare: ParallelObjectsPrepare,
    repo_outcome: RepoSyncRuntimeOutcome,
    warnings: Vec<Warning>,
    started_at: Instant,
    objects_started_at: Instant,
    task_count: usize,
    tasks_submitted: usize,
    first_task_submitted_at: Option<Instant>,
    last_task_submitted_at: Option<Instant>,
    first_result_at: Option<Instant>,
    last_result_at: Option<Instant>,
    worker_ms_total: u64,
    worker_ms_max: u64,
    queue_wait_ms_total: u64,
    queue_wait_ms_max: u64,
    finalize_enqueued_at: Option<Instant>,
    results: Vec<crate::validation::objects::RoaTaskResult>,
}

struct FinishedPublicationPoint {
    node: FinishedPublicationPointNode,
    result: FinishedPublicationPointResult,
}

#[derive(Clone, Debug)]
struct FinishedPublicationPointNode {
    id: u64,
    parent_id: Option<u64>,
    discovered_from: Option<DiscoveredFrom>,
    manifest_rsync_uri: String,
}

impl FinishedPublicationPointNode {
    fn from_queued(node: QueuedCaInstance) -> Self {
        Self {
            id: node.id,
            parent_id: node.parent_id,
            discovered_from: node.discovered_from,
            manifest_rsync_uri: node.handle.manifest_rsync_uri,
        }
    }
}

#[derive(Debug)]
enum FinishedPublicationPointResult {
    Ok {
        source: PublicationPointSource,
        warnings: Vec<Warning>,
        objects: ObjectsOutput,
        audit: PublicationPointAudit,
        cir_fresh_objects: Vec<crate::audit::ObjectAuditEntry>,
        cir_cached_objects: Vec<crate::audit::ObjectAuditEntry>,
    },
    Err(String),
}

struct FinalizeTask {
    state: InflightPublicationPoint,
}

/// Outcome of the pure compute phase for one ready publication point.
///
/// `compute_ready_publication_point_stage` only performs read-only validation
/// work (publication point cache lookup, fresh snapshot staging, ROA prepare)
/// and returns this enum. `apply_ready_publication_point_stage` then performs
/// every write to control-loop state (`ca_queue`/`next_id`, `finished`,
/// `pending_roa_dispatch`, `pending_finalization`, `inflight_publication_points`)
/// in the same per-publication-point order the monolithic staging function did.
/// Each variant payload is boxed so the enum itself stays small.
enum StageOutcome {
    /// Publication point cache hit: the run result is fully built in memory and
    /// only needs child enqueueing plus a `finished` entry.
    CacheHit(Box<CacheHitOutcome>),
    /// Fresh staging failed: apply runs the existing blocking
    /// `run_publication_point` fallback inline on the control thread.
    FreshError(Box<FreshErrorOutcome>),
    /// Fresh staging succeeded and ROA prepare returned complete objects (no
    /// ROA tasks to dispatch): apply hands the publication point to the
    /// finalize worker through `pending_finalization` instead of running the
    /// finalize synchronously on the control thread.
    Complete(Box<CompleteOutcome>),
    /// Fresh staging succeeded with a staged objects plan that contains zero
    /// ROA tasks: apply queues the finalize task directly.
    ZeroTask(Box<StagedOutcome>),
    /// Fresh staging succeeded with ROA tasks to dispatch: apply appends the
    /// tasks to `pending_roa_dispatch` and registers the inflight publication
    /// point.
    Fresh(Box<StagedOutcome>),
}

struct CacheHitOutcome {
    ready: ReadyCaInstance,
    publication_point_started: Instant,
    result: PublicationPointRunResult,
}

struct FreshErrorOutcome {
    ready: ReadyCaInstance,
    publication_point_started: Instant,
}

struct CompleteOutcome {
    ready: ReadyCaInstance,
    publication_point_started: Instant,
    fresh_stage: FreshPublicationPointStage,
    warnings: Vec<Warning>,
    objects: ObjectsOutput,
}

struct StagedOutcome {
    ready: ReadyCaInstance,
    publication_point_started: Instant,
    fresh_stage: FreshPublicationPointStage,
    warnings: Vec<Warning>,
    objects_stage: ParallelObjectsStage,
}

struct FinalizeWorkerResult {
    finished: FinishedPublicationPoint,
    metrics: FinalizePublicationPointMetrics,
}

/// Task submitted to the experimental ready-stage worker pool: everything
/// `compute_ready_publication_point_stage` needs for one ready publication
/// point.
struct ReadyStageTask {
    ready: ReadyCaInstance,
    ready_queue_len_after_pop: usize,
    submitted_at: Instant,
}

/// Result drained from the ready-stage worker pool: the compute outcome and
/// its metrics plus per-task pool timing for the `phase2_stage_pool_stats`
/// observability event.
struct ReadyStageWorkerResult {
    outcome: StageOutcome,
    metrics: ReadyStageMetrics,
    queue_wait_ms: u64,
    worker_ms: u64,
}

/// Executor borrowing the publication point runner so stage workers can run
/// the read-only compute phase off the control thread. The runner is shared
/// with the finalize worker and the ROA pool in the same way; the scoped pool
/// guarantees all borrows end before the enclosing `std::thread::scope`.
struct ReadyStageTaskExecutor<'a> {
    runner: &'a Rpkiv1PublicationPointRunner<'a>,
}

impl<'a> ObjectTaskExecutor<ReadyStageTask, ReadyStageWorkerResult> for ReadyStageTaskExecutor<'a> {
    fn execute(&self, _worker_index: usize, task: ReadyStageTask) -> ReadyStageWorkerResult {
        let worker_started = Instant::now();
        let queue_wait_ms = worker_started
            .saturating_duration_since(task.submitted_at)
            .as_millis() as u64;
        let (outcome, metrics) = compute_ready_publication_point_stage(
            self.runner,
            task.ready,
            task.ready_queue_len_after_pop,
        );
        ReadyStageWorkerResult {
            outcome,
            metrics,
            queue_wait_ms,
            worker_ms: elapsed_ms(worker_started),
        }
    }
}

type ReadyStagePool<'scope, 'env> = ScopedObjectWorkerPool<
    'scope,
    'env,
    ReadyStageTask,
    ReadyStageWorkerResult,
    ReadyStageTaskExecutor<'env>,
>;

#[derive(Default)]
struct StageDispatchMetrics {
    submitted: usize,
    queue_full: bool,
    duration_ms: u64,
}

#[derive(Default)]
struct StageDrainMetrics {
    results_drained: usize,
    queue_wait_ms_total: u64,
    queue_wait_ms_max: u64,
    worker_ms_total: u64,
    worker_ms_max: u64,
    duration_ms: u64,
}

#[derive(Default)]
struct ReadyStageMetrics {
    manifest_rsync_uri: Option<String>,
    publication_point_rsync_uri: Option<String>,
    ready_count: usize,
    fallback_count: usize,
    complete_count: usize,
    staged_count: usize,
    zero_task_count: usize,
    error_count: usize,
    discovered_children: usize,
    locked_files: usize,
    roa_tasks: usize,
    aspa_objects: usize,
    stage_fresh_ms: u64,
    snapshot_prepare_ms: u64,
    snapshot_current_index_lock_ms: u64,
    snapshot_manifest_load_ms: u64,
    snapshot_manifest_index_lookup_ms: u64,
    snapshot_manifest_blob_load_ms: u64,
    snapshot_manifest_decode_ms: u64,
    snapshot_replay_guard_ms: u64,
    replay_meta_hit_count: usize,
    replay_meta_miss_count: usize,
    snapshot_manifest_entries_ms: u64,
    snapshot_pack_files_ms: u64,
    snapshot_pack_files_index_lookup_ms: u64,
    snapshot_pack_files_blob_load_ms: u64,
    snapshot_ee_path_validate_ms: u64,
    snapshot_manifest_file_count: usize,
    child_discovery_ms: u64,
    child_enqueue_ms: u64,
    ready_queue_wait_ms: u64,
    ready_queue_len_after_pop: usize,
    roa_presence_scan_ms: u64,
    roa_cache_view_ms: u64,
    direct_finalize_ms: u64,
    fallback_full_run_ms: u64,
    prepare_ms: u64,
    build_roa_tasks_ms: u64,
    total_ms: u64,
}

#[derive(Default)]
struct ReadyStageBatchMetrics {
    ready_count: usize,
    fallback_count: usize,
    complete_count: usize,
    staged_count: usize,
    zero_task_count: usize,
    error_count: usize,
    discovered_children: usize,
    locked_files: usize,
    roa_tasks: usize,
    aspa_objects: usize,
    stage_fresh_ms_total: u64,
    stage_fresh_ms_max: u64,
    stage_fresh_ms_max_manifest_rsync_uri: Option<String>,
    stage_fresh_ms_max_publication_point_rsync_uri: Option<String>,
    snapshot_prepare_ms_total: u64,
    snapshot_prepare_ms_max: u64,
    snapshot_current_index_lock_ms_total: u64,
    snapshot_current_index_lock_ms_max: u64,
    snapshot_manifest_load_ms_total: u64,
    snapshot_manifest_load_ms_max: u64,
    snapshot_manifest_index_lookup_ms_total: u64,
    snapshot_manifest_index_lookup_ms_max: u64,
    snapshot_manifest_blob_load_ms_total: u64,
    snapshot_manifest_blob_load_ms_max: u64,
    snapshot_manifest_decode_ms_total: u64,
    snapshot_manifest_decode_ms_max: u64,
    snapshot_replay_guard_ms_total: u64,
    snapshot_replay_guard_ms_max: u64,
    replay_meta_hit_count: usize,
    replay_meta_miss_count: usize,
    snapshot_manifest_entries_ms_total: u64,
    snapshot_manifest_entries_ms_max: u64,
    snapshot_pack_files_ms_total: u64,
    snapshot_pack_files_ms_max: u64,
    snapshot_pack_files_index_lookup_ms_total: u64,
    snapshot_pack_files_index_lookup_ms_max: u64,
    snapshot_pack_files_blob_load_ms_total: u64,
    snapshot_pack_files_blob_load_ms_max: u64,
    snapshot_ee_path_validate_ms_total: u64,
    snapshot_ee_path_validate_ms_max: u64,
    snapshot_manifest_file_count_total: usize,
    snapshot_manifest_file_count_max: usize,
    child_discovery_ms_total: u64,
    child_discovery_ms_max: u64,
    child_enqueue_ms_total: u64,
    child_enqueue_ms_max: u64,
    ready_queue_wait_ms_total: u64,
    ready_queue_wait_ms_max: u64,
    roa_presence_scan_ms_total: u64,
    roa_presence_scan_ms_max: u64,
    roa_cache_view_ms_total: u64,
    roa_cache_view_ms_max: u64,
    direct_finalize_ms_total: u64,
    direct_finalize_ms_max: u64,
    fallback_full_run_ms_total: u64,
    fallback_full_run_ms_max: u64,
    prepare_ms_total: u64,
    prepare_ms_max: u64,
    build_roa_tasks_ms_total: u64,
    build_roa_tasks_ms_max: u64,
    total_ms: u64,
}

impl ReadyStageBatchMetrics {
    fn record(&mut self, metrics: ReadyStageMetrics) {
        self.ready_count += metrics.ready_count;
        self.fallback_count += metrics.fallback_count;
        self.complete_count += metrics.complete_count;
        self.staged_count += metrics.staged_count;
        self.zero_task_count += metrics.zero_task_count;
        self.error_count += metrics.error_count;
        self.discovered_children += metrics.discovered_children;
        self.locked_files += metrics.locked_files;
        self.roa_tasks += metrics.roa_tasks;
        self.aspa_objects += metrics.aspa_objects;
        if metrics.stage_fresh_ms >= self.stage_fresh_ms_max {
            self.stage_fresh_ms_max_manifest_rsync_uri = metrics.manifest_rsync_uri.clone();
            self.stage_fresh_ms_max_publication_point_rsync_uri =
                metrics.publication_point_rsync_uri.clone();
        }
        self.stage_fresh_ms_total += metrics.stage_fresh_ms;
        self.stage_fresh_ms_max = self.stage_fresh_ms_max.max(metrics.stage_fresh_ms);
        self.snapshot_prepare_ms_total += metrics.snapshot_prepare_ms;
        self.snapshot_prepare_ms_max = self
            .snapshot_prepare_ms_max
            .max(metrics.snapshot_prepare_ms);
        self.snapshot_current_index_lock_ms_total += metrics.snapshot_current_index_lock_ms;
        self.snapshot_current_index_lock_ms_max = self
            .snapshot_current_index_lock_ms_max
            .max(metrics.snapshot_current_index_lock_ms);
        self.snapshot_manifest_load_ms_total += metrics.snapshot_manifest_load_ms;
        self.snapshot_manifest_load_ms_max = self
            .snapshot_manifest_load_ms_max
            .max(metrics.snapshot_manifest_load_ms);
        self.snapshot_manifest_index_lookup_ms_total += metrics.snapshot_manifest_index_lookup_ms;
        self.snapshot_manifest_index_lookup_ms_max = self
            .snapshot_manifest_index_lookup_ms_max
            .max(metrics.snapshot_manifest_index_lookup_ms);
        self.snapshot_manifest_blob_load_ms_total += metrics.snapshot_manifest_blob_load_ms;
        self.snapshot_manifest_blob_load_ms_max = self
            .snapshot_manifest_blob_load_ms_max
            .max(metrics.snapshot_manifest_blob_load_ms);
        self.snapshot_manifest_decode_ms_total += metrics.snapshot_manifest_decode_ms;
        self.snapshot_manifest_decode_ms_max = self
            .snapshot_manifest_decode_ms_max
            .max(metrics.snapshot_manifest_decode_ms);
        self.snapshot_replay_guard_ms_total += metrics.snapshot_replay_guard_ms;
        self.snapshot_replay_guard_ms_max = self
            .snapshot_replay_guard_ms_max
            .max(metrics.snapshot_replay_guard_ms);
        self.replay_meta_hit_count += metrics.replay_meta_hit_count;
        self.replay_meta_miss_count += metrics.replay_meta_miss_count;
        self.snapshot_manifest_entries_ms_total += metrics.snapshot_manifest_entries_ms;
        self.snapshot_manifest_entries_ms_max = self
            .snapshot_manifest_entries_ms_max
            .max(metrics.snapshot_manifest_entries_ms);
        self.snapshot_pack_files_ms_total += metrics.snapshot_pack_files_ms;
        self.snapshot_pack_files_ms_max = self
            .snapshot_pack_files_ms_max
            .max(metrics.snapshot_pack_files_ms);
        self.snapshot_pack_files_index_lookup_ms_total +=
            metrics.snapshot_pack_files_index_lookup_ms;
        self.snapshot_pack_files_index_lookup_ms_max = self
            .snapshot_pack_files_index_lookup_ms_max
            .max(metrics.snapshot_pack_files_index_lookup_ms);
        self.snapshot_pack_files_blob_load_ms_total += metrics.snapshot_pack_files_blob_load_ms;
        self.snapshot_pack_files_blob_load_ms_max = self
            .snapshot_pack_files_blob_load_ms_max
            .max(metrics.snapshot_pack_files_blob_load_ms);
        self.snapshot_ee_path_validate_ms_total += metrics.snapshot_ee_path_validate_ms;
        self.snapshot_ee_path_validate_ms_max = self
            .snapshot_ee_path_validate_ms_max
            .max(metrics.snapshot_ee_path_validate_ms);
        self.snapshot_manifest_file_count_total += metrics.snapshot_manifest_file_count;
        self.snapshot_manifest_file_count_max = self
            .snapshot_manifest_file_count_max
            .max(metrics.snapshot_manifest_file_count);
        self.child_discovery_ms_total += metrics.child_discovery_ms;
        self.child_discovery_ms_max = self.child_discovery_ms_max.max(metrics.child_discovery_ms);
        self.child_enqueue_ms_total += metrics.child_enqueue_ms;
        self.child_enqueue_ms_max = self.child_enqueue_ms_max.max(metrics.child_enqueue_ms);
        self.ready_queue_wait_ms_total += metrics.ready_queue_wait_ms;
        self.ready_queue_wait_ms_max = self
            .ready_queue_wait_ms_max
            .max(metrics.ready_queue_wait_ms);
        self.roa_presence_scan_ms_total += metrics.roa_presence_scan_ms;
        self.roa_presence_scan_ms_max = self
            .roa_presence_scan_ms_max
            .max(metrics.roa_presence_scan_ms);
        self.roa_cache_view_ms_total += metrics.roa_cache_view_ms;
        self.roa_cache_view_ms_max = self.roa_cache_view_ms_max.max(metrics.roa_cache_view_ms);
        self.direct_finalize_ms_total += metrics.direct_finalize_ms;
        self.direct_finalize_ms_max = self.direct_finalize_ms_max.max(metrics.direct_finalize_ms);
        self.fallback_full_run_ms_total += metrics.fallback_full_run_ms;
        self.fallback_full_run_ms_max = self
            .fallback_full_run_ms_max
            .max(metrics.fallback_full_run_ms);
        self.prepare_ms_total += metrics.prepare_ms;
        self.prepare_ms_max = self.prepare_ms_max.max(metrics.prepare_ms);
        self.build_roa_tasks_ms_total += metrics.build_roa_tasks_ms;
        self.build_roa_tasks_ms_max = self.build_roa_tasks_ms_max.max(metrics.build_roa_tasks_ms);
        self.total_ms += metrics.total_ms;
    }
}

#[derive(Default)]
struct RoaDispatchMetrics {
    attempted: usize,
    submitted: usize,
    queue_full: bool,
    pending_remaining: usize,
    duration_ms: u64,
}

#[derive(Default)]
struct ObjectDrainMetrics {
    results_drained: usize,
    publication_points_completed: usize,
    worker_ms_total: u64,
    worker_ms_max: u64,
    queue_wait_ms_total: u64,
    queue_wait_ms_max: u64,
    result_budget_exhausted: bool,
    duration_ms: u64,
}

#[derive(Default)]
struct FinalizeSubmitMetrics {
    submitted: usize,
    queue_full: bool,
    duration_ms: u64,
}

#[derive(Default)]
struct FinalizePublicationPointMetrics {
    reduce_ms: u64,
    finalize_ms: u64,
    finalize_queue_wait_ms: Option<u64>,
    finalize_worker_ms: u64,
    snapshot_pack_ms: u64,
    persist_vcir_ms: u64,
    persist_build_vcir_ms: u64,
    persist_replace_vcir_ms: u64,
    persist_replace_breakdown: VcirReplaceTimingBreakdown,
    ccr_projection_build_ms: u64,
    ccr_append_ms: u64,
    audit_build_ms: u64,
    locked_files: usize,
    child_count: usize,
    warning_count: usize,
    vrp_count: usize,
    vap_count: usize,
    router_key_count: usize,
    audit_object_count: usize,
}

#[derive(Default)]
struct FinalizeResultsDrainMetrics {
    results_drained: usize,
    reduce_ms_total: u64,
    reduce_ms_max: u64,
    finalize_ms_total: u64,
    finalize_ms_max: u64,
    finalize_queue_wait_ms_max: u64,
    finalize_worker_ms_total: u64,
    finalize_worker_ms_max: u64,
    snapshot_pack_ms_total: u64,
    snapshot_pack_ms_max: u64,
    persist_vcir_ms_total: u64,
    persist_vcir_ms_max: u64,
    persist_build_vcir_ms_total: u64,
    persist_build_vcir_ms_max: u64,
    persist_replace_vcir_ms_total: u64,
    persist_replace_vcir_ms_max: u64,
    ccr_projection_build_ms_total: u64,
    ccr_projection_build_ms_max: u64,
    ccr_append_ms_total: u64,
    ccr_append_ms_max: u64,
    audit_build_ms_total: u64,
    audit_build_ms_max: u64,
    duration_ms: u64,
}

#[derive(Default)]
struct RepoDrainMetrics {
    event_count: usize,
    completions: usize,
    ready_enqueued: usize,
    duration_ms: u64,
}

const REPO_RESULT_DRAIN_MAX_EVENTS: usize = 64;

fn elapsed_ms(started: Instant) -> u64 {
    started.elapsed().as_millis() as u64
}

fn emit_control_loop_slow(
    duration_ms: u64,
    repo_poll_timeout: Duration,
    repo_metrics: &RepoDrainMetrics,
    ready_batch_metrics: &ReadyStageBatchMetrics,
    ca_queue_len: usize,
    ready_queue_len: usize,
    ca_waiting_repo_identities: usize,
    pending_roa_dispatch_len: usize,
    inflight_publication_points_len: usize,
    pending_finalization_len: usize,
    finalize_inflight: usize,
) {
    let threshold_ms = crate::progress_log::control_loop_slow_threshold_ms();
    if duration_ms < threshold_ms {
        return;
    }
    crate::progress_log::emit(
        "phase2_control_loop_slow",
        serde_json::json!({
            "duration_ms": duration_ms,
            "slow_threshold_ms": threshold_ms,
            "repo_poll_timeout_ms": repo_poll_timeout.as_millis() as u64,
            "repo_event_count": repo_metrics.event_count,
            "repo_completions": repo_metrics.completions,
            "repo_ready_enqueued": repo_metrics.ready_enqueued,
            "repo_drain_duration_ms": repo_metrics.duration_ms,
            "ready_count": ready_batch_metrics.ready_count,
            "ready_batch_duration_ms": ready_batch_metrics.total_ms,
            "ready_batch_stage_fresh_ms_total": ready_batch_metrics.stage_fresh_ms_total,
            "ready_batch_stage_fresh_ms_max": ready_batch_metrics.stage_fresh_ms_max,
            "ready_batch_stage_fresh_ms_max_manifest_rsync_uri": ready_batch_metrics.stage_fresh_ms_max_manifest_rsync_uri,
            "ready_batch_child_discovery_ms_total": ready_batch_metrics.child_discovery_ms_total,
            "ready_batch_child_discovery_ms_max": ready_batch_metrics.child_discovery_ms_max,
            "ready_batch_prepare_ms_total": ready_batch_metrics.prepare_ms_total,
            "ready_batch_prepare_ms_max": ready_batch_metrics.prepare_ms_max,
            "ready_batch_direct_finalize_ms_total": ready_batch_metrics.direct_finalize_ms_total,
            "ready_batch_direct_finalize_ms_max": ready_batch_metrics.direct_finalize_ms_max,
            "ca_queue_len": ca_queue_len,
            "ready_queue_len": ready_queue_len,
            "ca_waiting_repo_identities": ca_waiting_repo_identities,
            "pending_roa_dispatch_len": pending_roa_dispatch_len,
            "inflight_publication_points_len": inflight_publication_points_len,
            "pending_finalization_len": pending_finalization_len,
            "finalize_inflight": finalize_inflight,
        }),
    );
}

fn compact_phase2_finished_result(
    mut result: PublicationPointRunResult,
    compact_audit: bool,
) -> FinishedPublicationPointResult {
    result.objects.audit.clear();
    result.objects.local_outputs_cache.clear();
    let cir_fresh_objects = if compact_audit && result.source == PublicationPointSource::Fresh {
        result.audit.objects.clone()
    } else {
        result.cir_fresh_objects
    };
    if compact_audit {
        result.audit.objects.clear();
        result.audit.warnings.clear();
    }
    FinishedPublicationPointResult::Ok {
        source: result.source,
        warnings: result.warnings,
        objects: result.objects,
        audit: result.audit,
        cir_fresh_objects,
        cir_cached_objects: result.cir_cached_objects,
    }
}

fn compact_phase2_finished_result_result(
    result: Result<PublicationPointRunResult, String>,
    compact_audit: bool,
) -> FinishedPublicationPointResult {
    match result {
        Ok(result) => compact_phase2_finished_result(result, compact_audit),
        Err(err) => FinishedPublicationPointResult::Err(err),
    }
}

pub fn run_tree_parallel_phase2_audit_multi_root(
    roots: Vec<CaInstanceHandle>,
    runner: &Rpkiv1PublicationPointRunner<'_>,
    config: &TreeRunConfig,
) -> Result<TreeRunAuditOutput, TreeRunError> {
    if runner.policy.signed_object_failure_policy == SignedObjectFailurePolicy::DropPublicationPoint
    {
        return run_tree_serial_audit_multi_root(roots, runner, config);
    }

    let Some(repo_runtime) = runner.repo_sync_runtime.as_ref() else {
        return run_tree_serial_audit_multi_root(roots, runner, config);
    };
    if runner.parallel_roa_worker_pool.is_none() {
        return run_tree_serial_audit_multi_root(roots, runner, config);
    }

    let mut next_id: u64 = 0;
    let mut ca_queue: VecDeque<QueuedCaInstance> = VecDeque::new();
    for root in roots {
        ca_queue.push_back(QueuedCaInstance {
            id: next_id,
            handle: root,
            parent_id: None,
            discovered_from: None,
        });
        next_id += 1;
    }

    let mut visited_manifest_uris: HashSet<String> = HashSet::new();
    let mut ca_waiting_repo_by_identity: HashMap<RepoIdentity, Vec<QueuedCaInstance>> =
        HashMap::new();
    let mut ready_queue: VecDeque<ReadyCaInstance> = VecDeque::new();
    let mut inflight_publication_points: HashMap<u64, InflightPublicationPoint> = HashMap::new();
    let mut pending_finalization: VecDeque<FinalizeTask> = VecDeque::new();
    let mut pending_roa_dispatch: VecDeque<OwnedRoaTask> = VecDeque::new();
    let mut finished: Vec<FinishedPublicationPoint> = Vec::new();
    let mut instances_started = 0usize;
    let phase2_config = runner.parallel_phase2_config.as_ref();
    let ready_batch_size = phase2_config
        .map(|cfg| cfg.ready_batch_size)
        .unwrap_or(256)
        .max(1);
    let ready_batch_wall_time_budget_ms = phase2_config
        .map(|cfg| cfg.ready_batch_wall_time_budget_ms)
        .unwrap_or(100)
        .max(1);
    let ready_batch_wall_time_budget = Duration::from_millis(ready_batch_wall_time_budget_ms);
    let object_result_drain_batch_size = phase2_config
        .map(|cfg| cfg.object_result_drain_batch_size)
        .unwrap_or(2048)
        .max(1);
    let publication_point_finalize_queue_capacity = phase2_config
        .map(|cfg| cfg.publication_point_finalize_queue_capacity)
        .unwrap_or(32768)
        .max(1);
    // Experimental ready-stage pool: `stage_workers == 0` keeps the inline
    // compute+apply staging path byte-for-byte; any positive value moves the
    // compute phase to a scoped worker pool.
    let stage_worker_count = phase2_config.map(|cfg| cfg.stage_workers).unwrap_or(0);
    let stage_queue_capacity = phase2_config
        .map(|cfg| cfg.worker_queue_capacity)
        .unwrap_or(256)
        .max(1);

    let (finalize_task_tx, finalize_task_rx) =
        mpsc::sync_channel::<FinalizeTask>(publication_point_finalize_queue_capacity);
    let (finalize_result_tx, finalize_result_rx) = mpsc::channel::<FinalizeWorkerResult>();
    let mut finalize_inflight = 0usize;

    return std::thread::scope(|scope| {
        let finalize_worker = scope.spawn(move || {
            run_finalize_worker(
                runner,
                finalize_task_rx,
                finalize_result_tx,
                config.compact_audit,
            )
        });

        // The stage pool borrows the runner like the finalize worker does; it
        // lives inside this scope and is dropped before the scope joins.
        let mut stage_pool = if stage_worker_count > 0 {
            Some(
                ReadyStagePool::new(
                    scope,
                    stage_worker_count,
                    stage_queue_capacity,
                    ReadyStageTaskExecutor { runner },
                )
                .map_err(TreeRunError::Runner)?,
            )
        } else {
            None
        };
        // Submitted-but-not-yet-drained stage tasks. The drain loop collects
        // every available result each turn, so `staging_inflight == 0` also
        // implies the stage result channel is empty.
        let mut staging_inflight = 0usize;

        let run_result: Result<(), TreeRunError> = (|| {
            loop {
                let control_loop_started = Instant::now();
                // With the stage pool enabled the batch wall clock covers the
                // whole turn (turn-head drain + dispatch + apply); the inline
                // path keeps its historical start point at the ready batch.
                let turn_stage_started = Instant::now();
                let mut ready_batch_metrics = ReadyStageBatchMetrics::default();
                let mut stage_drain_metrics = StageDrainMetrics::default();
                drain_finalize_results_with_progress(
                    &finalize_result_rx,
                    &mut finished,
                    &mut finalize_inflight,
                    pending_finalization.len(),
                    pending_roa_dispatch.len(),
                    inflight_publication_points.len(),
                )?;
                flush_pending_roa_dispatch_with_progress(
                    runner,
                    &mut pending_roa_dispatch,
                    &mut inflight_publication_points,
                    &pending_finalization,
                )?;
                drain_object_results_with_progress(
                    runner,
                    &mut inflight_publication_points,
                    &mut pending_finalization,
                    pending_roa_dispatch.len(),
                    object_result_drain_batch_size,
                )?;
                if let Some(pool) = stage_pool.as_ref() {
                    drain_stage_results(
                        pool,
                        runner,
                        &mut next_id,
                        &mut ca_queue,
                        &mut pending_roa_dispatch,
                        &mut inflight_publication_points,
                        &mut pending_finalization,
                        &mut finished,
                        &mut staging_inflight,
                        &mut ready_batch_metrics,
                        &mut stage_drain_metrics,
                        config,
                    )?;
                }
                submit_pending_finalization_with_progress(
                    &finalize_task_tx,
                    &mut pending_finalization,
                    &mut finalize_inflight,
                    publication_point_finalize_queue_capacity,
                    pending_roa_dispatch.len(),
                    inflight_publication_points.len(),
                )?;

                start_queued_ca_instances(
                    repo_runtime.as_ref(),
                    &mut ca_queue,
                    &mut ready_queue,
                    &mut ca_waiting_repo_by_identity,
                    &mut finished,
                    &mut visited_manifest_uris,
                    &mut instances_started,
                    config,
                );

                let repo_poll_timeout = event_poll_timeout(
                    &ca_queue,
                    &ready_queue,
                    &pending_roa_dispatch,
                    &inflight_publication_points,
                    &pending_finalization,
                    finalize_inflight,
                    staging_inflight,
                    instances_started,
                    config,
                );
                let repo_metrics = drain_repo_events(
                    repo_runtime.as_ref(),
                    &mut ca_waiting_repo_by_identity,
                    &mut ready_queue,
                    repo_poll_timeout,
                )?;
                if repo_metrics.event_count > 0 {
                    crate::progress_log::emit(
                        "phase2_repo_events_drain",
                        serde_json::json!({
                            "event_count": repo_metrics.event_count,
                            "completions": repo_metrics.completions,
                            "ready_enqueued": repo_metrics.ready_enqueued,
                            "duration_ms": repo_metrics.duration_ms,
                            "ready_queue_len": ready_queue.len(),
                            "ca_waiting_repo_identities": ca_waiting_repo_by_identity.len(),
                        }),
                    );
                }

                let ready_batch_started = Instant::now();
                let mut ready_time_budget_exhausted = false;
                let mut stage_dispatch_metrics = StageDispatchMetrics::default();
                if let Some(pool) = stage_pool.as_mut() {
                    // Pool path: the ready batch becomes a dispatch loop that
                    // submits compute tasks and applies whatever results are
                    // already available; backpressure requeues for next turn.
                    stage_dispatch_metrics = submit_ready_batch_to_stage_pool(
                        pool,
                        &mut ready_queue,
                        &mut staging_inflight,
                        ready_batch_size,
                        ready_batch_wall_time_budget,
                    )?;
                    ready_time_budget_exhausted = stage_dispatch_metrics.queue_full
                        || (!ready_queue.is_empty()
                            && ready_batch_started.elapsed() >= ready_batch_wall_time_budget);
                    drain_stage_results(
                        pool,
                        runner,
                        &mut next_id,
                        &mut ca_queue,
                        &mut pending_roa_dispatch,
                        &mut inflight_publication_points,
                        &mut pending_finalization,
                        &mut finished,
                        &mut staging_inflight,
                        &mut ready_batch_metrics,
                        &mut stage_drain_metrics,
                        config,
                    )?;
                } else {
                    while ready_batch_metrics.ready_count < ready_batch_size {
                        let Some(ready) = ready_queue.pop_front() else {
                            break;
                        };
                        let ready_queue_len_after_pop = ready_queue.len();
                        let (outcome, metrics) = compute_ready_publication_point_stage(
                            runner,
                            ready,
                            ready_queue_len_after_pop,
                        );
                        let metrics = apply_ready_publication_point_stage(
                            runner,
                            &mut next_id,
                            &mut ca_queue,
                            &mut pending_roa_dispatch,
                            &mut inflight_publication_points,
                            &mut pending_finalization,
                            &mut finished,
                            outcome,
                            metrics,
                            config,
                            config.compact_audit,
                        );
                        ready_batch_metrics.record(metrics);
                        if ready_batch_metrics.ready_count > 0
                            && ready_batch_started.elapsed() >= ready_batch_wall_time_budget
                        {
                            ready_time_budget_exhausted = !ready_queue.is_empty();
                            break;
                        }
                    }
                }
                if ready_batch_metrics.ready_count > 0 {
                    ready_batch_metrics.total_ms = if stage_pool.is_some() {
                        elapsed_ms(turn_stage_started)
                    } else {
                        elapsed_ms(ready_batch_started)
                    };
                    let ready_count_budget_exhausted = ready_batch_metrics.ready_count
                        >= ready_batch_size
                        && !ready_queue.is_empty();
                    ready_time_budget_exhausted = ready_time_budget_exhausted
                        || (!ready_queue.is_empty()
                            && ready_batch_metrics.total_ms >= ready_batch_wall_time_budget_ms);
                    emit_ready_queue_batch_progress(
                        &ready_batch_metrics,
                        ready_batch_size,
                        ready_batch_wall_time_budget_ms,
                        ready_queue.len(),
                        ready_count_budget_exhausted,
                        ready_time_budget_exhausted,
                        ca_queue.len(),
                        pending_roa_dispatch.len(),
                        inflight_publication_points.len(),
                        pending_finalization.len(),
                        finalize_inflight,
                    );
                    crate::progress_log::emit(
                        "phase2_ready_queue_stage_fresh_breakdown",
                        serde_json::json!({
                            "ready_count": ready_batch_metrics.ready_count,
                            "stage_fresh_ms_total": ready_batch_metrics.stage_fresh_ms_total,
                            "stage_fresh_ms_max": ready_batch_metrics.stage_fresh_ms_max,
                            "stage_fresh_ms_max_manifest_rsync_uri": ready_batch_metrics.stage_fresh_ms_max_manifest_rsync_uri,
                            "stage_fresh_ms_max_publication_point_rsync_uri": ready_batch_metrics.stage_fresh_ms_max_publication_point_rsync_uri,
                            "snapshot_prepare_ms_total": ready_batch_metrics.snapshot_prepare_ms_total,
                            "snapshot_prepare_ms_max": ready_batch_metrics.snapshot_prepare_ms_max,
                            "snapshot_current_index_lock_ms_total": ready_batch_metrics.snapshot_current_index_lock_ms_total,
                            "snapshot_current_index_lock_ms_max": ready_batch_metrics.snapshot_current_index_lock_ms_max,
                            "snapshot_manifest_load_ms_total": ready_batch_metrics.snapshot_manifest_load_ms_total,
                            "snapshot_manifest_load_ms_max": ready_batch_metrics.snapshot_manifest_load_ms_max,
                            "snapshot_manifest_index_lookup_ms_total": ready_batch_metrics.snapshot_manifest_index_lookup_ms_total,
                            "snapshot_manifest_index_lookup_ms_max": ready_batch_metrics.snapshot_manifest_index_lookup_ms_max,
                            "snapshot_manifest_blob_load_ms_total": ready_batch_metrics.snapshot_manifest_blob_load_ms_total,
                            "snapshot_manifest_blob_load_ms_max": ready_batch_metrics.snapshot_manifest_blob_load_ms_max,
                            "snapshot_manifest_decode_ms_total": ready_batch_metrics.snapshot_manifest_decode_ms_total,
                            "snapshot_manifest_decode_ms_max": ready_batch_metrics.snapshot_manifest_decode_ms_max,
                            "snapshot_replay_guard_ms_total": ready_batch_metrics.snapshot_replay_guard_ms_total,
                            "snapshot_replay_guard_ms_max": ready_batch_metrics.snapshot_replay_guard_ms_max,
                            "replay_meta_hit_count": ready_batch_metrics.replay_meta_hit_count,
                            "replay_meta_miss_count": ready_batch_metrics.replay_meta_miss_count,
                            "snapshot_manifest_entries_ms_total": ready_batch_metrics.snapshot_manifest_entries_ms_total,
                            "snapshot_manifest_entries_ms_max": ready_batch_metrics.snapshot_manifest_entries_ms_max,
                            "snapshot_pack_files_ms_total": ready_batch_metrics.snapshot_pack_files_ms_total,
                            "snapshot_pack_files_ms_max": ready_batch_metrics.snapshot_pack_files_ms_max,
                            "snapshot_pack_files_index_lookup_ms_total": ready_batch_metrics.snapshot_pack_files_index_lookup_ms_total,
                            "snapshot_pack_files_index_lookup_ms_max": ready_batch_metrics.snapshot_pack_files_index_lookup_ms_max,
                            "snapshot_pack_files_blob_load_ms_total": ready_batch_metrics.snapshot_pack_files_blob_load_ms_total,
                            "snapshot_pack_files_blob_load_ms_max": ready_batch_metrics.snapshot_pack_files_blob_load_ms_max,
                            "snapshot_ee_path_validate_ms_total": ready_batch_metrics.snapshot_ee_path_validate_ms_total,
                            "snapshot_ee_path_validate_ms_max": ready_batch_metrics.snapshot_ee_path_validate_ms_max,
                            "snapshot_manifest_file_count_total": ready_batch_metrics.snapshot_manifest_file_count_total,
                            "snapshot_manifest_file_count_max": ready_batch_metrics.snapshot_manifest_file_count_max,
                            "child_discovery_ms_total": ready_batch_metrics.child_discovery_ms_total,
                            "child_discovery_ms_max": ready_batch_metrics.child_discovery_ms_max,
                            "batch_duration_ms": ready_batch_metrics.total_ms,
                        }),
                    );
                }
                if stage_pool.is_some()
                    && (stage_dispatch_metrics.submitted > 0
                        || stage_drain_metrics.results_drained > 0
                        || stage_dispatch_metrics.queue_full)
                {
                    crate::progress_log::emit(
                        "phase2_stage_pool_stats",
                        serde_json::json!({
                            "stage_workers": stage_worker_count,
                            "submitted": stage_dispatch_metrics.submitted,
                            "results_drained": stage_drain_metrics.results_drained,
                            "queue_full": stage_dispatch_metrics.queue_full,
                            "staging_inflight": staging_inflight,
                            "ready_queue_len": ready_queue.len(),
                            "queue_wait_ms_total": stage_drain_metrics.queue_wait_ms_total,
                            "queue_wait_ms_max": stage_drain_metrics.queue_wait_ms_max,
                            "worker_ms_total": stage_drain_metrics.worker_ms_total,
                            "worker_ms_max": stage_drain_metrics.worker_ms_max,
                            "dispatch_duration_ms": stage_dispatch_metrics.duration_ms,
                            "drain_duration_ms": stage_drain_metrics.duration_ms,
                        }),
                    );
                }

                flush_pending_roa_dispatch_with_progress(
                    runner,
                    &mut pending_roa_dispatch,
                    &mut inflight_publication_points,
                    &pending_finalization,
                )?;
                drain_object_results_with_progress(
                    runner,
                    &mut inflight_publication_points,
                    &mut pending_finalization,
                    pending_roa_dispatch.len(),
                    object_result_drain_batch_size,
                )?;
                submit_pending_finalization_with_progress(
                    &finalize_task_tx,
                    &mut pending_finalization,
                    &mut finalize_inflight,
                    publication_point_finalize_queue_capacity,
                    pending_roa_dispatch.len(),
                    inflight_publication_points.len(),
                )?;
                drain_finalize_results_with_progress(
                    &finalize_result_rx,
                    &mut finished,
                    &mut finalize_inflight,
                    pending_finalization.len(),
                    pending_roa_dispatch.len(),
                    inflight_publication_points.len(),
                )?;

                emit_control_loop_slow(
                    elapsed_ms(control_loop_started),
                    repo_poll_timeout,
                    &repo_metrics,
                    &ready_batch_metrics,
                    ca_queue.len(),
                    ready_queue.len(),
                    ca_waiting_repo_by_identity.len(),
                    pending_roa_dispatch.len(),
                    inflight_publication_points.len(),
                    pending_finalization.len(),
                    finalize_inflight,
                );

                if is_complete(
                    &ca_queue,
                    &ready_queue,
                    &ca_waiting_repo_by_identity,
                    &pending_roa_dispatch,
                    &inflight_publication_points,
                    &pending_finalization,
                    finalize_inflight,
                    staging_inflight,
                    instances_started,
                    config,
                ) {
                    break;
                }
            }

            repo_runtime
                .reset_run_state()
                .map_err(TreeRunError::Runner)?;
            Ok(())
        })();

        drop(finalize_task_tx);
        // Dropping the pool closes the stage task queues so the scoped stage
        // workers exit before the scope joins them below.
        drop(stage_pool);
        let worker_result = finalize_worker
            .join()
            .map_err(|_| TreeRunError::Runner("phase2 finalize worker panicked".to_string()))?;
        run_result?;
        worker_result?;
        drain_finalize_results_with_progress(
            &finalize_result_rx,
            &mut finished,
            &mut finalize_inflight,
            pending_finalization.len(),
            pending_roa_dispatch.len(),
            inflight_publication_points.len(),
        )?;
        if finalize_inflight != 0 || !pending_finalization.is_empty() {
            return Err(TreeRunError::Runner(format!(
                "phase2 finalize worker stopped with pending work: queued={} inflight={}",
                pending_finalization.len(),
                finalize_inflight
            )));
        }
        Ok(build_tree_output(finished))
    });
}

fn emit_ready_queue_batch_progress(
    metrics: &ReadyStageBatchMetrics,
    ready_batch_size: usize,
    ready_batch_wall_time_budget_ms: u64,
    ready_queue_len_after_batch: usize,
    ready_count_budget_exhausted: bool,
    ready_time_budget_exhausted: bool,
    ca_queue_len_after_batch: usize,
    pending_roa_dispatch_len_after_batch: usize,
    inflight_publication_points_after_batch: usize,
    pending_finalization_len_after_batch: usize,
    finalize_inflight_after_batch: usize,
) {
    crate::progress_log::emit(
        "phase2_ready_queue_batch",
        serde_json::json!({
            "ready_count": metrics.ready_count,
            "fallback_count": metrics.fallback_count,
            "complete_count": metrics.complete_count,
            "staged_count": metrics.staged_count,
            "zero_task_count": metrics.zero_task_count,
            "error_count": metrics.error_count,
            "discovered_children": metrics.discovered_children,
            "locked_files": metrics.locked_files,
            "roa_tasks": metrics.roa_tasks,
            "aspa_objects": metrics.aspa_objects,
            "stage_fresh_ms_total": metrics.stage_fresh_ms_total,
            "stage_fresh_ms_max": metrics.stage_fresh_ms_max,
            "stage_fresh_ms_max_manifest_rsync_uri": metrics.stage_fresh_ms_max_manifest_rsync_uri,
            "stage_fresh_ms_max_publication_point_rsync_uri": metrics.stage_fresh_ms_max_publication_point_rsync_uri,
            "prepare_ms_total": metrics.prepare_ms_total,
            "prepare_ms_max": metrics.prepare_ms_max,
            "build_roa_tasks_ms_total": metrics.build_roa_tasks_ms_total,
            "build_roa_tasks_ms_max": metrics.build_roa_tasks_ms_max,
            "batch_duration_ms": metrics.total_ms,
            "ready_batch_size": ready_batch_size,
            "ready_batch_wall_time_budget_ms": ready_batch_wall_time_budget_ms,
            "ready_queue_len_after_batch": ready_queue_len_after_batch,
            "ready_queue_budget_exhausted": ready_queue_len_after_batch > 0,
            "ready_count_budget_exhausted": ready_count_budget_exhausted,
            "ready_time_budget_exhausted": ready_time_budget_exhausted,
            "ca_queue_len_after_batch": ca_queue_len_after_batch,
            "pending_roa_dispatch_len_after_batch": pending_roa_dispatch_len_after_batch,
            "inflight_publication_points_after_batch": inflight_publication_points_after_batch,
            "pending_finalization_len_after_batch": pending_finalization_len_after_batch,
            "finalize_inflight_after_batch": finalize_inflight_after_batch,
        }),
    );
    crate::progress_log::emit(
        "phase2_ready_queue_control_breakdown",
        serde_json::json!({
            "ready_count": metrics.ready_count,
            "ready_queue_wait_ms_total": metrics.ready_queue_wait_ms_total,
            "ready_queue_wait_ms_max": metrics.ready_queue_wait_ms_max,
            "child_enqueue_ms_total": metrics.child_enqueue_ms_total,
            "child_enqueue_ms_max": metrics.child_enqueue_ms_max,
            "roa_presence_scan_ms_total": metrics.roa_presence_scan_ms_total,
            "roa_presence_scan_ms_max": metrics.roa_presence_scan_ms_max,
            "roa_cache_view_ms_total": metrics.roa_cache_view_ms_total,
            "roa_cache_view_ms_max": metrics.roa_cache_view_ms_max,
            "direct_finalize_ms_total": metrics.direct_finalize_ms_total,
            "direct_finalize_ms_max": metrics.direct_finalize_ms_max,
            "fallback_full_run_ms_total": metrics.fallback_full_run_ms_total,
            "fallback_full_run_ms_max": metrics.fallback_full_run_ms_max,
            "batch_duration_ms": metrics.total_ms,
        }),
    );
}

fn can_start_more(instances_started: usize, config: &TreeRunConfig) -> bool {
    config
        .max_instances
        .map(|max| instances_started < max)
        .unwrap_or(true)
}

fn start_queued_ca_instances(
    repo_runtime: &dyn crate::parallel::repo_runtime::RepoSyncRuntime,
    ca_queue: &mut VecDeque<QueuedCaInstance>,
    ready_queue: &mut VecDeque<ReadyCaInstance>,
    ca_waiting_repo_by_identity: &mut HashMap<RepoIdentity, Vec<QueuedCaInstance>>,
    finished: &mut Vec<FinishedPublicationPoint>,
    visited_manifest_uris: &mut HashSet<String>,
    instances_started: &mut usize,
    config: &TreeRunConfig,
) {
    while can_start_more(*instances_started, config) {
        let Some(node) = ca_queue.pop_front() else {
            break;
        };
        if !visited_manifest_uris.insert(node.handle.manifest_rsync_uri.clone()) {
            continue;
        }
        if !ca_depth_is_allowed(config, node.handle.depth) {
            continue;
        }
        *instances_started += 1;
        match repo_runtime.request_publication_point_repo(&node.handle, 0) {
            Ok(RepoSyncRequestStatus::Ready { mut outcome, .. }) => {
                // Ready here means this CA is reusing repo work that has already completed
                // (often due to child prefetch). Do not add the transport duration again.
                outcome.repo_sync_duration_ms = 0;
                ready_queue.push_back(ReadyCaInstance {
                    node,
                    repo_outcome: outcome,
                    ready_enqueued_at: Instant::now(),
                });
            }
            Ok(RepoSyncRequestStatus::Pending { identity, .. }) => {
                ca_waiting_repo_by_identity
                    .entry(identity)
                    .or_default()
                    .push(node);
            }
            Err(err) => {
                finished.push(FinishedPublicationPoint {
                    node: FinishedPublicationPointNode::from_queued(node),
                    result: FinishedPublicationPointResult::Err(err),
                });
            }
        }
    }
}

fn compute_ready_publication_point_stage(
    runner: &Rpkiv1PublicationPointRunner<'_>,
    ready: ReadyCaInstance,
    ready_queue_len_after_pop: usize,
) -> (StageOutcome, ReadyStageMetrics) {
    let publication_point_started = Instant::now();
    let ready_queue_wait_ms = publication_point_started
        .saturating_duration_since(ready.ready_enqueued_at)
        .as_millis() as u64;
    let mut metrics = ReadyStageMetrics {
        ready_count: 1,
        manifest_rsync_uri: Some(ready.node.handle.manifest_rsync_uri.clone()),
        publication_point_rsync_uri: Some(ready.node.handle.publication_point_rsync_uri.clone()),
        ready_queue_wait_ms,
        ready_queue_len_after_pop,
        ..ReadyStageMetrics::default()
    };
    let mut warnings = ready.repo_outcome.warnings.clone();
    let repo_outcome = ready.repo_outcome.clone();
    if let Some(result) = runner.observe_or_reuse_publication_point_cache(
        &ready.node.handle,
        repo_outcome.repo_sync_source.as_deref(),
        repo_outcome.repo_sync_phase.as_deref(),
        repo_outcome.repo_sync_duration_ms,
        repo_outcome.repo_sync_err.as_deref(),
        &warnings,
    ) {
        metrics.complete_count = 1;
        metrics.discovered_children = result.discovered_children.len();
        return (
            StageOutcome::CacheHit(Box::new(CacheHitOutcome {
                ready,
                publication_point_started,
                result,
            })),
            metrics,
        );
    }

    let stage_fresh_started = Instant::now();
    let stage = runner.stage_fresh_publication_point_after_repo_ready(
        &ready.node.handle,
        repo_outcome.repo_sync_ok,
        repo_outcome.repo_sync_err.as_deref(),
    );
    metrics.stage_fresh_ms = elapsed_ms(stage_fresh_started);

    let fresh_stage = match stage {
        Ok(stage) => stage,
        Err(err) => {
            if metrics.stage_fresh_ms >= crate::progress_log::stage_fresh_slow_threshold_ms() {
                crate::progress_log::emit(
                    "phase2_stage_fresh_slow",
                    serde_json::json!({
                        "manifest_rsync_uri": ready.node.handle.manifest_rsync_uri.as_str(),
                        "publication_point_rsync_uri": ready.node.handle.publication_point_rsync_uri.as_str(),
                        "status": "error",
                        "error": err.error.to_string(),
                        "stage_fresh_ms": metrics.stage_fresh_ms,
                        "snapshot_prepare_ms": err.snapshot_prepare_ms,
                        "repo_sync_source": repo_outcome.repo_sync_source.as_deref(),
                        "repo_sync_phase": repo_outcome.repo_sync_phase.as_deref(),
                        "repo_sync_duration_ms": repo_outcome.repo_sync_duration_ms,
                    }),
                );
            }
            // The blocking `run_publication_point` fallback stays on the control
            // thread; it is executed by the apply phase for this outcome.
            return (
                StageOutcome::FreshError(Box::new(FreshErrorOutcome {
                    ready,
                    publication_point_started,
                })),
                metrics,
            );
        }
    };
    metrics.snapshot_prepare_ms = fresh_stage.snapshot_prepare_ms;
    metrics.snapshot_current_index_lock_ms =
        fresh_stage.snapshot_prepare_timing.current_index_lock_ms;
    metrics.snapshot_manifest_load_ms = fresh_stage.snapshot_prepare_timing.manifest_load_ms;
    metrics.snapshot_manifest_index_lookup_ms =
        fresh_stage.snapshot_prepare_timing.manifest_index_lookup_ms;
    metrics.snapshot_manifest_blob_load_ms =
        fresh_stage.snapshot_prepare_timing.manifest_blob_load_ms;
    metrics.snapshot_manifest_decode_ms = fresh_stage.snapshot_prepare_timing.manifest_decode_ms;
    metrics.snapshot_replay_guard_ms = fresh_stage.snapshot_prepare_timing.replay_guard_ms;
    metrics.replay_meta_hit_count = fresh_stage.snapshot_prepare_timing.replay_meta_hit as usize;
    metrics.replay_meta_miss_count = fresh_stage.snapshot_prepare_timing.replay_meta_miss as usize;
    metrics.snapshot_manifest_entries_ms = fresh_stage.snapshot_prepare_timing.manifest_entries_ms;
    metrics.snapshot_pack_files_ms = fresh_stage.snapshot_prepare_timing.pack_files_ms;
    metrics.snapshot_pack_files_index_lookup_ms = fresh_stage
        .snapshot_prepare_timing
        .pack_files_index_lookup_ms;
    metrics.snapshot_pack_files_blob_load_ms =
        fresh_stage.snapshot_prepare_timing.pack_files_blob_load_ms;
    metrics.snapshot_ee_path_validate_ms = fresh_stage.snapshot_prepare_timing.ee_path_validate_ms;
    metrics.snapshot_manifest_file_count = fresh_stage.snapshot_prepare_timing.manifest_file_count;
    metrics.child_discovery_ms = fresh_stage.child_discovery_ms;
    if metrics.stage_fresh_ms >= crate::progress_log::stage_fresh_slow_threshold_ms() {
        crate::progress_log::emit(
            "phase2_stage_fresh_slow",
            serde_json::json!({
                "manifest_rsync_uri": ready.node.handle.manifest_rsync_uri.as_str(),
                "publication_point_rsync_uri": ready.node.handle.publication_point_rsync_uri.as_str(),
                "status": "ok",
                "stage_fresh_ms": metrics.stage_fresh_ms,
                "snapshot_prepare_ms": fresh_stage.snapshot_prepare_ms,
                "snapshot_current_index_lock_ms": fresh_stage.snapshot_prepare_timing.current_index_lock_ms,
                "snapshot_manifest_load_ms": fresh_stage.snapshot_prepare_timing.manifest_load_ms,
                "snapshot_manifest_index_lookup_ms": fresh_stage.snapshot_prepare_timing.manifest_index_lookup_ms,
                "snapshot_manifest_blob_load_ms": fresh_stage.snapshot_prepare_timing.manifest_blob_load_ms,
                "snapshot_manifest_decode_ms": fresh_stage.snapshot_prepare_timing.manifest_decode_ms,
                "snapshot_replay_guard_ms": fresh_stage.snapshot_prepare_timing.replay_guard_ms,
                "replay_meta_hit": fresh_stage.snapshot_prepare_timing.replay_meta_hit,
                "replay_meta_miss": fresh_stage.snapshot_prepare_timing.replay_meta_miss,
                "snapshot_manifest_entries_ms": fresh_stage.snapshot_prepare_timing.manifest_entries_ms,
                "snapshot_pack_files_ms": fresh_stage.snapshot_prepare_timing.pack_files_ms,
                "snapshot_pack_files_index_lookup_ms": fresh_stage.snapshot_prepare_timing.pack_files_index_lookup_ms,
                "snapshot_pack_files_blob_load_ms": fresh_stage.snapshot_prepare_timing.pack_files_blob_load_ms,
                "snapshot_ee_path_validate_ms": fresh_stage.snapshot_prepare_timing.ee_path_validate_ms,
                "snapshot_manifest_file_count": fresh_stage.snapshot_prepare_timing.manifest_file_count,
                "child_discovery_ms": fresh_stage.child_discovery_ms,
                "child_count": fresh_stage.discovered_children.len(),
                "repo_sync_source": repo_outcome.repo_sync_source.as_deref(),
                "repo_sync_phase": repo_outcome.repo_sync_phase.as_deref(),
                "repo_sync_duration_ms": repo_outcome.repo_sync_duration_ms,
            }),
        );
    }
    warnings.extend(fresh_stage.warnings.clone());

    metrics.discovered_children = fresh_stage.discovered_children.len();

    let prepare_started = Instant::now();
    let roa_presence_scan_started = Instant::now();
    let has_roa = fresh_stage
        .fresh_point
        .files()
        .iter()
        .any(|file| file.rsync_uri.ends_with(".roa"));
    metrics.roa_presence_scan_ms = elapsed_ms(roa_presence_scan_started);
    if runner.enable_roa_validation_cache {
        if let Some(timing) = runner.timing.as_ref() {
            if has_roa {
                timing.record_count("roa_validation_cache_roa_candidate_publication_points", 1);
            } else {
                timing.record_count("roa_validation_cache_skipped_no_roa_publication_points", 1);
            }
        }
    }
    let roa_cache_view = if has_roa {
        let roa_cache_view_started = Instant::now();
        let view = runner
            .roa_validation_cache_view_for_fresh_point(&fresh_stage.fresh_point.manifest_rsync_uri);
        metrics.roa_cache_view_ms = elapsed_ms(roa_cache_view_started);
        view
    } else {
        None
    };
    let roa_cache = if runner.enable_roa_validation_cache && has_roa {
        RoaValidationCacheInput::enabled_with_context(
            roa_cache_view.as_ref(),
            crate::validation::tree_runner::ca_validation_context_digest_for_ca(&ready.node.handle),
            crate::validation::tree_runner::publication_point_cache_policy_fingerprint(
                runner.policy,
            ),
        )
    } else {
        RoaValidationCacheInput::disabled()
    };
    match prepare_publication_point_for_parallel_roa_with_cache(
        ready.node.id,
        &fresh_stage.fresh_point,
        runner.policy,
        fresh_stage.issuer_ca_der.as_ref(),
        ready.node.handle.ca_certificate_rsync_uri.as_deref(),
        ready.node.handle.effective_ip_resources.as_ref(),
        ready.node.handle.effective_as_resources.as_ref(),
        runner.validation_time,
        runner.persist_vcir,
        roa_cache,
    ) {
        ParallelObjectsPrepare::Complete(mut objects) => {
            metrics.prepare_ms = elapsed_ms(prepare_started);
            runner.record_publication_point_step_ms(
                &ready.node.handle.manifest_rsync_uri,
                "fresh_objects_prepare",
                metrics.prepare_ms,
            );
            metrics.complete_count = 1;
            metrics.roa_tasks = objects.stats.roa_total;
            metrics.aspa_objects = objects.stats.aspa_total;
            objects
                .router_keys
                .extend(fresh_stage.discovered_router_keys.clone());
            objects.local_outputs_cache.extend(
                crate::validation::tree_runner::build_router_key_local_outputs(
                    &ready.node.handle,
                    &objects.router_keys,
                ),
            );
            (
                StageOutcome::Complete(Box::new(CompleteOutcome {
                    ready,
                    publication_point_started,
                    fresh_stage,
                    warnings,
                    objects,
                })),
                metrics,
            )
        }
        ParallelObjectsPrepare::Staged(objects_stage) => {
            metrics.prepare_ms = elapsed_ms(prepare_started);
            runner.record_publication_point_step_ms(
                &ready.node.handle.manifest_rsync_uri,
                "fresh_objects_prepare",
                metrics.prepare_ms,
            );
            metrics.staged_count = 1;
            metrics.locked_files = objects_stage.locked_file_count();
            metrics.aspa_objects = objects_stage.aspa_task_count();
            let task_count = objects_stage.roa_task_count();
            metrics.roa_tasks = task_count;
            let outcome = StagedOutcome {
                ready,
                publication_point_started,
                fresh_stage,
                warnings,
                objects_stage,
            };
            if task_count == 0 {
                metrics.zero_task_count = 1;
                (StageOutcome::ZeroTask(Box::new(outcome)), metrics)
            } else {
                (StageOutcome::Fresh(Box::new(outcome)), metrics)
            }
        }
    }
}

fn apply_ready_publication_point_stage(
    runner: &Rpkiv1PublicationPointRunner<'_>,
    next_id: &mut u64,
    ca_queue: &mut VecDeque<QueuedCaInstance>,
    pending_roa_dispatch: &mut VecDeque<OwnedRoaTask>,
    inflight_publication_points: &mut HashMap<u64, InflightPublicationPoint>,
    pending_finalization: &mut VecDeque<FinalizeTask>,
    finished: &mut Vec<FinishedPublicationPoint>,
    outcome: StageOutcome,
    mut metrics: ReadyStageMetrics,
    config: &TreeRunConfig,
    compact_audit: bool,
) -> ReadyStageMetrics {
    match outcome {
        StageOutcome::CacheHit(outcome) => {
            let CacheHitOutcome {
                ready,
                publication_point_started,
                result,
            } = *outcome;
            let repo_outcome = ready.repo_outcome.clone();
            let child_enqueue_started = Instant::now();
            enqueue_discovered_children(
                runner,
                next_id,
                ca_queue,
                &ready.node,
                config,
                result.discovered_children.clone(),
            );
            metrics.child_enqueue_ms = elapsed_ms(child_enqueue_started);
            runner.record_publication_point_step_ms(
                metrics.manifest_rsync_uri.as_deref().unwrap_or_default(),
                "publication_point_cache_child_enqueue",
                metrics.child_enqueue_ms,
            );
            finished.push(FinishedPublicationPoint {
                node: FinishedPublicationPointNode::from_queued(ready.node),
                result: compact_phase2_finished_result(result, compact_audit),
            });
            metrics.total_ms = elapsed_ms(publication_point_started);
            emit_ready_publication_point_control_slow(
                metrics.manifest_rsync_uri.as_deref().unwrap_or_default(),
                metrics
                    .publication_point_rsync_uri
                    .as_deref()
                    .unwrap_or_default(),
                &repo_outcome,
                &metrics,
                "publication_point_cache",
                false,
            );
            metrics
        }
        StageOutcome::FreshError(outcome) => {
            let FreshErrorOutcome {
                ready,
                publication_point_started,
            } = *outcome;
            let repo_outcome = ready.repo_outcome.clone();
            metrics.fallback_count = 1;
            let fallback_started = Instant::now();
            let fallback = runner.run_publication_point(&ready.node.handle);
            metrics.fallback_full_run_ms = elapsed_ms(fallback_started);
            if let Ok(result) = fallback.as_ref() {
                metrics.discovered_children = result.discovered_children.len();
                let child_enqueue_started = Instant::now();
                enqueue_discovered_children(
                    runner,
                    next_id,
                    ca_queue,
                    &ready.node,
                    config,
                    result.discovered_children.clone(),
                );
                metrics.child_enqueue_ms = elapsed_ms(child_enqueue_started);
            }
            finished.push(FinishedPublicationPoint {
                node: FinishedPublicationPointNode::from_queued(ready.node),
                result: compact_phase2_finished_result_result(fallback, compact_audit),
            });
            metrics.total_ms = elapsed_ms(publication_point_started);
            emit_ready_publication_point_control_slow(
                metrics.manifest_rsync_uri.as_deref().unwrap_or_default(),
                metrics
                    .publication_point_rsync_uri
                    .as_deref()
                    .unwrap_or_default(),
                &repo_outcome,
                &metrics,
                "fallback",
                true,
            );
            metrics
        }
        StageOutcome::Complete(outcome) => {
            let CompleteOutcome {
                ready,
                publication_point_started,
                fresh_stage,
                warnings,
                objects,
            } = *outcome;
            let repo_outcome = ready.repo_outcome.clone();
            let child_enqueue_started = Instant::now();
            enqueue_discovered_children(
                runner,
                next_id,
                ca_queue,
                &ready.node,
                config,
                fresh_stage.discovered_children.clone(),
            );
            metrics.child_enqueue_ms = elapsed_ms(child_enqueue_started);
            // The finalize no longer runs synchronously here: the publication
            // point is queued for the shared finalize worker through the same
            // pending_finalization path (queue capacity backpressure and
            // finalize_inflight accounting included) as zero-task staging. The
            // per-publication-point total timing is recorded by the finalize
            // worker, exactly like the zero-task and staged paths.
            let direct_finalize_started = Instant::now();
            pending_finalization.push_back(FinalizeTask {
                state: InflightPublicationPoint {
                    node: ready.node,
                    fresh_stage,
                    objects_prepare: ParallelObjectsPrepare::Complete(objects),
                    repo_outcome: repo_outcome.clone(),
                    warnings,
                    started_at: publication_point_started,
                    objects_started_at: Instant::now(),
                    task_count: 0,
                    tasks_submitted: 0,
                    first_task_submitted_at: None,
                    last_task_submitted_at: None,
                    first_result_at: None,
                    last_result_at: None,
                    worker_ms_total: 0,
                    worker_ms_max: 0,
                    queue_wait_ms_total: 0,
                    queue_wait_ms_max: 0,
                    finalize_enqueued_at: Some(Instant::now()),
                    results: Vec::new(),
                },
            });
            metrics.direct_finalize_ms = elapsed_ms(direct_finalize_started);
            runner.record_publication_point_step_ms(
                &metrics.manifest_rsync_uri.clone().unwrap_or_default(),
                "fresh_direct_finalize",
                metrics.direct_finalize_ms,
            );
            metrics.total_ms = elapsed_ms(publication_point_started);
            emit_ready_publication_point_control_slow(
                metrics.manifest_rsync_uri.as_deref().unwrap_or_default(),
                metrics
                    .publication_point_rsync_uri
                    .as_deref()
                    .unwrap_or_default(),
                &repo_outcome,
                &metrics,
                "complete",
                false,
            );
            metrics
        }
        StageOutcome::ZeroTask(outcome) => {
            let StagedOutcome {
                ready,
                publication_point_started,
                fresh_stage,
                warnings,
                objects_stage,
            } = *outcome;
            let repo_outcome = ready.repo_outcome.clone();
            let child_enqueue_started = Instant::now();
            enqueue_discovered_children(
                runner,
                next_id,
                ca_queue,
                &ready.node,
                config,
                fresh_stage.discovered_children.clone(),
            );
            metrics.child_enqueue_ms = elapsed_ms(child_enqueue_started);
            let build_tasks_started = Instant::now();
            objects_stage.append_roa_tasks_to(pending_roa_dispatch);
            metrics.build_roa_tasks_ms = elapsed_ms(build_tasks_started);
            runner.record_publication_point_step_ms(
                &ready.node.handle.manifest_rsync_uri,
                "fresh_build_roa_tasks",
                metrics.build_roa_tasks_ms,
            );
            let task_count = objects_stage.roa_task_count();
            pending_finalization.push_back(FinalizeTask {
                state: InflightPublicationPoint {
                    node: ready.node,
                    fresh_stage,
                    objects_prepare: ParallelObjectsPrepare::Staged(objects_stage),
                    repo_outcome: repo_outcome.clone(),
                    warnings,
                    started_at: publication_point_started,
                    objects_started_at: Instant::now(),
                    task_count,
                    tasks_submitted: 0,
                    first_task_submitted_at: None,
                    last_task_submitted_at: None,
                    first_result_at: None,
                    last_result_at: None,
                    worker_ms_total: 0,
                    worker_ms_max: 0,
                    queue_wait_ms_total: 0,
                    queue_wait_ms_max: 0,
                    finalize_enqueued_at: Some(Instant::now()),
                    results: Vec::new(),
                },
            });
            metrics.total_ms = elapsed_ms(publication_point_started);
            emit_ready_publication_point_control_slow(
                metrics.manifest_rsync_uri.as_deref().unwrap_or_default(),
                metrics
                    .publication_point_rsync_uri
                    .as_deref()
                    .unwrap_or_default(),
                &repo_outcome,
                &metrics,
                "zero_task",
                false,
            );
            metrics
        }
        StageOutcome::Fresh(outcome) => {
            let StagedOutcome {
                ready,
                publication_point_started,
                fresh_stage,
                warnings,
                objects_stage,
            } = *outcome;
            let repo_outcome = ready.repo_outcome.clone();
            let child_enqueue_started = Instant::now();
            enqueue_discovered_children(
                runner,
                next_id,
                ca_queue,
                &ready.node,
                config,
                fresh_stage.discovered_children.clone(),
            );
            metrics.child_enqueue_ms = elapsed_ms(child_enqueue_started);
            let build_tasks_started = Instant::now();
            objects_stage.append_roa_tasks_to(pending_roa_dispatch);
            metrics.build_roa_tasks_ms = elapsed_ms(build_tasks_started);
            runner.record_publication_point_step_ms(
                &ready.node.handle.manifest_rsync_uri,
                "fresh_build_roa_tasks",
                metrics.build_roa_tasks_ms,
            );
            let task_count = objects_stage.roa_task_count();
            inflight_publication_points.insert(
                ready.node.id,
                InflightPublicationPoint {
                    node: ready.node,
                    fresh_stage,
                    objects_prepare: ParallelObjectsPrepare::Staged(objects_stage),
                    repo_outcome: repo_outcome.clone(),
                    warnings,
                    started_at: publication_point_started,
                    objects_started_at: Instant::now(),
                    task_count,
                    tasks_submitted: 0,
                    first_task_submitted_at: None,
                    last_task_submitted_at: None,
                    first_result_at: None,
                    last_result_at: None,
                    worker_ms_total: 0,
                    worker_ms_max: 0,
                    queue_wait_ms_total: 0,
                    queue_wait_ms_max: 0,
                    finalize_enqueued_at: None,
                    results: Vec::with_capacity(task_count),
                },
            );
            metrics.total_ms = elapsed_ms(publication_point_started);
            emit_ready_publication_point_control_slow(
                metrics.manifest_rsync_uri.as_deref().unwrap_or_default(),
                metrics
                    .publication_point_rsync_uri
                    .as_deref()
                    .unwrap_or_default(),
                &repo_outcome,
                &metrics,
                "staged",
                false,
            );
            metrics
        }
    }
}

fn emit_ready_publication_point_control_slow(
    manifest_rsync_uri: &str,
    publication_point_rsync_uri: &str,
    repo_outcome: &RepoSyncRuntimeOutcome,
    metrics: &ReadyStageMetrics,
    status: &str,
    force_error_path: bool,
) {
    let threshold_ms = crate::progress_log::pp_control_slow_threshold_ms();
    if !force_error_path && metrics.total_ms < threshold_ms {
        return;
    }
    crate::progress_log::emit(
        "phase2_ready_publication_point_control_slow",
        serde_json::json!({
            "manifest_rsync_uri": manifest_rsync_uri,
            "publication_point_rsync_uri": publication_point_rsync_uri,
            "status": status,
            "repo_sync_source": repo_outcome.repo_sync_source.as_deref(),
            "repo_sync_phase": repo_outcome.repo_sync_phase.as_deref(),
            "repo_sync_duration_ms": repo_outcome.repo_sync_duration_ms,
            "repo_sync_ok": repo_outcome.repo_sync_ok,
            "repo_sync_err": repo_outcome.repo_sync_err.as_deref(),
            "ready_queue_wait_ms": metrics.ready_queue_wait_ms,
            "ready_queue_len_after_pop": metrics.ready_queue_len_after_pop,
            "stage_fresh_ms": metrics.stage_fresh_ms,
            "child_discovery_ms": metrics.child_discovery_ms,
            "child_enqueue_ms": metrics.child_enqueue_ms,
            "discovered_children": metrics.discovered_children,
            "roa_presence_scan_ms": metrics.roa_presence_scan_ms,
            "roa_cache_view_ms": metrics.roa_cache_view_ms,
            "prepare_ms": metrics.prepare_ms,
            "build_roa_tasks_ms": metrics.build_roa_tasks_ms,
            "direct_finalize_ms": metrics.direct_finalize_ms,
            "fallback_full_run_ms": metrics.fallback_full_run_ms,
            "locked_files": metrics.locked_files,
            "roa_tasks": metrics.roa_tasks,
            "aspa_objects": metrics.aspa_objects,
            "complete_count": metrics.complete_count,
            "staged_count": metrics.staged_count,
            "zero_task_count": metrics.zero_task_count,
            "fallback_count": metrics.fallback_count,
            "total_ms": metrics.total_ms,
            "slow_threshold_ms": threshold_ms,
        }),
    );
    crate::progress_log::emit(
        "phase2_ready_publication_point_control_snapshot_breakdown",
        serde_json::json!({
            "manifest_rsync_uri": manifest_rsync_uri,
            "publication_point_rsync_uri": publication_point_rsync_uri,
            "status": status,
            "snapshot_prepare_ms": metrics.snapshot_prepare_ms,
            "snapshot_current_index_lock_ms": metrics.snapshot_current_index_lock_ms,
            "snapshot_manifest_load_ms": metrics.snapshot_manifest_load_ms,
            "snapshot_manifest_index_lookup_ms": metrics.snapshot_manifest_index_lookup_ms,
            "snapshot_manifest_blob_load_ms": metrics.snapshot_manifest_blob_load_ms,
            "snapshot_manifest_decode_ms": metrics.snapshot_manifest_decode_ms,
            "snapshot_replay_guard_ms": metrics.snapshot_replay_guard_ms,
            "replay_meta_hit_count": metrics.replay_meta_hit_count,
            "replay_meta_miss_count": metrics.replay_meta_miss_count,
            "snapshot_manifest_entries_ms": metrics.snapshot_manifest_entries_ms,
            "snapshot_pack_files_ms": metrics.snapshot_pack_files_ms,
            "snapshot_pack_files_index_lookup_ms": metrics.snapshot_pack_files_index_lookup_ms,
            "snapshot_pack_files_blob_load_ms": metrics.snapshot_pack_files_blob_load_ms,
            "snapshot_ee_path_validate_ms": metrics.snapshot_ee_path_validate_ms,
            "snapshot_manifest_file_count": metrics.snapshot_manifest_file_count,
            "total_ms": metrics.total_ms,
            "slow_threshold_ms": threshold_ms,
        }),
    );
}

fn enqueue_discovered_children(
    runner: &Rpkiv1PublicationPointRunner<'_>,
    next_id: &mut u64,
    ca_queue: &mut VecDeque<QueuedCaInstance>,
    parent: &QueuedCaInstance,
    config: &TreeRunConfig,
    mut children: Vec<DiscoveredChildCaInstance>,
) {
    let Some(child_depth) = next_allowed_ca_depth(config, parent.handle.depth) else {
        return;
    };

    children.sort_by(|a, b| {
        a.handle
            .manifest_rsync_uri
            .cmp(&b.handle.manifest_rsync_uri)
            .then_with(|| {
                a.discovered_from
                    .child_ca_certificate_rsync_uri
                    .cmp(&b.discovered_from.child_ca_certificate_rsync_uri)
            })
    });
    if let Some(runtime) = runner.repo_sync_runtime.as_ref() {
        let _ = runtime.prefetch_discovered_children(&children);
    }
    for child in children {
        let mut handle = child.handle.with_depth(child_depth);
        handle.parent_manifest_rsync_uri = Some(parent.handle.manifest_rsync_uri.clone());
        ca_queue.push_back(QueuedCaInstance {
            id: *next_id,
            handle,
            parent_id: Some(parent.id),
            discovered_from: Some(child.discovered_from),
        });
        *next_id += 1;
    }
}

fn finalize_metrics_from_output(
    output: &FreshPublicationPointFinalizeOutput,
    reduce_ms: u64,
    finalize_ms: u64,
    finalize_queue_wait_ms: Option<u64>,
    finalize_worker_ms: u64,
    locked_files: usize,
) -> FinalizePublicationPointMetrics {
    FinalizePublicationPointMetrics {
        reduce_ms,
        finalize_ms,
        finalize_queue_wait_ms,
        finalize_worker_ms,
        snapshot_pack_ms: output.snapshot_pack_ms,
        persist_vcir_ms: output.persist_vcir_ms,
        persist_build_vcir_ms: output.persist_vcir_timing.build_vcir_ms,
        persist_replace_vcir_ms: output.persist_vcir_timing.replace_vcir_ms,
        persist_replace_breakdown: output.persist_vcir_timing.replace_vcir.clone(),
        ccr_projection_build_ms: output.ccr_projection_build_ms,
        ccr_append_ms: output.ccr_append_ms,
        audit_build_ms: output.audit_build_ms,
        locked_files,
        child_count: output.result.discovered_children.len(),
        warning_count: output.result.warnings.len(),
        vrp_count: output.result.objects.vrps.len(),
        vap_count: output.result.objects.aspas.len(),
        router_key_count: output.result.objects.router_keys.len(),
        audit_object_count: output.result.audit.objects.len(),
    }
}

fn emit_finalize_breakdown(
    event_name: &str,
    manifest_rsync_uri: &str,
    publication_point_rsync_uri: &str,
    metrics: &FinalizePublicationPointMetrics,
) {
    crate::progress_log::emit(
        event_name,
        serde_json::json!({
            "manifest_rsync_uri": manifest_rsync_uri,
            "publication_point_rsync_uri": publication_point_rsync_uri,
            "reduce_ms": metrics.reduce_ms,
            "finalize_ms": metrics.finalize_ms,
            "finalize_queue_wait_ms": metrics.finalize_queue_wait_ms,
            "finalize_worker_ms": metrics.finalize_worker_ms,
            "snapshot_pack_ms": metrics.snapshot_pack_ms,
            "persist_vcir_ms": metrics.persist_vcir_ms,
            "persist_build_vcir_ms": metrics.persist_build_vcir_ms,
            "persist_replace_vcir_ms": metrics.persist_replace_vcir_ms,
            "persist_replace_breakdown": &metrics.persist_replace_breakdown,
            "ccr_projection_build_ms": metrics.ccr_projection_build_ms,
            "ccr_append_ms": metrics.ccr_append_ms,
            "audit_build_ms": metrics.audit_build_ms,
            "locked_files": metrics.locked_files,
            "child_count": metrics.child_count,
            "warning_count": metrics.warning_count,
            "vrp_count": metrics.vrp_count,
            "vap_count": metrics.vap_count,
            "router_key_count": metrics.router_key_count,
            "audit_object_count": metrics.audit_object_count,
        }),
    );
}

fn flush_pending_roa_dispatch(
    runner: &Rpkiv1PublicationPointRunner<'_>,
    pending_roa_dispatch: &mut VecDeque<OwnedRoaTask>,
    inflight_publication_points: &mut HashMap<u64, InflightPublicationPoint>,
) -> Result<RoaDispatchMetrics, TreeRunError> {
    let started = Instant::now();
    let mut metrics = RoaDispatchMetrics::default();
    let Some(pool) = runner.parallel_roa_worker_pool.as_ref() else {
        return Ok(metrics);
    };
    while let Some(mut task) = pending_roa_dispatch.pop_front() {
        metrics.attempted += 1;
        let pp_id = task.publication_point_id;
        task.submitted_at = Some(Instant::now());
        match pool.try_submit_round_robin(task) {
            Ok(_) => {
                metrics.submitted += 1;
                if let Some(state) = inflight_publication_points.get_mut(&pp_id) {
                    let now = Instant::now();
                    state.tasks_submitted += 1;
                    if state.first_task_submitted_at.is_none() {
                        state.first_task_submitted_at = Some(now);
                    }
                    state.last_task_submitted_at = Some(now);
                }
            }
            Err(ObjectWorkerSubmitError::QueueFull { task, .. }) => {
                pending_roa_dispatch.push_front(task);
                metrics.queue_full = true;
                break;
            }
            Err(ObjectWorkerSubmitError::Disconnected { .. }) => {
                return Err(TreeRunError::Runner(
                    "parallel ROA worker queue disconnected".to_string(),
                ));
            }
        }
    }
    metrics.pending_remaining = pending_roa_dispatch.len();
    metrics.duration_ms = elapsed_ms(started);
    Ok(metrics)
}

fn flush_pending_roa_dispatch_with_progress(
    runner: &Rpkiv1PublicationPointRunner<'_>,
    pending_roa_dispatch: &mut VecDeque<OwnedRoaTask>,
    inflight_publication_points: &mut HashMap<u64, InflightPublicationPoint>,
    pending_finalization: &VecDeque<FinalizeTask>,
) -> Result<(), TreeRunError> {
    let dispatch_metrics =
        flush_pending_roa_dispatch(runner, pending_roa_dispatch, inflight_publication_points)?;
    if dispatch_metrics.attempted > 0 || dispatch_metrics.queue_full {
        crate::progress_log::emit(
            "phase2_roa_dispatch_batch",
            serde_json::json!({
                "attempted": dispatch_metrics.attempted,
                "submitted": dispatch_metrics.submitted,
                "queue_full": dispatch_metrics.queue_full,
                "pending_remaining": dispatch_metrics.pending_remaining,
                "duration_ms": dispatch_metrics.duration_ms,
                "inflight_publication_points": inflight_publication_points.len(),
                "pending_finalization_len": pending_finalization.len(),
            }),
        );
    }
    Ok(())
}

fn drain_object_results(
    runner: &Rpkiv1PublicationPointRunner<'_>,
    inflight_publication_points: &mut HashMap<u64, InflightPublicationPoint>,
    pending_finalization: &mut VecDeque<FinalizeTask>,
    result_budget: usize,
) -> Result<ObjectDrainMetrics, TreeRunError> {
    let started = Instant::now();
    let mut metrics = ObjectDrainMetrics::default();
    let Some(pool) = runner.parallel_roa_worker_pool.as_ref() else {
        return Ok(metrics);
    };
    let result_budget = result_budget.max(1);
    while metrics.results_drained < result_budget {
        let Some(result) = pool
            .recv_result_timeout(Duration::from_millis(0))
            .map_err(TreeRunError::Runner)?
        else {
            break;
        };
        metrics.results_drained += 1;
        let pp_id = result.publication_point_id;
        let _worker_index = result.worker_index;
        metrics.worker_ms_total += result.worker_ms;
        metrics.worker_ms_max = metrics.worker_ms_max.max(result.worker_ms);
        metrics.queue_wait_ms_total += result.queue_wait_ms;
        metrics.queue_wait_ms_max = metrics.queue_wait_ms_max.max(result.queue_wait_ms);
        let should_finalize = if let Some(state) = inflight_publication_points.get_mut(&pp_id) {
            let now = Instant::now();
            if state.first_result_at.is_none() {
                state.first_result_at = Some(now);
            }
            state.last_result_at = Some(now);
            state.worker_ms_total += result.worker_ms;
            state.worker_ms_max = state.worker_ms_max.max(result.worker_ms);
            state.queue_wait_ms_total += result.queue_wait_ms;
            state.queue_wait_ms_max = state.queue_wait_ms_max.max(result.queue_wait_ms);
            state.results.push(result);
            state.results.len() == state.task_count
        } else {
            false
        };
        if should_finalize {
            let mut state = inflight_publication_points
                .remove(&pp_id)
                .expect("inflight publication point must exist");
            state.finalize_enqueued_at = Some(Instant::now());
            metrics.publication_points_completed += 1;
            pending_finalization.push_back(FinalizeTask { state });
        }
    }
    metrics.result_budget_exhausted = metrics.results_drained == result_budget;
    metrics.duration_ms = elapsed_ms(started);
    Ok(metrics)
}

fn drain_object_results_with_progress(
    runner: &Rpkiv1PublicationPointRunner<'_>,
    inflight_publication_points: &mut HashMap<u64, InflightPublicationPoint>,
    pending_finalization: &mut VecDeque<FinalizeTask>,
    pending_roa_dispatch_len: usize,
    result_budget: usize,
) -> Result<(), TreeRunError> {
    let drain_metrics = drain_object_results(
        runner,
        inflight_publication_points,
        pending_finalization,
        result_budget,
    )?;
    if drain_metrics.results_drained > 0 || drain_metrics.result_budget_exhausted {
        crate::progress_log::emit(
            "phase2_object_results_drain",
            serde_json::json!({
                "results_drained": drain_metrics.results_drained,
                "publication_points_completed": drain_metrics.publication_points_completed,
                "result_budget_exhausted": drain_metrics.result_budget_exhausted,
                "result_drain_batch_size": result_budget,
                "worker_ms_total": drain_metrics.worker_ms_total,
                "worker_ms_max": drain_metrics.worker_ms_max,
                "queue_wait_ms_total": drain_metrics.queue_wait_ms_total,
                "queue_wait_ms_max": drain_metrics.queue_wait_ms_max,
                "duration_ms": drain_metrics.duration_ms,
                "pending_roa_dispatch_len": pending_roa_dispatch_len,
                "inflight_publication_points": inflight_publication_points.len(),
                "pending_finalization_len": pending_finalization.len(),
            }),
        );
    }
    Ok(())
}

/// Submission endpoint of the stage pool, abstracted so the dispatch loop can
/// be tested with a deterministic backpressure source.
trait ReadyStageSubmitter {
    // The error must hand the task back so the dispatch loop can requeue it;
    // boxing it away would only shuffle the same bytes around.
    #[allow(clippy::result_large_err)]
    fn try_submit_ready_stage(
        &mut self,
        task: ReadyStageTask,
    ) -> Result<(), ObjectWorkerSubmitError<ReadyStageTask>>;
}

impl ReadyStageSubmitter for ReadyStagePool<'_, '_> {
    fn try_submit_ready_stage(
        &mut self,
        task: ReadyStageTask,
    ) -> Result<(), ObjectWorkerSubmitError<ReadyStageTask>> {
        self.try_submit_round_robin(task).map(|_| ())
    }
}

/// Dispatch loop of the pool-enabled ready batch: pop ready publication
/// points and submit their compute tasks to the stage pool, following the
/// `flush_pending_roa_dispatch` backpressure pattern — on a full worker queue
/// the publication point goes back to the head of the ready queue so the next
/// turn retries it first (nothing lost, nothing duplicated).
fn submit_ready_batch_to_stage_pool(
    stage_pool: &mut impl ReadyStageSubmitter,
    ready_queue: &mut VecDeque<ReadyCaInstance>,
    staging_inflight: &mut usize,
    ready_batch_size: usize,
    ready_batch_wall_time_budget: Duration,
) -> Result<StageDispatchMetrics, TreeRunError> {
    let started = Instant::now();
    let mut metrics = StageDispatchMetrics::default();
    while metrics.submitted < ready_batch_size {
        let Some(ready) = ready_queue.pop_front() else {
            break;
        };
        let task = ReadyStageTask {
            ready_queue_len_after_pop: ready_queue.len(),
            ready,
            submitted_at: Instant::now(),
        };
        match stage_pool.try_submit_ready_stage(task) {
            Ok(_) => {
                *staging_inflight += 1;
                metrics.submitted += 1;
            }
            Err(ObjectWorkerSubmitError::QueueFull { task, .. }) => {
                ready_queue.push_front(task.ready);
                metrics.queue_full = true;
                break;
            }
            Err(ObjectWorkerSubmitError::Disconnected { .. }) => {
                return Err(TreeRunError::Runner(
                    "ready stage worker queue disconnected".to_string(),
                ));
            }
        }
        if metrics.submitted > 0 && started.elapsed() >= ready_batch_wall_time_budget {
            break;
        }
    }
    metrics.duration_ms = elapsed_ms(started);
    Ok(metrics)
}

/// Collect every available stage result without blocking and run the apply
/// phase for each on the control thread, exactly like the inline path would
/// have done right after compute. Drained metrics aggregate into the same
/// `ReadyStageBatchMetrics`, keeping the `phase2_ready_queue_*` events
/// unchanged.
#[allow(clippy::too_many_arguments)]
fn drain_stage_results(
    stage_pool: &ReadyStagePool<'_, '_>,
    runner: &Rpkiv1PublicationPointRunner<'_>,
    next_id: &mut u64,
    ca_queue: &mut VecDeque<QueuedCaInstance>,
    pending_roa_dispatch: &mut VecDeque<OwnedRoaTask>,
    inflight_publication_points: &mut HashMap<u64, InflightPublicationPoint>,
    pending_finalization: &mut VecDeque<FinalizeTask>,
    finished: &mut Vec<FinishedPublicationPoint>,
    staging_inflight: &mut usize,
    batch_metrics: &mut ReadyStageBatchMetrics,
    pool_metrics: &mut StageDrainMetrics,
    config: &TreeRunConfig,
) -> Result<(), TreeRunError> {
    let started = Instant::now();
    loop {
        let Some(result) = stage_pool
            .recv_result_timeout(Duration::from_millis(0))
            .map_err(TreeRunError::Runner)?
        else {
            break;
        };
        pool_metrics.results_drained += 1;
        pool_metrics.queue_wait_ms_total += result.queue_wait_ms;
        pool_metrics.queue_wait_ms_max = pool_metrics.queue_wait_ms_max.max(result.queue_wait_ms);
        pool_metrics.worker_ms_total += result.worker_ms;
        pool_metrics.worker_ms_max = pool_metrics.worker_ms_max.max(result.worker_ms);
        *staging_inflight = staging_inflight.saturating_sub(1);
        let metrics = apply_ready_publication_point_stage(
            runner,
            next_id,
            ca_queue,
            pending_roa_dispatch,
            inflight_publication_points,
            pending_finalization,
            finished,
            result.outcome,
            result.metrics,
            config,
            config.compact_audit,
        );
        batch_metrics.record(metrics);
    }
    pool_metrics.duration_ms += elapsed_ms(started);
    Ok(())
}

fn submit_pending_finalization(
    finalize_task_tx: &SyncSender<FinalizeTask>,
    pending_finalization: &mut VecDeque<FinalizeTask>,
    finalize_inflight: &mut usize,
) -> Result<FinalizeSubmitMetrics, TreeRunError> {
    let started = Instant::now();
    let mut metrics = FinalizeSubmitMetrics::default();
    while let Some(task) = pending_finalization.pop_front() {
        match finalize_task_tx.try_send(task) {
            Ok(()) => {
                metrics.submitted += 1;
                *finalize_inflight += 1;
            }
            Err(TrySendError::Full(task)) => {
                pending_finalization.push_front(task);
                metrics.queue_full = true;
                break;
            }
            Err(TrySendError::Disconnected(_task)) => {
                return Err(TreeRunError::Runner(
                    "phase2 finalize worker queue disconnected".to_string(),
                ));
            }
        }
    }
    metrics.duration_ms = elapsed_ms(started);
    Ok(metrics)
}

fn submit_pending_finalization_with_progress(
    finalize_task_tx: &SyncSender<FinalizeTask>,
    pending_finalization: &mut VecDeque<FinalizeTask>,
    finalize_inflight: &mut usize,
    finalize_queue_capacity: usize,
    pending_roa_dispatch_len: usize,
    inflight_publication_points_len: usize,
) -> Result<(), TreeRunError> {
    let submit_metrics =
        submit_pending_finalization(finalize_task_tx, pending_finalization, finalize_inflight)?;
    if submit_metrics.submitted > 0 || submit_metrics.queue_full {
        crate::progress_log::emit(
            "phase2_finalize_task_submit",
            serde_json::json!({
                "submitted": submit_metrics.submitted,
                "queue_full": submit_metrics.queue_full,
                "duration_ms": submit_metrics.duration_ms,
                "finalize_queue_capacity": finalize_queue_capacity,
                "pending_finalization_len": pending_finalization.len(),
                "finalize_inflight": *finalize_inflight,
                "pending_roa_dispatch_len": pending_roa_dispatch_len,
                "inflight_publication_points": inflight_publication_points_len,
            }),
        );
    }
    Ok(())
}

fn drain_finalize_results(
    finalize_result_rx: &Receiver<FinalizeWorkerResult>,
    finished: &mut Vec<FinishedPublicationPoint>,
    finalize_inflight: &mut usize,
) -> Result<FinalizeResultsDrainMetrics, TreeRunError> {
    let started = Instant::now();
    let mut metrics = FinalizeResultsDrainMetrics::default();
    loop {
        match finalize_result_rx.try_recv() {
            Ok(result) => {
                metrics.results_drained += 1;
                metrics.reduce_ms_total += result.metrics.reduce_ms;
                metrics.reduce_ms_max = metrics.reduce_ms_max.max(result.metrics.reduce_ms);
                metrics.finalize_ms_total += result.metrics.finalize_ms;
                metrics.finalize_ms_max = metrics.finalize_ms_max.max(result.metrics.finalize_ms);
                metrics.finalize_queue_wait_ms_max = metrics
                    .finalize_queue_wait_ms_max
                    .max(result.metrics.finalize_queue_wait_ms.unwrap_or(0));
                metrics.finalize_worker_ms_total += result.metrics.finalize_worker_ms;
                metrics.finalize_worker_ms_max = metrics
                    .finalize_worker_ms_max
                    .max(result.metrics.finalize_worker_ms);
                metrics.snapshot_pack_ms_total += result.metrics.snapshot_pack_ms;
                metrics.snapshot_pack_ms_max = metrics
                    .snapshot_pack_ms_max
                    .max(result.metrics.snapshot_pack_ms);
                metrics.persist_vcir_ms_total += result.metrics.persist_vcir_ms;
                metrics.persist_vcir_ms_max = metrics
                    .persist_vcir_ms_max
                    .max(result.metrics.persist_vcir_ms);
                metrics.persist_build_vcir_ms_total += result.metrics.persist_build_vcir_ms;
                metrics.persist_build_vcir_ms_max = metrics
                    .persist_build_vcir_ms_max
                    .max(result.metrics.persist_build_vcir_ms);
                metrics.persist_replace_vcir_ms_total += result.metrics.persist_replace_vcir_ms;
                metrics.persist_replace_vcir_ms_max = metrics
                    .persist_replace_vcir_ms_max
                    .max(result.metrics.persist_replace_vcir_ms);
                metrics.ccr_projection_build_ms_total += result.metrics.ccr_projection_build_ms;
                metrics.ccr_projection_build_ms_max = metrics
                    .ccr_projection_build_ms_max
                    .max(result.metrics.ccr_projection_build_ms);
                metrics.ccr_append_ms_total += result.metrics.ccr_append_ms;
                metrics.ccr_append_ms_max =
                    metrics.ccr_append_ms_max.max(result.metrics.ccr_append_ms);
                metrics.audit_build_ms_total += result.metrics.audit_build_ms;
                metrics.audit_build_ms_max = metrics
                    .audit_build_ms_max
                    .max(result.metrics.audit_build_ms);
                *finalize_inflight = finalize_inflight.saturating_sub(1);
                finished.push(result.finished);
            }
            Err(TryRecvError::Empty) => break,
            Err(TryRecvError::Disconnected) => {
                if *finalize_inflight == 0 {
                    break;
                }
                return Err(TreeRunError::Runner(
                    "phase2 finalize result channel disconnected".to_string(),
                ));
            }
        }
    }
    metrics.duration_ms = elapsed_ms(started);
    Ok(metrics)
}

fn drain_finalize_results_with_progress(
    finalize_result_rx: &Receiver<FinalizeWorkerResult>,
    finished: &mut Vec<FinishedPublicationPoint>,
    finalize_inflight: &mut usize,
    pending_finalization_len: usize,
    pending_roa_dispatch_len: usize,
    inflight_publication_points_len: usize,
) -> Result<(), TreeRunError> {
    let drain_metrics = drain_finalize_results(finalize_result_rx, finished, finalize_inflight)?;
    if drain_metrics.results_drained >= 64
        || (drain_metrics.results_drained > 0
            && (*finalize_inflight == 0 || pending_finalization_len > 0))
    {
        crate::progress_log::emit(
            "phase2_finalize_results_drain",
            serde_json::json!({
                "results_drained": drain_metrics.results_drained,
                "reduce_ms_total": drain_metrics.reduce_ms_total,
                "reduce_ms_max": drain_metrics.reduce_ms_max,
                "finalize_ms_total": drain_metrics.finalize_ms_total,
                "finalize_ms_max": drain_metrics.finalize_ms_max,
                "finalize_queue_wait_ms_max": drain_metrics.finalize_queue_wait_ms_max,
                "finalize_worker_ms_total": drain_metrics.finalize_worker_ms_total,
                "finalize_worker_ms_max": drain_metrics.finalize_worker_ms_max,
                "snapshot_pack_ms_total": drain_metrics.snapshot_pack_ms_total,
                "snapshot_pack_ms_max": drain_metrics.snapshot_pack_ms_max,
                "persist_vcir_ms_total": drain_metrics.persist_vcir_ms_total,
                "persist_vcir_ms_max": drain_metrics.persist_vcir_ms_max,
                "persist_build_vcir_ms_total": drain_metrics.persist_build_vcir_ms_total,
                "persist_build_vcir_ms_max": drain_metrics.persist_build_vcir_ms_max,
                "persist_replace_vcir_ms_total": drain_metrics.persist_replace_vcir_ms_total,
                "persist_replace_vcir_ms_max": drain_metrics.persist_replace_vcir_ms_max,
                "ccr_projection_build_ms_total": drain_metrics.ccr_projection_build_ms_total,
                "ccr_projection_build_ms_max": drain_metrics.ccr_projection_build_ms_max,
                "ccr_append_ms_total": drain_metrics.ccr_append_ms_total,
                "ccr_append_ms_max": drain_metrics.ccr_append_ms_max,
                "audit_build_ms_total": drain_metrics.audit_build_ms_total,
                "audit_build_ms_max": drain_metrics.audit_build_ms_max,
                "duration_ms": drain_metrics.duration_ms,
                "pending_finalization_len": pending_finalization_len,
                "finalize_inflight": *finalize_inflight,
                "pending_roa_dispatch_len": pending_roa_dispatch_len,
                "inflight_publication_points": inflight_publication_points_len,
            }),
        );
    }
    Ok(())
}

fn run_finalize_worker(
    runner: &Rpkiv1PublicationPointRunner<'_>,
    finalize_task_rx: Receiver<FinalizeTask>,
    finalize_result_tx: mpsc::Sender<FinalizeWorkerResult>,
    compact_audit: bool,
) -> Result<(), TreeRunError> {
    while let Ok(task) = finalize_task_rx.recv() {
        let result = finalize_publication_point_state(runner, task.state, compact_audit);
        if finalize_result_tx.send(result).is_err() {
            return Err(TreeRunError::Runner(
                "phase2 finalize result receiver disconnected".to_string(),
            ));
        }
    }
    Ok(())
}

fn finalize_publication_point_state(
    runner: &Rpkiv1PublicationPointRunner<'_>,
    state: InflightPublicationPoint,
    compact_audit: bool,
) -> FinalizeWorkerResult {
    let finalize_worker_started = Instant::now();
    let InflightPublicationPoint {
        node,
        fresh_stage,
        objects_prepare,
        repo_outcome,
        warnings,
        started_at,
        objects_started_at,
        task_count,
        tasks_submitted,
        first_task_submitted_at,
        last_task_submitted_at,
        first_result_at,
        last_result_at,
        worker_ms_total,
        worker_ms_max,
        queue_wait_ms_total,
        queue_wait_ms_max,
        finalize_enqueued_at,
        results,
    } = state;
    let finalize_queue_wait_ms = finalize_enqueued_at.or(last_result_at).map(|ready_at| {
        Instant::now()
            .saturating_duration_since(ready_at)
            .as_millis() as u64
    });
    let objects_processing_ms = objects_started_at.elapsed().as_millis() as u64;
    runner.record_publication_point_step_ms(
        &node.handle.manifest_rsync_uri,
        "fresh_objects_processing_lifetime",
        objects_processing_ms,
    );

    let (result, mut metrics, reduce_ms) = match objects_prepare {
        ParallelObjectsPrepare::Staged(objects_stage) => {
            let reduce_started = Instant::now();
            let locked_files = objects_stage.locked_file_count();
            let reduce_result =
                reduce_parallel_roa_stage(objects_stage, results, runner.timing.as_ref());
            let reduce_ms = elapsed_ms(reduce_started);
            runner.record_publication_point_step_ms(
                &node.handle.manifest_rsync_uri,
                "fresh_roa_reduce",
                reduce_ms,
            );

            let (result, metrics) = match reduce_result {
                Ok(mut objects) => {
                    let finalize_started = Instant::now();
                    objects
                        .router_keys
                        .extend(fresh_stage.discovered_router_keys.clone());
                    objects.local_outputs_cache.extend(
                        crate::validation::tree_runner::build_router_key_local_outputs(
                            &node.handle,
                            &objects.router_keys,
                        ),
                    );
                    let finalized = runner.finalize_fresh_publication_point_from_reducer(
                        &node.handle,
                        &fresh_stage.fresh_point,
                        warnings,
                        objects,
                        fresh_stage.child_audits,
                        fresh_stage.discovered_children,
                        repo_outcome.repo_sync_source.as_deref(),
                        repo_outcome.repo_sync_phase.as_deref(),
                        repo_outcome.repo_sync_duration_ms,
                        repo_outcome.repo_sync_err.as_deref(),
                    );
                    let finalize_ms = elapsed_ms(finalize_started);
                    match finalized {
                        Ok(output) => {
                            let metrics = finalize_metrics_from_output(
                                &output,
                                reduce_ms,
                                finalize_ms,
                                finalize_queue_wait_ms,
                                0,
                                locked_files,
                            );
                            (
                                compact_phase2_finished_result(output.result, compact_audit),
                                metrics,
                            )
                        }
                        Err(err) => (
                            FinishedPublicationPointResult::Err(err),
                            FinalizePublicationPointMetrics {
                                reduce_ms,
                                finalize_ms,
                                finalize_queue_wait_ms,
                                locked_files,
                                ..FinalizePublicationPointMetrics::default()
                            },
                        ),
                    }
                }
                Err(err) => (
                    FinishedPublicationPointResult::Err(err),
                    FinalizePublicationPointMetrics {
                        reduce_ms,
                        finalize_queue_wait_ms,
                        locked_files,
                        ..FinalizePublicationPointMetrics::default()
                    },
                ),
            };
            (result, metrics, reduce_ms)
        }
        ParallelObjectsPrepare::Complete(objects) => {
            // ROA prepare already produced complete objects for this publication
            // point, so there is nothing to reduce; finalize directly. This is
            // the former control-thread "direct finalize", now running on the
            // finalize worker through the regular task queue.
            let locked_files = fresh_stage.fresh_point.files().len();
            let finalize_started = Instant::now();
            let finalized = runner.finalize_fresh_publication_point_from_reducer(
                &node.handle,
                &fresh_stage.fresh_point,
                warnings,
                objects,
                fresh_stage.child_audits,
                fresh_stage.discovered_children,
                repo_outcome.repo_sync_source.as_deref(),
                repo_outcome.repo_sync_phase.as_deref(),
                repo_outcome.repo_sync_duration_ms,
                repo_outcome.repo_sync_err.as_deref(),
            );
            let finalize_ms = elapsed_ms(finalize_started);
            let (result, metrics) = match finalized {
                Ok(output) => {
                    let metrics = finalize_metrics_from_output(
                        &output,
                        0,
                        finalize_ms,
                        finalize_queue_wait_ms,
                        0,
                        locked_files,
                    );
                    (
                        compact_phase2_finished_result(output.result, compact_audit),
                        metrics,
                    )
                }
                Err(err) => (
                    FinishedPublicationPointResult::Err(err),
                    FinalizePublicationPointMetrics {
                        finalize_ms,
                        finalize_queue_wait_ms,
                        locked_files,
                        ..FinalizePublicationPointMetrics::default()
                    },
                ),
            };
            (result, metrics, 0)
        }
    };
    let finalize_worker_ms = elapsed_ms(finalize_worker_started);
    metrics.finalize_worker_ms = finalize_worker_ms;
    runner.record_publication_point_step_ms(
        &node.handle.manifest_rsync_uri,
        "fresh_finalize_worker",
        finalize_worker_ms,
    );
    runner.record_publication_point_step_ms(
        &node.handle.manifest_rsync_uri,
        "fresh_finalize_queue_wait",
        finalize_queue_wait_ms.unwrap_or(0),
    );
    runner.record_publication_point_step_ms(
        &node.handle.manifest_rsync_uri,
        "fresh_finalize",
        metrics.finalize_ms,
    );
    runner.record_publication_point_total_ms(
        &node.handle.manifest_rsync_uri,
        started_at.elapsed().as_millis() as u64,
    );
    emit_finalize_breakdown(
        "phase2_finalize_worker_breakdown",
        node.handle.manifest_rsync_uri.as_str(),
        node.handle.publication_point_rsync_uri.as_str(),
        &metrics,
    );
    crate::progress_log::emit(
        "phase2_publication_point_reduced",
        serde_json::json!({
            "manifest_rsync_uri": node.handle.manifest_rsync_uri.as_str(),
            "publication_point_rsync_uri": node.handle.publication_point_rsync_uri.as_str(),
            "objects_processing_ms": objects_processing_ms,
            "task_count": task_count,
            "tasks_submitted": tasks_submitted,
            "first_task_submitted_ms": first_task_submitted_at.map(|t| t.saturating_duration_since(objects_started_at).as_millis() as u64),
            "last_task_submitted_ms": last_task_submitted_at.map(|t| t.saturating_duration_since(objects_started_at).as_millis() as u64),
            "task_submit_span_ms": match (first_task_submitted_at, last_task_submitted_at) {
                (Some(first), Some(last)) => Some(last.saturating_duration_since(first).as_millis() as u64),
                _ => None,
            },
            "first_result_ms": first_result_at.map(|t| t.saturating_duration_since(objects_started_at).as_millis() as u64),
            "last_result_ms": last_result_at.map(|t| t.saturating_duration_since(objects_started_at).as_millis() as u64),
            "all_results_ready_ms": last_result_at.map(|t| t.saturating_duration_since(objects_started_at).as_millis() as u64),
            "finalize_queue_wait_ms": finalize_queue_wait_ms,
            "result_span_ms": match (first_result_at, last_result_at) {
                (Some(first), Some(last)) => Some(last.saturating_duration_since(first).as_millis() as u64),
                _ => None,
            },
            "worker_ms_total": worker_ms_total,
            "worker_ms_max": worker_ms_max,
            "worker_ms_avg": if task_count > 0 { worker_ms_total / task_count as u64 } else { 0 },
            "queue_wait_ms_total": queue_wait_ms_total,
            "queue_wait_ms_max": queue_wait_ms_max,
            "queue_wait_ms_avg": if task_count > 0 { queue_wait_ms_total / task_count as u64 } else { 0 },
            "reduce_ms": reduce_ms,
            "finalize_ms": metrics.finalize_ms,
            "finalize_worker_ms": finalize_worker_ms,
            "snapshot_pack_ms": metrics.snapshot_pack_ms,
            "persist_vcir_ms": metrics.persist_vcir_ms,
            "persist_build_vcir_ms": metrics.persist_build_vcir_ms,
            "persist_replace_vcir_ms": metrics.persist_replace_vcir_ms,
            "ccr_projection_build_ms": metrics.ccr_projection_build_ms,
            "ccr_append_ms": metrics.ccr_append_ms,
            "audit_build_ms": metrics.audit_build_ms,
            "locked_files": metrics.locked_files,
            "child_count": metrics.child_count,
            "warning_count": metrics.warning_count,
            "vrp_count": metrics.vrp_count,
            "vap_count": metrics.vap_count,
            "router_key_count": metrics.router_key_count,
            "audit_object_count": metrics.audit_object_count,
            "total_duration_ms": started_at.elapsed().as_millis() as u64,
        }),
    );
    FinalizeWorkerResult {
        finished: FinishedPublicationPoint {
            node: FinishedPublicationPointNode::from_queued(node),
            result,
        },
        metrics,
    }
}

fn drain_repo_events(
    repo_runtime: &dyn crate::parallel::repo_runtime::RepoSyncRuntime,
    ca_waiting_repo_by_identity: &mut HashMap<RepoIdentity, Vec<QueuedCaInstance>>,
    ready_queue: &mut VecDeque<ReadyCaInstance>,
    timeout: Duration,
) -> Result<RepoDrainMetrics, TreeRunError> {
    let started = Instant::now();
    let mut metrics = RepoDrainMetrics::default();
    let events = repo_runtime
        .drain_repo_results_timeout(timeout, REPO_RESULT_DRAIN_MAX_EVENTS)
        .map_err(TreeRunError::Runner)?;
    for event in events {
        metrics.event_count += 1;
        metrics.completions += event.completions.len();
        for completion in event.completions {
            let mut outcome = completion.outcome;
            if completion.identity != event.transport_identity {
                // Shared RRDP/rsync transports release many publication points, but the transport
                // wall time should only be counted once in per-PP stage timing aggregation.
                outcome.repo_sync_duration_ms = 0;
            }
            if let Some(waiters) = ca_waiting_repo_by_identity.remove(&completion.identity) {
                metrics.ready_enqueued += waiters.len();
                for node in waiters {
                    ready_queue.push_back(ReadyCaInstance {
                        node,
                        repo_outcome: outcome.clone(),
                        ready_enqueued_at: Instant::now(),
                    });
                }
            }
        }
    }
    metrics.duration_ms = elapsed_ms(started);
    Ok(metrics)
}

fn event_poll_timeout(
    ca_queue: &VecDeque<QueuedCaInstance>,
    ready_queue: &VecDeque<ReadyCaInstance>,
    pending_roa_dispatch: &VecDeque<OwnedRoaTask>,
    inflight_publication_points: &HashMap<u64, InflightPublicationPoint>,
    pending_finalization: &VecDeque<FinalizeTask>,
    finalize_inflight: usize,
    staging_inflight: usize,
    instances_started: usize,
    config: &TreeRunConfig,
) -> Duration {
    // Stage results pending collection must not let the loop sleep: with the
    // stage pool enabled a 50ms nap per turn would collapse throughput.
    if !ready_queue.is_empty()
        || !pending_roa_dispatch.is_empty()
        || !inflight_publication_points.is_empty()
        || !pending_finalization.is_empty()
        || staging_inflight > 0
        || (!ca_queue.is_empty() && can_start_more(instances_started, config))
    {
        Duration::from_millis(0)
    } else if finalize_inflight > 0 {
        Duration::from_millis(10)
    } else {
        Duration::from_millis(50)
    }
}

fn is_complete(
    ca_queue: &VecDeque<QueuedCaInstance>,
    ready_queue: &VecDeque<ReadyCaInstance>,
    ca_waiting_repo_by_identity: &HashMap<RepoIdentity, Vec<QueuedCaInstance>>,
    pending_roa_dispatch: &VecDeque<OwnedRoaTask>,
    inflight_publication_points: &HashMap<u64, InflightPublicationPoint>,
    pending_finalization: &VecDeque<FinalizeTask>,
    finalize_inflight: usize,
    staging_inflight: usize,
    instances_started: usize,
    config: &TreeRunConfig,
) -> bool {
    // `staging_inflight == 0` additionally implies the stage result channel is
    // drained empty: the drain loop collects every available result each turn
    // and only decrements the counter while collecting.
    let ca_queue_done = ca_queue.is_empty() || !can_start_more(instances_started, config);
    ca_queue_done
        && ready_queue.is_empty()
        && ca_waiting_repo_by_identity.is_empty()
        && pending_roa_dispatch.is_empty()
        && inflight_publication_points.is_empty()
        && pending_finalization.is_empty()
        && finalize_inflight == 0
        && staging_inflight == 0
}

/// Minimum publication points per reduction shard; smaller runs stay
/// single-threaded to avoid thread-spawn overhead dominating the reduction.
const TREE_OUTPUT_MIN_SHARD_LEN: usize = 4096;
/// Hard cap on reduction shards regardless of core count.
const TREE_OUTPUT_MAX_SHARDS: usize = 8;

fn tree_output_shard_count(len: usize) -> usize {
    if len < TREE_OUTPUT_MIN_SHARD_LEN * 2 {
        return 1;
    }
    let parallel = std::thread::available_parallelism()
        .map(|n| n.get())
        .unwrap_or(1);
    parallel
        .min(TREE_OUTPUT_MAX_SHARDS)
        .min(len / TREE_OUTPUT_MIN_SHARD_LEN)
        .max(1)
}

/// Per-shard reduction result of the phase2 output merge. Shards cover
/// contiguous ranges of the id-sorted finished list, so concatenating shard
/// outputs in shard order reproduces the sequential single-threaded order
/// exactly.
#[derive(Default)]
struct TreeOutputShardReduction {
    instances_processed: usize,
    instances_failed: usize,
    warnings: Vec<Warning>,
    vrps: Vec<crate::validation::objects::Vrp>,
    aspas: Vec<crate::validation::objects::AspaAttestation>,
    router_keys: Vec<crate::validation::objects::RouterKeyPayload>,
    publication_points: Vec<PublicationPointAudit>,
    roa_cache_stats: crate::validation::objects::RoaValidationCacheStats,
    cir_input: CirInputAccumulator,
}

fn reduce_finished_shard(items: Vec<FinishedPublicationPoint>) -> TreeOutputShardReduction {
    let mut reduction = TreeOutputShardReduction::default();
    for item in items {
        match item.result {
            FinishedPublicationPointResult::Ok {
                source,
                warnings: result_warnings,
                objects,
                audit,
                cir_fresh_objects,
                cir_cached_objects,
            } => {
                reduction.instances_processed += 1;
                reduction.warnings.extend(result_warnings);
                reduction.warnings.extend(objects.warnings);
                reduction
                    .roa_cache_stats
                    .add_assign(&objects.roa_cache_stats);
                reduction.vrps.extend(objects.vrps);
                reduction.aspas.extend(objects.aspas);
                reduction.router_keys.extend(objects.router_keys);

                let mut audit: PublicationPointAudit = audit;
                audit.node_id = Some(item.node.id);
                audit.parent_node_id = item.node.parent_id;
                audit.discovered_from = item.node.discovered_from;
                crate::validation::tree::submit_publication_point_cir_input(
                    &mut reduction.cir_input,
                    source,
                    &audit,
                    &cir_fresh_objects,
                    &cir_cached_objects,
                )
                .expect("CIR input collection from validated audit must not fail");
                reduction.publication_points.push(audit);
            }
            FinishedPublicationPointResult::Err(err) => {
                reduction.instances_failed += 1;
                reduction.warnings.push(
                    Warning::new(format!("publication point failed: {err}"))
                        .with_context(&item.node.manifest_rsync_uri),
                );
            }
        }
    }
    reduction
}

fn merge_shard_reductions(
    reductions: Vec<TreeOutputShardReduction>,
) -> TreeOutputShardReduction {
    let mut iter = reductions.into_iter();
    let mut merged = iter.next().unwrap_or_default();
    for mut other in iter {
        merged.instances_processed += other.instances_processed;
        merged.instances_failed += other.instances_failed;
        merged.warnings.append(&mut other.warnings);
        merged.vrps.append(&mut other.vrps);
        merged.aspas.append(&mut other.aspas);
        merged.router_keys.append(&mut other.router_keys);
        merged
            .publication_points
            .append(&mut other.publication_points);
        merged.roa_cache_stats.add_assign(&other.roa_cache_stats);
        merged
            .cir_input
            .merge(std::mem::take(&mut other.cir_input))
            .expect("CIR input merge from validated audits must not fail");
    }
    merged
}

fn build_tree_output(mut finished: Vec<FinishedPublicationPoint>) -> TreeRunAuditOutput {
    let total_started = Instant::now();
    finished.sort_by_key(|item| item.node.id);
    let sort_ms = total_started.elapsed().as_millis() as u64;
    let shard_count = tree_output_shard_count(finished.len());

    let reduce_started = Instant::now();
    let reductions = if shard_count <= 1 {
        vec![reduce_finished_shard(finished)]
    } else {
        let chunk_len = finished.len().div_ceil(shard_count);
        let mut shards: Vec<Vec<FinishedPublicationPoint>> = Vec::new();
        let mut rest = finished;
        while !rest.is_empty() {
            let split_at = chunk_len.min(rest.len());
            let tail = rest.split_off(split_at);
            shards.push(std::mem::replace(&mut rest, tail));
        }
        std::thread::scope(|scope| {
            let handles: Vec<_> = shards
                .into_iter()
                .map(|shard| scope.spawn(move || reduce_finished_shard(shard)))
                .collect();
            handles
                .into_iter()
                .map(|handle| {
                    handle
                        .join()
                        .expect("tree output reduction shard panicked")
                })
                .collect()
        })
    };
    let reduce_ms = reduce_started.elapsed().as_millis() as u64;

    let merge_started = Instant::now();
    let merged = merge_shard_reductions(reductions);
    let merge_ms = merge_started.elapsed().as_millis() as u64;

    let finalize_started = Instant::now();
    let output = TreeRunAuditOutput {
        tree: TreeRunOutput {
            instances_processed: merged.instances_processed,
            instances_failed: merged.instances_failed,
            warnings: merged.warnings,
            vrps: merged.vrps,
            aspas: merged.aspas,
            router_keys: merged.router_keys,
        },
        publication_points: merged.publication_points,
        roa_cache_stats: merged.roa_cache_stats,
        cir_input: merged.cir_input.finalize(),
    };
    let finalize_ms = finalize_started.elapsed().as_millis() as u64;

    crate::progress_log::emit(
        "phase2_build_tree_output",
        serde_json::json!({
            "sort_ms": sort_ms,
            "shard_count": shard_count,
            "reduce_ms": reduce_ms,
            "merge_ms": merge_ms,
            "finalize_ms": finalize_ms,
            "total_ms": total_started.elapsed().as_millis() as u64,
            "publication_points": output.publication_points.len(),
            "instances_processed": output.tree.instances_processed,
            "instances_failed": output.tree.instances_failed,
        }),
    );
    output
}

pub fn run_tree_parallel_phase2_audit(
    root: CaInstanceHandle,
    runner: &Rpkiv1PublicationPointRunner<'_>,
    config: &TreeRunConfig,
) -> Result<TreeRunAuditOutput, TreeRunError> {
    run_tree_parallel_phase2_audit_multi_root(vec![root], runner, config)
}

#[cfg(test)]
mod tests {
    use super::{
        CacheHitOutcome, CompleteOutcome, FinishedPublicationPoint, FinishedPublicationPointNode,
        FinishedPublicationPointResult, InflightPublicationPoint, QueuedCaInstance,
        ReadyCaInstance, ReadyStageMetrics, ReadyStageSubmitter, StageOutcome,
        TREE_OUTPUT_MAX_SHARDS, TREE_OUTPUT_MIN_SHARD_LEN, apply_ready_publication_point_stage,
        build_tree_output, compact_phase2_finished_result, compact_phase2_finished_result_result,
        compute_ready_publication_point_stage, event_poll_timeout, finalize_metrics_from_output,
        finalize_publication_point_state, is_complete, merge_shard_reductions,
        reduce_finished_shard, submit_ready_batch_to_stage_pool, tree_output_shard_count,
    };
    use crate::audit::{
        AuditObjectKind, AuditObjectResult, DiscoveredFrom, ObjectAuditEntry,
        PublicationPointAudit,
    };
    use crate::fetch::rsync::{RsyncFetchError, RsyncFetcher};
    use crate::parallel::repo_runtime::RepoSyncRuntimeOutcome;
    use crate::policy::{CaFailedFetchPolicy, Policy, SyncPreference};
    use crate::storage::{PackTime, RocksStore};
    use crate::sync::rrdp::Fetcher;
    use crate::validation::manifest::{
        FreshPublicationPointTimingBreakdown, FreshValidatedPublicationPoint,
        PublicationPointSource,
    };
    use crate::validation::objects::{ObjectsOutput, ObjectsStats, ParallelObjectsPrepare};
    use crate::validation::publication_point::PublicationPointSnapshot;
    use crate::validation::tree::{
        CaCertificateRef, CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult,
        TreeRunConfig,
    };
    use crate::validation::tree_runner::{
        BuildVcirTimingBreakdown, FreshPublicationPointFinalizeOutput, FreshPublicationPointStage,
        PersistVcirTimingBreakdown, Rpkiv1PublicationPointRunner,
    };
    use std::collections::{HashMap, VecDeque};
    use std::sync::Mutex;
    use std::time::{Duration, Instant};

    fn sample_snapshot() -> PublicationPointSnapshot {
        PublicationPointSnapshot {
            format_version: PublicationPointSnapshot::FORMAT_VERSION_V1,
            manifest_rsync_uri: "rsync://example.test/repo/example.mft".to_string(),
            publication_point_rsync_uri: "rsync://example.test/repo/".to_string(),
            manifest_number_be: vec![1],
            this_update: PackTime {
                rfc3339_utc: "2026-04-21T00:00:00Z".to_string(),
            },
            next_update: PackTime {
                rfc3339_utc: "2026-04-22T00:00:00Z".to_string(),
            },
            verified_at: PackTime {
                rfc3339_utc: "2026-04-21T00:00:01Z".to_string(),
            },
            manifest_bytes: vec![1, 2, 3],
            files: Vec::new(),
        }
    }

    fn sample_result() -> PublicationPointRunResult {
        PublicationPointRunResult {
            source: PublicationPointSource::Fresh,
            snapshot: Some(sample_snapshot()),
            warnings: Vec::new(),
            objects: ObjectsOutput {
                vrps: Vec::new(),
                aspas: Vec::new(),
                router_keys: Vec::new(),
                local_outputs_cache: Vec::new(),
                warnings: Vec::new(),
                stats: ObjectsStats::default(),
                audit: Vec::new(),
                roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(),
                roa_cache_object_meta: Vec::new(),
            },
            audit: PublicationPointAudit::default(),
            cir_fresh_objects: Vec::new(),
            cir_cached_objects: Vec::new(),
            discovered_children: Vec::new(),
        }
    }

    #[test]
    fn compact_phase2_finished_result_drops_snapshot() {
        let result = compact_phase2_finished_result(sample_result(), false);
        match result {
            FinishedPublicationPointResult::Ok { warnings, .. } => {
                assert!(warnings.is_empty());
            }
            FinishedPublicationPointResult::Err(err) => panic!("unexpected error: {err}"),
        }
    }

    #[test]
    fn compact_phase2_finished_result_can_drop_audit_payload() {
        let mut sample = sample_result();
        sample.audit.objects.push(crate::audit::ObjectAuditEntry {
            rsync_uri: "rsync://example.test/repo/a.roa".to_string(),
            sha256_hex: "11".repeat(32),
            kind: crate::audit::AuditObjectKind::Roa,
            result: crate::audit::AuditObjectResult::Ok,
            detail: None,
        });
        sample.audit.warnings.push(crate::audit::AuditWarning {
            message: "warning".to_string(),
            rfc_refs: Vec::new(),
            context: None,
        });
        sample.objects.audit.push(crate::audit::ObjectAuditEntry {
            rsync_uri: "rsync://example.test/repo/b.roa".to_string(),
            sha256_hex: "22".repeat(32),
            kind: crate::audit::AuditObjectKind::Roa,
            result: crate::audit::AuditObjectResult::Ok,
            detail: None,
        });
        let result = compact_phase2_finished_result(sample, true);
        match result {
            FinishedPublicationPointResult::Ok {
                objects,
                audit,
                cir_fresh_objects,
                ..
            } => {
                assert!(audit.objects.is_empty());
                assert!(audit.warnings.is_empty());
                assert!(objects.audit.is_empty());
                assert_eq!(cir_fresh_objects.len(), 1);
            }
            FinishedPublicationPointResult::Err(err) => panic!("unexpected error: {err}"),
        }
    }

    #[test]
    fn compact_phase2_finished_result_result_preserves_err() {
        match compact_phase2_finished_result_result(Err("boom".to_string()), false) {
            FinishedPublicationPointResult::Err(err) => assert_eq!(err, "boom"),
            FinishedPublicationPointResult::Ok { .. } => panic!("error should be preserved"),
        }
    }

    fn finished_ok_item(id: u64) -> FinishedPublicationPoint {
        let mut result = sample_result();
        result
            .warnings
            .push(crate::report::Warning::new(format!("result-warning-{id:05}")));
        result
            .objects
            .warnings
            .push(crate::report::Warning::new(format!("objects-warning-{id:05}")));
        result.objects.vrps.push(crate::validation::objects::Vrp {
            asn: 64496 + id as u32,
            prefix: crate::data_model::roa::IpPrefix {
                afi: crate::data_model::roa::RoaAfi::Ipv4,
                addr: [192, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                prefix_len: 24,
            },
            max_length: 24,
        });
        result.audit.objects.push(crate::audit::ObjectAuditEntry {
            rsync_uri: format!("rsync://example.test/repo/{id:05}.roa"),
            sha256_hex: format!("{id:064x}"),
            kind: crate::audit::AuditObjectKind::Roa,
            result: crate::audit::AuditObjectResult::Ok,
            detail: None,
        });
        FinishedPublicationPoint {
            node: FinishedPublicationPointNode {
                id,
                parent_id: None,
                discovered_from: None,
                manifest_rsync_uri: format!("rsync://example.test/repo/{id:05}.mft"),
            },
            result: compact_phase2_finished_result(result, false),
        }
    }

    fn finished_err_item(id: u64) -> FinishedPublicationPoint {
        FinishedPublicationPoint {
            node: FinishedPublicationPointNode {
                id,
                parent_id: None,
                discovered_from: None,
                manifest_rsync_uri: format!("rsync://example.test/repo/{id:05}.mft"),
            },
            result: FinishedPublicationPointResult::Err(format!("boom-{id:05}")),
        }
    }

    fn mixed_finished_items(n: u64) -> Vec<FinishedPublicationPoint> {
        (0..n)
            .map(|id| {
                if id % 97 == 0 {
                    finished_err_item(id)
                } else {
                    finished_ok_item(id)
                }
            })
            .collect()
    }

    #[test]
    fn tree_output_shard_count_respects_thresholds() {
        assert_eq!(tree_output_shard_count(0), 1);
        assert_eq!(tree_output_shard_count(TREE_OUTPUT_MIN_SHARD_LEN * 2 - 1), 1);
        assert!(tree_output_shard_count(TREE_OUTPUT_MIN_SHARD_LEN * 2) >= 1);
        let parallel = std::thread::available_parallelism()
            .map(|n| n.get())
            .unwrap_or(1);
        assert_eq!(
            tree_output_shard_count(1_000_000),
            parallel.min(TREE_OUTPUT_MAX_SHARDS)
        );
    }

    #[test]
    fn tree_output_sharded_merge_matches_single_reduction() {
        let single = merge_shard_reductions(vec![reduce_finished_shard(mixed_finished_items(300))]);

        let mut chunks: Vec<Vec<FinishedPublicationPoint>> = Vec::new();
        let mut rest = mixed_finished_items(300);
        while !rest.is_empty() {
            let split_at = 100.min(rest.len());
            let tail = rest.split_off(split_at);
            chunks.push(std::mem::replace(&mut rest, tail));
        }
        let merged = merge_shard_reductions(
            chunks.into_iter().map(reduce_finished_shard).collect(),
        );

        assert_eq!(single.instances_processed, merged.instances_processed);
        assert_eq!(single.instances_failed, merged.instances_failed);
        assert_eq!(format!("{:?}", single.warnings), format!("{:?}", merged.warnings));
        assert_eq!(format!("{:?}", single.vrps), format!("{:?}", merged.vrps));
        assert_eq!(
            format!("{:?}", single.publication_points),
            format!("{:?}", merged.publication_points)
        );
        assert_eq!(
            format!("{:?}", single.roa_cache_stats),
            format!("{:?}", merged.roa_cache_stats)
        );
        assert_eq!(single.cir_input.finalize(), merged.cir_input.finalize());
    }

    #[test]
    fn build_tree_output_orders_results_by_node_id() {
        let mut items = mixed_finished_items(50);
        items.reverse();
        let output = build_tree_output(items);
        let got: Vec<u64> = output
            .publication_points
            .iter()
            .map(|pp| pp.node_id.expect("node id set"))
            .collect();
        let want: Vec<u64> = (1..50).filter(|id| id % 97 != 0).collect();
        assert_eq!(got, want);
        assert_eq!(output.tree.instances_failed, 1);
        let first_warning = format!("{:?}", output.tree.warnings[0]);
        assert!(
            first_warning.contains("publication point failed: boom-00000"),
            "failed publication point warning keeps id order: {first_warning}"
        );
        assert_eq!(output.cir_input.fresh_validated_objects.len(), 49);
        assert_eq!(
            output.cir_input.fresh_validated_objects[0].rsync_uri,
            "rsync://example.test/repo/00001.roa"
        );
    }

    #[test]
    fn finalize_metrics_from_output_captures_breakdown_and_counts() {
        let mut result = sample_result();
        result.objects.vrps.push(crate::validation::objects::Vrp {
            asn: 64496,
            prefix: crate::data_model::roa::IpPrefix {
                afi: crate::data_model::roa::RoaAfi::Ipv4,
                addr: [192, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                prefix_len: 24,
            },
            max_length: 24,
        });
        result
            .objects
            .aspas
            .push(crate::validation::objects::AspaAttestation {
                customer_as_id: 64497,
                provider_as_ids: vec![64498],
            });
        result.audit.objects.push(ObjectAuditEntry {
            rsync_uri: "rsync://example.test/repo/a.roa".to_string(),
            sha256_hex: "11".repeat(32),
            kind: AuditObjectKind::Roa,
            result: AuditObjectResult::Ok,
            detail: None,
        });
        result
            .discovered_children
            .push(crate::validation::tree::DiscoveredChildCaInstance {
                handle: crate::validation::tree::CaInstanceHandle {
                    tal_id: "test".to_string(),
                    ca_certificate: crate::validation::tree::CaCertificateRef::inline_der(vec![1]),
                    ca_certificate_rsync_uri: Some(
                        "rsync://example.test/repo/child.cer".to_string(),
                    ),
                    effective_ip_resources: None,
                    effective_as_resources: None,
                    manifest_rsync_uri: "rsync://example.test/repo/child.mft".to_string(),
                    publication_point_rsync_uri: "rsync://example.test/repo/".to_string(),
                    rsync_base_uri: "rsync://example.test/repo/".to_string(),
                    rrdp_notification_uri: None,
                    parent_manifest_rsync_uri: None,
                    depth: 1,
                },
                discovered_from: crate::audit::DiscoveredFrom {
                    parent_manifest_rsync_uri: "rsync://example.test/repo/example.mft".to_string(),
                    child_ca_certificate_rsync_uri: "rsync://example.test/repo/child.cer"
                        .to_string(),
                    child_ca_certificate_sha256_hex: "55".repeat(32),
                },
                child_entry_projection: None,
            });
        let output = FreshPublicationPointFinalizeOutput {
            result,
            snapshot_pack_ms: 1,
            persist_vcir_ms: 2,
            persist_vcir_timing: PersistVcirTimingBreakdown {
                build_vcir_ms: 3,
                replace_vcir_ms: 4,
                build_vcir: BuildVcirTimingBreakdown {
                    related_artifacts_ms: 5,
                    ..BuildVcirTimingBreakdown::default()
                },
                ..PersistVcirTimingBreakdown::default()
            },
            ccr_projection_build_ms: 6,
            ccr_append_ms: 7,
            audit_build_ms: 8,
        };

        let metrics = finalize_metrics_from_output(&output, 9, 10, Some(11), 12, 13);

        assert_eq!(metrics.snapshot_pack_ms, 1);
        assert_eq!(metrics.persist_vcir_ms, 2);
        assert_eq!(metrics.persist_build_vcir_ms, 3);
        assert_eq!(metrics.persist_replace_vcir_ms, 4);
        assert_eq!(
            metrics.persist_replace_breakdown,
            crate::storage::VcirReplaceTimingBreakdown::default()
        );
        assert_eq!(metrics.ccr_projection_build_ms, 6);
        assert_eq!(metrics.ccr_append_ms, 7);
        assert_eq!(metrics.audit_build_ms, 8);
        assert_eq!(metrics.reduce_ms, 9);
        assert_eq!(metrics.finalize_ms, 10);
        assert_eq!(metrics.finalize_queue_wait_ms, Some(11));
        assert_eq!(metrics.finalize_worker_ms, 12);
        assert_eq!(metrics.locked_files, 13);
        assert_eq!(metrics.child_count, 1);
        assert_eq!(metrics.vrp_count, 1);
        assert_eq!(metrics.vap_count, 1);
        assert_eq!(metrics.audit_object_count, 1);
    }

    struct NeverHttpFetcher;
    impl Fetcher for NeverHttpFetcher {
        fn fetch(&self, _uri: &str) -> Result<Vec<u8>, String> {
            Err("http fetch disabled in test".to_string())
        }
    }

    struct FailingRsyncFetcher;
    impl RsyncFetcher for FailingRsyncFetcher {
        fn fetch_objects(
            &self,
            _rsync_base_uri: &str,
        ) -> Result<Vec<(String, Vec<u8>)>, RsyncFetchError> {
            Err(RsyncFetchError::Fetch("rsync disabled in test".to_string()))
        }
    }

    fn stage_test_runner<'a>(
        store: &'a RocksStore,
        policy: &'a Policy,
    ) -> Rpkiv1PublicationPointRunner<'a> {
        Rpkiv1PublicationPointRunner {
            store,
            policy,
            http_fetcher: &NeverHttpFetcher,
            rsync_fetcher: &FailingRsyncFetcher,
            validation_time: time::OffsetDateTime::now_utc(),
            timing: None,
            download_log: None,
            replay_archive_index: None,
            replay_delta_index: None,
            rrdp_dedup: false,
            rrdp_repo_cache: Mutex::new(HashMap::new()),
            rsync_dedup: false,
            rsync_repo_cache: Mutex::new(HashMap::new()),
            current_repo_index: None,
            repo_sync_runtime: None,
            parallel_phase2_config: None,
            parallel_roa_worker_pool: None,
            ccr_accumulator: None,
            persist_vcir: true,
            enable_roa_validation_cache: false,
            enable_child_certificate_validation_cache: false,
            publication_point_cache_observe_only: false,
            enable_publication_point_validation_cache: false,
        }
    }

    fn stage_test_handle(manifest_rsync_uri: &str) -> CaInstanceHandle {
        CaInstanceHandle {
            tal_id: "test".to_string(),
            ca_certificate: CaCertificateRef::inline_der(vec![1]),
            ca_certificate_rsync_uri: Some("rsync://example.test/repo/ca.cer".to_string()),
            effective_ip_resources: None,
            effective_as_resources: None,
            manifest_rsync_uri: manifest_rsync_uri.to_string(),
            publication_point_rsync_uri: "rsync://example.test/repo/".to_string(),
            rsync_base_uri: "rsync://example.test/repo/".to_string(),
            rrdp_notification_uri: None,
            parent_manifest_rsync_uri: None,
            depth: 0,
        }
    }

    fn stage_test_ready(id: u64) -> ReadyCaInstance {
        ReadyCaInstance {
            node: QueuedCaInstance {
                id,
                handle: stage_test_handle("rsync://example.test/repo/example.mft"),
                parent_id: None,
                discovered_from: None,
            },
            repo_outcome: stage_test_repo_outcome(),
            ready_enqueued_at: Instant::now(),
        }
    }

    fn stage_test_repo_outcome() -> RepoSyncRuntimeOutcome {
        RepoSyncRuntimeOutcome {
            repo_sync_ok: true,
            repo_sync_err: None,
            repo_sync_source: Some("rsync".to_string()),
            repo_sync_phase: Some("rsync".to_string()),
            repo_sync_duration_ms: 0,
            warnings: Vec::new(),
        }
    }

    fn stage_test_child(
        manifest_rsync_uri: &str,
        ca_certificate_rsync_uri: &str,
    ) -> DiscoveredChildCaInstance {
        DiscoveredChildCaInstance {
            handle: CaInstanceHandle {
                ca_certificate_rsync_uri: Some(ca_certificate_rsync_uri.to_string()),
                ..stage_test_handle(manifest_rsync_uri)
            },
            discovered_from: DiscoveredFrom {
                parent_manifest_rsync_uri: "rsync://example.test/repo/example.mft".to_string(),
                child_ca_certificate_rsync_uri: ca_certificate_rsync_uri.to_string(),
                child_ca_certificate_sha256_hex: "55".repeat(32),
            },
            child_entry_projection: None,
        }
    }

    fn stage_test_fresh_stage() -> FreshPublicationPointStage {
        FreshPublicationPointStage {
            fresh_point: FreshValidatedPublicationPoint {
                manifest_rsync_uri: "rsync://example.test/repo/example.mft".to_string(),
                publication_point_rsync_uri: "rsync://example.test/repo/".to_string(),
                manifest_number_be: vec![1],
                this_update: PackTime {
                    rfc3339_utc: "2026-04-21T00:00:00Z".to_string(),
                },
                next_update: PackTime {
                    rfc3339_utc: "2026-04-22T00:00:00Z".to_string(),
                },
                verified_at: PackTime {
                    rfc3339_utc: "2026-04-21T00:00:01Z".to_string(),
                },
                manifest_bytes: vec![1, 2, 3],
                files: Vec::new(),
            },
            issuer_ca_der: vec![1u8].into(),
            snapshot_prepare_timing: FreshPublicationPointTimingBreakdown::default(),
            snapshot_prepare_ms: 0,
            discovered_children: Vec::new(),
            child_audits: Vec::new(),
            discovered_router_keys: Vec::new(),
            child_discovery_ms: 0,
            warnings: Vec::new(),
        }
    }

    fn stage_test_metrics() -> ReadyStageMetrics {
        ReadyStageMetrics {
            ready_count: 1,
            manifest_rsync_uri: Some("rsync://example.test/repo/example.mft".to_string()),
            publication_point_rsync_uri: Some("rsync://example.test/repo/".to_string()),
            ..ReadyStageMetrics::default()
        }
    }

    #[test]
    fn compute_apply_fresh_error_runs_inline_fallback_and_finishes_err() {
        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
        let policy = Policy {
            sync_preference: SyncPreference::RsyncOnly,
            ca_failed_fetch_policy: CaFailedFetchPolicy::StopAllOutput,
            ..Policy::default()
        };
        let runner = stage_test_runner(&store, &policy);
        let config = TreeRunConfig::default();
        let mut next_id = 1u64;
        let mut ca_queue = VecDeque::new();
        let mut pending_roa_dispatch = VecDeque::new();
        let mut inflight_publication_points = HashMap::new();
        let mut pending_finalization = VecDeque::new();
        let mut finished = Vec::new();

        let (outcome, metrics) =
            compute_ready_publication_point_stage(&runner, stage_test_ready(0), 0);
        assert!(
            matches!(outcome, StageOutcome::FreshError(_)),
            "fresh staging against an empty store must fail"
        );
        let metrics = apply_ready_publication_point_stage(
            &runner,
            &mut next_id,
            &mut ca_queue,
            &mut pending_roa_dispatch,
            &mut inflight_publication_points,
            &mut pending_finalization,
            &mut finished,
            outcome,
            metrics,
            &config,
            false,
        );

        assert_eq!(metrics.ready_count, 1);
        assert_eq!(metrics.fallback_count, 1);
        assert_eq!(metrics.complete_count, 0);
        assert_eq!(metrics.staged_count, 0);
        assert_eq!(metrics.zero_task_count, 0);
        assert_eq!(finished.len(), 1);
        match &finished[0].result {
            FinishedPublicationPointResult::Err(_) => {}
            FinishedPublicationPointResult::Ok { .. } => {
                panic!("fallback without repository data must fail")
            }
        }
        assert!(ca_queue.is_empty());
        assert!(pending_roa_dispatch.is_empty());
        assert!(pending_finalization.is_empty());
        assert!(inflight_publication_points.is_empty());
        assert_eq!(next_id, 1);
    }

    #[test]
    fn apply_cache_hit_enqueues_children_sorted_and_finishes() {
        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
        let policy = Policy::default();
        let runner = stage_test_runner(&store, &policy);
        let config = TreeRunConfig::default();
        let mut next_id = 42u64;
        let mut ca_queue = VecDeque::new();
        let mut pending_roa_dispatch = VecDeque::new();
        let mut inflight_publication_points = HashMap::new();
        let mut pending_finalization = VecDeque::new();
        let mut finished = Vec::new();

        let mut result = sample_result();
        result.discovered_children = vec![
            stage_test_child(
                "rsync://example.test/repo/z.mft",
                "rsync://example.test/repo/z.cer",
            ),
            stage_test_child(
                "rsync://example.test/repo/a.mft",
                "rsync://example.test/repo/a.cer",
            ),
        ];
        let outcome = StageOutcome::CacheHit(Box::new(CacheHitOutcome {
            ready: stage_test_ready(7),
            publication_point_started: Instant::now(),
            result,
        }));
        let mut metrics = stage_test_metrics();
        metrics.complete_count = 1;
        metrics.discovered_children = 2;
        let metrics = apply_ready_publication_point_stage(
            &runner,
            &mut next_id,
            &mut ca_queue,
            &mut pending_roa_dispatch,
            &mut inflight_publication_points,
            &mut pending_finalization,
            &mut finished,
            outcome,
            metrics,
            &config,
            false,
        );

        assert_eq!(finished.len(), 1);
        match &finished[0].result {
            FinishedPublicationPointResult::Ok { .. } => {}
            FinishedPublicationPointResult::Err(err) => {
                panic!("cache hit must finish ok: {err}")
            }
        }
        assert_eq!(finished[0].node.id, 7);
        // Children are enqueued sorted by manifest URI with sequential ids
        // taken from next_id, exactly like the monolithic staging did.
        assert_eq!(ca_queue.len(), 2);
        assert_eq!(
            ca_queue[0].handle.manifest_rsync_uri,
            "rsync://example.test/repo/a.mft"
        );
        assert_eq!(
            ca_queue[1].handle.manifest_rsync_uri,
            "rsync://example.test/repo/z.mft"
        );
        assert_eq!(ca_queue[0].id, 42);
        assert_eq!(ca_queue[1].id, 43);
        assert_eq!(ca_queue[0].parent_id, Some(7));
        assert_eq!(ca_queue[1].parent_id, Some(7));
        assert_eq!(next_id, 44);
        assert!(pending_roa_dispatch.is_empty());
        assert!(pending_finalization.is_empty());
        assert!(inflight_publication_points.is_empty());
        assert_eq!(metrics.complete_count, 1);
        assert_eq!(metrics.discovered_children, 2);
    }

    #[test]
    fn apply_complete_enqueues_finalize_task_instead_of_finishing() {
        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
        let policy = Policy::default();
        let runner = stage_test_runner(&store, &policy);
        let config = TreeRunConfig::default();
        let mut next_id = 1u64;
        let mut ca_queue = VecDeque::new();
        let mut pending_roa_dispatch = VecDeque::new();
        let mut inflight_publication_points = HashMap::new();
        let mut pending_finalization = VecDeque::new();
        let mut finished = Vec::new();

        let outcome = StageOutcome::Complete(Box::new(CompleteOutcome {
            ready: stage_test_ready(9),
            publication_point_started: Instant::now(),
            fresh_stage: stage_test_fresh_stage(),
            warnings: Vec::new(),
            objects: sample_result().objects,
        }));
        let mut metrics = stage_test_metrics();
        metrics.complete_count = 1;
        let metrics = apply_ready_publication_point_stage(
            &runner,
            &mut next_id,
            &mut ca_queue,
            &mut pending_roa_dispatch,
            &mut inflight_publication_points,
            &mut pending_finalization,
            &mut finished,
            outcome,
            metrics,
            &config,
            false,
        );

        assert!(
            finished.is_empty(),
            "complete staging must defer the finalize to the worker queue"
        );
        assert_eq!(pending_finalization.len(), 1);
        let task = pending_finalization.pop_front().expect("finalize task");
        let state = task.state;
        assert_eq!(state.node.id, 9);
        assert_eq!(state.task_count, 0);
        assert!(state.finalize_enqueued_at.is_some());
        assert!(state.results.is_empty());
        assert!(matches!(
            state.objects_prepare,
            ParallelObjectsPrepare::Complete(_)
        ));
        assert!(inflight_publication_points.is_empty());
        assert!(pending_roa_dispatch.is_empty());
        assert!(ca_queue.is_empty());
        assert_eq!(next_id, 1);
        assert_eq!(metrics.complete_count, 1);
    }

    #[test]
    fn finalize_worker_complete_arm_finalizes_without_reduce() {
        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
        let policy = Policy::default();
        let mut runner = stage_test_runner(&store, &policy);
        runner.persist_vcir = false;

        let state = InflightPublicationPoint {
            node: QueuedCaInstance {
                id: 3,
                handle: stage_test_handle("rsync://example.test/repo/example.mft"),
                parent_id: None,
                discovered_from: None,
            },
            fresh_stage: stage_test_fresh_stage(),
            objects_prepare: ParallelObjectsPrepare::Complete(sample_result().objects),
            repo_outcome: stage_test_repo_outcome(),
            warnings: Vec::new(),
            started_at: Instant::now(),
            objects_started_at: Instant::now(),
            task_count: 0,
            tasks_submitted: 0,
            first_task_submitted_at: None,
            last_task_submitted_at: None,
            first_result_at: None,
            last_result_at: None,
            worker_ms_total: 0,
            worker_ms_max: 0,
            queue_wait_ms_total: 0,
            queue_wait_ms_max: 0,
            finalize_enqueued_at: Some(Instant::now()),
            results: Vec::new(),
        };
        let result = finalize_publication_point_state(&runner, state, false);

        match result.finished.result {
            FinishedPublicationPointResult::Ok { source, .. } => {
                assert_eq!(source, PublicationPointSource::Fresh);
            }
            FinishedPublicationPointResult::Err(err) => {
                panic!("complete finalize must succeed: {err}")
            }
        }
        assert_eq!(result.finished.node.id, 3);
        assert_eq!(result.metrics.reduce_ms, 0);
        assert_eq!(result.metrics.locked_files, 0);
    }

    #[test]
    fn is_complete_waits_for_staging_inflight() {
        let ca_queue = VecDeque::new();
        let ready_queue = VecDeque::new();
        let ca_waiting_repo_by_identity = HashMap::new();
        let pending_roa_dispatch = VecDeque::new();
        let inflight_publication_points = HashMap::new();
        let pending_finalization = VecDeque::new();
        let config = TreeRunConfig::default();

        // Everything drained and no staging in flight: the loop may exit.
        assert!(is_complete(
            &ca_queue,
            &ready_queue,
            &ca_waiting_repo_by_identity,
            &pending_roa_dispatch,
            &inflight_publication_points,
            &pending_finalization,
            0,
            0,
            0,
            &config,
        ));
        // A submitted stage task whose result has not been collected yet must
        // keep the loop alive, otherwise its publication point would be lost.
        assert!(!is_complete(
            &ca_queue,
            &ready_queue,
            &ca_waiting_repo_by_identity,
            &pending_roa_dispatch,
            &inflight_publication_points,
            &pending_finalization,
            0,
            1,
            0,
            &config,
        ));
    }

    #[test]
    fn event_poll_timeout_stays_awake_while_staging() {
        let ca_queue = VecDeque::new();
        let ready_queue = VecDeque::new();
        let pending_roa_dispatch = VecDeque::new();
        let inflight_publication_points = HashMap::new();
        let pending_finalization = VecDeque::new();
        let config = TreeRunConfig::default();

        // Staging in flight: poll must not sleep, or stage results pile up.
        assert_eq!(
            event_poll_timeout(
                &ca_queue,
                &ready_queue,
                &pending_roa_dispatch,
                &inflight_publication_points,
                &pending_finalization,
                0,
                1,
                0,
                &config,
            ),
            Duration::from_millis(0)
        );
        // Without staging the historical tiers are unchanged.
        assert_eq!(
            event_poll_timeout(
                &ca_queue,
                &ready_queue,
                &pending_roa_dispatch,
                &inflight_publication_points,
                &pending_finalization,
                1,
                0,
                0,
                &config,
            ),
            Duration::from_millis(10)
        );
        assert_eq!(
            event_poll_timeout(
                &ca_queue,
                &ready_queue,
                &pending_roa_dispatch,
                &inflight_publication_points,
                &pending_finalization,
                0,
                0,
                0,
                &config,
            ),
            Duration::from_millis(50)
        );
    }

    struct MockStageSubmitter {
        capacity: usize,
        submitted_ids: Vec<u64>,
    }

    impl ReadyStageSubmitter for MockStageSubmitter {
        fn try_submit_ready_stage(
            &mut self,
            task: super::ReadyStageTask,
        ) -> Result<(), crate::parallel::object_worker::ObjectWorkerSubmitError<super::ReadyStageTask>>
        {
            if self.submitted_ids.len() < self.capacity {
                self.submitted_ids.push(task.ready.node.id);
                Ok(())
            } else {
                Err(
                    crate::parallel::object_worker::ObjectWorkerSubmitError::QueueFull {
                        worker_index: 0,
                        task,
                    },
                )
            }
        }
    }

    #[test]
    fn submit_ready_batch_requeues_on_backpressure_without_loss_or_duplication() {
        let mut ready_queue = VecDeque::new();
        for id in [10, 11, 12] {
            ready_queue.push_back(stage_test_ready(id));
        }
        let mut staging_inflight = 0usize;

        // First turn: only two tasks fit, the third must be returned to the
        // head of the ready queue.
        let mut submitter = MockStageSubmitter {
            capacity: 2,
            submitted_ids: Vec::new(),
        };
        let metrics = submit_ready_batch_to_stage_pool(
            &mut submitter,
            &mut ready_queue,
            &mut staging_inflight,
            256,
            Duration::from_secs(60),
        )
        .expect("dispatch");
        assert_eq!(metrics.submitted, 2);
        assert!(metrics.queue_full);
        assert_eq!(staging_inflight, 2);
        assert_eq!(submitter.submitted_ids, vec![10, 11]);
        assert_eq!(ready_queue.len(), 1);
        assert_eq!(ready_queue[0].node.id, 12);

        // Next turn retries the requeued publication point; across both turns
        // every id is submitted exactly once.
        let mut submitter = MockStageSubmitter {
            capacity: 8,
            submitted_ids: Vec::new(),
        };
        let metrics = submit_ready_batch_to_stage_pool(
            &mut submitter,
            &mut ready_queue,
            &mut staging_inflight,
            256,
            Duration::from_secs(60),
        )
        .expect("dispatch");
        assert_eq!(metrics.submitted, 1);
        assert!(!metrics.queue_full);
        assert_eq!(staging_inflight, 3);
        assert_eq!(submitter.submitted_ids, vec![12]);
        assert!(ready_queue.is_empty());
    }
}
