use crate::analysis::timing::TimingHandle;
use crate::audit::{
    AuditObjectKind, AuditObjectResult, AuditWarning, ObjectAuditEntry, PublicationPointAudit,
    sha256_hex, sha256_hex_from_32,
};
use crate::audit_downloads::DownloadLogHandle;
use crate::ccr::CcrAccumulator;
use crate::current_repo_index::CurrentRepoIndexHandle;
use crate::data_model::aspa::AspaObject;
use crate::data_model::common::BigUnsigned;
use crate::data_model::crl::RpkixCrl;
use crate::data_model::manifest::ManifestObject;
use crate::data_model::rc::{ResourceCertificate, SubjectInfoAccess};
use crate::data_model::roa::{RoaAfi, RoaObject};
use crate::data_model::router_cert::{
    BgpsecRouterCertificate, BgpsecRouterCertificateDecodeError, BgpsecRouterCertificatePathError,
    BgpsecRouterCertificateProfileError,
};
use crate::fetch::rsync::RsyncFetcher;
use crate::parallel::config::ParallelPhase2Config;
use crate::parallel::repo_runtime::{RepoSyncRuntime, RepoSyncRuntimeOutcome};
use crate::policy::{Policy, ResourceValidationMode};
use crate::replay::archive::ReplayArchiveIndex;
use crate::replay::delta_archive::ReplayDeltaArchiveIndex;
use crate::report::{RfcRef, Warning};
use crate::storage::{
    CHILD_CERTIFICATE_CACHE_ALGORITHM_VERSION, CHILD_CERTIFICATE_CACHE_SCHEMA_VERSION,
    ChildCertificateCachePayload, ChildCertificateCacheProjection,
    ChildCertificateCacheRouterKeyProjection, PackFile, PackTime, PublicationPointCacheChild,
    PublicationPointCacheOutput, PublicationPointCacheProjection,
    PublicationPointCacheProjectionWriteAction, RawByHashEntry, RoaCacheProjectionContext,
    RocksStore, ValidatedCaInstanceResult, VcirArtifactKind, VcirArtifactRole,
    VcirArtifactValidationStatus, VcirAuditSummary, VcirCcrManifestProjection, VcirChildEntry,
    VcirFailedFetchReuseIdentity, VcirInstanceGate, VcirLocalOutput, VcirLocalOutputPayload,
    VcirOutputType, VcirRelatedArtifact, VcirReplaceTimingBreakdown, VcirSourceObjectType,
    VcirSummary,
};
use crate::sync::repo::{
    sync_publication_point, sync_publication_point_replay, sync_publication_point_replay_delta,
};
use crate::sync::rrdp::Fetcher;
use crate::validation::ca_instance::ca_instance_uris_from_ca_certificate;
use crate::validation::ca_path::{
    CaPathError, IssuerEffectiveResourcesIndex, ValidatedSubordinateCaLite,
    validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources,
};
use crate::validation::manifest::{
    FreshPublicationPointTimingBreakdown, FreshValidatedPublicationPoint, ManifestFreshError,
    PublicationPointData, PublicationPointSource,
    process_manifest_publication_point_fresh_after_repo_sync_with_timing,
};
use crate::validation::objects::{
    AspaAttestation, ParallelRoaWorkerPool, RoaValidationCacheInput, RoaValidationCacheView,
    RouterKeyPayload, Vrp, process_publication_point_for_issuer_parallel_roa_with_cache_options,
    process_publication_point_for_issuer_parallel_roa_with_pool_cache_options,
};
use crate::validation::publication_point::PublicationPointSnapshot;
use crate::validation::tree::{
    CaCertificateRef, CaInstanceHandle, DiscoveredChildCaInstance, DiscoveredChildEntryProjection,
    PublicationPointRunResult, PublicationPointRunner,
};
use sha2::Digest;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};

use base64::Engine as _;
use x509_parser::prelude::FromDer;
use x509_parser::x509::SubjectPublicKeyInfo;

use crate::ccr::manifest_location::select_manifest_signed_object_location;

const PUBLICATION_POINT_CACHE_CHILD_RESTORE_PARALLEL_MIN_CHILDREN: usize = 256;
const PUBLICATION_POINT_CACHE_CHILD_RESTORE_MAX_WORKERS: usize = 16;
const CHILD_CERTIFICATE_CACHE_MMAP_MIN_CER_COUNT: usize = 2048;

fn sha256_hex_to_32(hex_value: &str) -> [u8; 32] {
    let mut out = [0u8; 32];
    hex::decode_to_slice(hex_value, &mut out).expect("internal sha256 hex should decode");
    out
}

#[derive(Clone, Debug, Default)]
pub(crate) struct BuildVcirTimingBreakdown {
    pub(crate) select_crl_ms: u64,
    pub(crate) current_ca_decode_ms: u64,
    pub(crate) local_outputs_ms: u64,
    pub(crate) child_entries_ms: u64,
    pub(crate) related_artifacts_ms: u64,
    pub(crate) struct_build_ms: u64,
}

#[derive(Clone, Debug, Default)]
pub(crate) struct PersistVcirTimingBreakdown {
    pub(crate) embedded_collect_ms: u64,
    pub(crate) embedded_store_ms: u64,
    pub(crate) build_vcir_ms: u64,
    pub(crate) replace_vcir_ms: u64,
    pub(crate) publication_point_cache_future_notbefore_guarded: bool,
    pub(crate) build_vcir: BuildVcirTimingBreakdown,
    pub(crate) replace_vcir: VcirReplaceTimingBreakdown,
}

#[derive(Clone, Debug)]
pub(crate) struct FreshPublicationPointStage {
    pub(crate) fresh_point: FreshValidatedPublicationPoint,
    pub(crate) issuer_ca_der: Arc<[u8]>,
    pub(crate) snapshot_prepare_timing: FreshPublicationPointTimingBreakdown,
    pub(crate) snapshot_prepare_ms: u64,
    pub(crate) discovered_children: Vec<DiscoveredChildCaInstance>,
    pub(crate) child_audits: Vec<ObjectAuditEntry>,
    pub(crate) discovered_router_keys: Vec<RouterKeyPayload>,
    pub(crate) child_discovery_ms: u64,
    pub(crate) warnings: Vec<Warning>,
}

#[derive(Debug)]
pub(crate) struct FreshPublicationPointStageError {
    pub(crate) error: ManifestFreshError,
    pub(crate) snapshot_prepare_ms: u64,
}

#[derive(Clone, Debug)]
pub(crate) struct FreshPublicationPointFinalizeOutput {
    pub(crate) result: PublicationPointRunResult,
    pub(crate) snapshot_pack_ms: u64,
    pub(crate) persist_vcir_ms: u64,
    pub(crate) persist_vcir_timing: PersistVcirTimingBreakdown,
    pub(crate) ccr_projection_build_ms: u64,
    pub(crate) ccr_append_ms: u64,
    pub(crate) audit_build_ms: u64,
}

pub struct Rpkiv1PublicationPointRunner<'a> {
    pub store: &'a RocksStore,
    pub policy: &'a Policy,
    pub http_fetcher: &'a dyn Fetcher,
    pub rsync_fetcher: &'a dyn RsyncFetcher,
    pub validation_time: time::OffsetDateTime,
    pub timing: Option<TimingHandle>,
    pub download_log: Option<DownloadLogHandle>,
    pub replay_archive_index: Option<Arc<ReplayArchiveIndex>>,
    pub replay_delta_index: Option<Arc<ReplayDeltaArchiveIndex>>,
    /// In-run RRDP dedup: when RRDP is enabled, only sync each `rrdp_notification_uri` once per run.
    ///
    /// - If RRDP succeeded for a repo, later publication points referencing that same RRDP repo
    ///   skip network fetches and reuse the already-populated current repository view.
    /// - If RRDP failed for a repo, later publication points skip RRDP attempts and go straight
    ///   to rsync for their own `rsync_base_uri` (still per-publication-point).
    pub rrdp_dedup: bool,
    pub rrdp_repo_cache: Mutex<HashMap<String, bool>>, // notification_uri -> rrdp_ok

    /// In-run rsync dedup: when rsync is used, only sync each `rsync_base_uri` once per run.
    ///
    /// This reduces duplicate rsync network fetches when multiple publication points share the
    /// same `rsync_base_uri` (observed in APNIC full sync timing reports).
    pub rsync_dedup: bool,
    pub rsync_repo_cache: Mutex<HashMap<String, bool>>, // rsync_base_uri -> rsync_ok
    pub current_repo_index: Option<CurrentRepoIndexHandle>,
    pub repo_sync_runtime: Option<Arc<dyn RepoSyncRuntime>>,
    pub parallel_phase2_config: Option<ParallelPhase2Config>,
    pub parallel_roa_worker_pool: Option<ParallelRoaWorkerPool>,
    pub ccr_accumulator: Option<Mutex<CcrAccumulator>>,
    /// When false, skip VCIR persistence and per-output VCIR projection building.
    ///
    /// This is intended for replay/compare-only runs where the caller does not need
    /// the resulting DB to be reused by a later delta run.
    pub persist_vcir: bool,
    pub enable_roa_validation_cache: bool,
    pub enable_child_certificate_validation_cache: bool,
    pub publication_point_cache_observe_only: bool,
    pub enable_publication_point_validation_cache: bool,
}

impl<'a> Rpkiv1PublicationPointRunner<'a> {
    pub(crate) fn roa_validation_cache_view_for_fresh_point(
        &self,
        manifest_rsync_uri: &str,
    ) -> Option<RoaValidationCacheView> {
        if !self.enable_roa_validation_cache {
            return None;
        }
        let load_started = std::time::Instant::now();
        let loaded_projection = self.store.get_roa_cache_projection(manifest_rsync_uri);
        if let Some(timing) = self.timing.as_ref() {
            timing.record_phase_nanos(
                "roa_validation_cache_projection_load_total",
                load_started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
            );
        }
        match loaded_projection {
            Ok(Some(projection)) => {
                if let Some(timing) = self.timing.as_ref() {
                    timing
                        .record_count("roa_validation_cache_projection_hit_publication_points", 1);
                }
                let view_started = std::time::Instant::now();
                let view =
                    RoaValidationCacheView::from_projection(&projection, self.validation_time);
                if let Some(timing) = self.timing.as_ref() {
                    timing.record_phase_nanos(
                        "roa_validation_cache_projection_build_total",
                        view_started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
                    );
                }
                Some(view)
            }
            Ok(None) => {
                if let Some(timing) = self.timing.as_ref() {
                    timing.record_count(
                        "roa_validation_cache_projection_missing_publication_points",
                        1,
                    );
                }
                None
            }
            Err(err) => {
                if let Some(timing) = self.timing.as_ref() {
                    timing.record_count("roa_validation_cache_projection_load_errors", 1);
                }
                crate::progress_log::emit(
                    "roa_validation_cache_projection_load_error",
                    serde_json::json!({
                        "manifest_rsync_uri": manifest_rsync_uri,
                        "error": err.to_string(),
                    }),
                );
                None
            }
        }
    }

    pub(crate) fn observe_or_reuse_publication_point_cache(
        &self,
        ca: &CaInstanceHandle,
        repo_sync_source: Option<&str>,
        repo_sync_phase: Option<&str>,
        repo_sync_duration_ms: u64,
        repo_sync_err: Option<&str>,
        warnings: &[Warning],
    ) -> Option<PublicationPointRunResult> {
        if !self.publication_point_cache_observe_only
            && !self.enable_publication_point_validation_cache
        {
            return None;
        }

        let lookup_started = std::time::Instant::now();
        if let Some(timing) = self.timing.as_ref() {
            timing.record_count("publication_point_cache_lookup_total", 1);
        }
        let projection = match self
            .store
            .get_publication_point_cache_projection_cached(&ca.manifest_rsync_uri)
        {
            Ok(Some(projection)) => projection,
            Ok(None) => {
                self.finish_publication_point_cache_miss(ca, "missing_projection", lookup_started);
                return None;
            }
            Err(e) => {
                self.finish_publication_point_cache_miss(
                    ca,
                    "projection_load_error",
                    lookup_started,
                );
                crate::progress_log::emit(
                    "publication_point_cache_lookup_error",
                    serde_json::json!({
                        "manifest_rsync_uri": ca.manifest_rsync_uri,
                        "error": e.to_string(),
                    }),
                );
                return None;
            }
        };

        let current_identity = match self.current_publication_point_cache_identity(ca) {
            Ok(identity) => identity,
            Err(reason) => {
                self.finish_publication_point_cache_miss(ca, reason.as_str(), lookup_started);
                return None;
            }
        };

        if projection.ca_cert_uri != ca.ca_certificate_rsync_uri {
            self.finish_publication_point_cache_miss(ca, "ca_uri_mismatch", lookup_started);
            return None;
        }
        if projection.ca_cert_sha256 != current_identity.ca_cert_sha256 {
            self.finish_publication_point_cache_miss(ca, "ca_hash_mismatch", lookup_started);
            return None;
        }
        if projection.manifest_sha256 != current_identity.manifest_sha256 {
            self.finish_publication_point_cache_miss(ca, "manifest_hash_mismatch", lookup_started);
            return None;
        }
        if projection.tal_id != ca.tal_id {
            self.finish_publication_point_cache_miss(ca, "tal_mismatch", lookup_started);
            return None;
        }
        if projection.ta_context_digest != current_identity.ta_context_digest {
            self.finish_publication_point_cache_miss(ca, "ta_context_mismatch", lookup_started);
            return None;
        }
        if projection.ca_validation_context_digest != current_identity.ca_validation_context_digest
        {
            self.finish_publication_point_cache_miss(ca, "parent_context_mismatch", lookup_started);
            return None;
        }
        if projection.validation_policy_fingerprint != current_identity.policy_fingerprint {
            self.finish_publication_point_cache_miss(ca, "policy_mismatch", lookup_started);
            return None;
        }

        let instance_not_before =
            match parse_snapshot_time_value(&projection.instance_effective_not_before) {
                Ok(value) => value,
                Err(_) => {
                    self.finish_publication_point_cache_miss(
                        ca,
                        "instance_not_before_invalid",
                        lookup_started,
                    );
                    return None;
                }
            };
        let instance_until = match parse_snapshot_time_value(&projection.instance_effective_until) {
            Ok(value) => value,
            Err(_) => {
                self.finish_publication_point_cache_miss(
                    ca,
                    "instance_until_invalid",
                    lookup_started,
                );
                return None;
            }
        };
        if self.validation_time < instance_not_before || self.validation_time >= instance_until {
            self.finish_publication_point_cache_miss(ca, "instance_time_gate_miss", lookup_started);
            return None;
        }
        if let Err(reason) =
            publication_point_cache_projection_items_valid(&projection, self.validation_time)
        {
            self.finish_publication_point_cache_miss(ca, reason, lookup_started);
            return None;
        }

        if let Some(timing) = self.timing.as_ref() {
            let lookup_nanos = lookup_started
                .elapsed()
                .as_nanos()
                .min(u128::from(u64::MAX)) as u64;
            timing.record_count("publication_point_cache_theoretical_hits", 1);
            timing.record_phase_nanos("publication_point_cache_lookup_total", lookup_nanos);
            timing.record_phase_nanos("publication_point_cache_lookup_hit_total", lookup_nanos);
            timing.record_phase_nanos(
                "publication_point_cache_lookup_duration_total",
                lookup_nanos,
            );
        }
        if self.publication_point_cache_observe_only {
            return None;
        }

        match self.build_publication_point_cache_result(
            ca,
            projection,
            repo_sync_source,
            repo_sync_phase,
            repo_sync_duration_ms,
            repo_sync_err,
            warnings,
        ) {
            Ok(result) => {
                if let Some(timing) = self.timing.as_ref() {
                    timing.record_count("publication_point_cache_reuse_hits", 1);
                }
                Some(result)
            }
            Err(e) => {
                self.record_publication_point_cache_miss("reuse_build_error");
                crate::progress_log::emit(
                    "publication_point_cache_reuse_error",
                    serde_json::json!({
                        "manifest_rsync_uri": ca.manifest_rsync_uri,
                        "error": e,
                    }),
                );
                None
            }
        }
    }

    fn finish_publication_point_cache_miss(
        &self,
        ca: &CaInstanceHandle,
        reason: &str,
        lookup_started: std::time::Instant,
    ) {
        self.record_publication_point_cache_miss(reason);
        let lookup_nanos = lookup_started
            .elapsed()
            .as_nanos()
            .min(u128::from(u64::MAX)) as u64;
        if let Some(timing) = self.timing.as_ref() {
            timing.record_phase_nanos("publication_point_cache_lookup_miss_total", lookup_nanos);
            timing.record_phase_nanos(
                "publication_point_cache_lookup_duration_total",
                lookup_nanos,
            );
        }
        let elapsed_ms = lookup_nanos / 1_000_000;
        if elapsed_ms >= crate::progress_log::pp_cache_slow_threshold_ms() {
            crate::progress_log::emit(
                "publication_point_cache_miss_slow",
                serde_json::json!({
                    "manifest_rsync_uri": ca.manifest_rsync_uri.as_str(),
                    "publication_point_rsync_uri": ca.publication_point_rsync_uri.as_str(),
                    "ca_certificate_rsync_uri": ca.ca_certificate_rsync_uri.as_deref(),
                    "reason": reason,
                    "elapsed_ms": elapsed_ms,
                    "slow_threshold_ms": crate::progress_log::pp_cache_slow_threshold_ms(),
                }),
            );
        }
    }

    fn record_publication_point_cache_miss(&self, reason: &str) {
        if let Some(timing) = self.timing.as_ref() {
            timing.record_count("publication_point_cache_miss_total", 1);
            match reason {
                "missing_projection" => {
                    timing.record_count("publication_point_cache_miss_missing_projection", 1)
                }
                "projection_load_error" => {
                    timing.record_count("publication_point_cache_miss_projection_load_error", 1)
                }
                "current_manifest_missing" => {
                    timing.record_count("publication_point_cache_miss_current_manifest_missing", 1)
                }
                "ca_uri_mismatch" => {
                    timing.record_count("publication_point_cache_miss_ca_uri_mismatch", 1)
                }
                "ca_hash_mismatch" => {
                    timing.record_count("publication_point_cache_miss_ca_hash_mismatch", 1)
                }
                "manifest_hash_mismatch" => {
                    timing.record_count("publication_point_cache_miss_manifest_hash_mismatch", 1)
                }
                "tal_mismatch" => {
                    timing.record_count("publication_point_cache_miss_tal_mismatch", 1)
                }
                "ta_context_mismatch" => {
                    timing.record_count("publication_point_cache_miss_ta_context_mismatch", 1)
                }
                "parent_context_mismatch" => {
                    timing.record_count("publication_point_cache_miss_parent_context_mismatch", 1)
                }
                "policy_mismatch" => {
                    timing.record_count("publication_point_cache_miss_policy_mismatch", 1)
                }
                "instance_not_before_invalid" => timing.record_count(
                    "publication_point_cache_miss_instance_not_before_invalid",
                    1,
                ),
                "instance_until_invalid" => {
                    timing.record_count("publication_point_cache_miss_instance_until_invalid", 1)
                }
                "instance_time_gate_miss" => {
                    timing.record_count("publication_point_cache_miss_instance_time_gate", 1)
                }
                "output_time_gate_miss" => {
                    timing.record_count("publication_point_cache_miss_output_time_gate", 1)
                }
                "child_time_gate_miss" => {
                    timing.record_count("publication_point_cache_miss_child_time_gate", 1)
                }
                "reuse_build_error" => {
                    timing.record_count("publication_point_cache_miss_reuse_build_error", 1)
                }
                _ => timing.record_count("publication_point_cache_miss_other", 1),
            }
        }
    }

    fn current_publication_point_cache_identity(
        &self,
        ca: &CaInstanceHandle,
    ) -> Result<PublicationPointCacheIdentity, String> {
        let ca_cert_sha256 = match ca.ca_certificate_rsync_uri.as_deref() {
            Some(uri) => self
                .current_hash_for_uri(uri)
                .or_else(|| ca.ca_certificate_sha256_32())
                .ok_or_else(|| "current_ca_certificate_hash_missing".to_string())?,
            None => ca
                .ca_certificate_sha256_32()
                .ok_or_else(|| "current_ca_certificate_hash_missing".to_string())?,
        };
        let manifest_sha256 = self
            .current_hash_for_uri(&ca.manifest_rsync_uri)
            .ok_or_else(|| "current_manifest_missing".to_string())?;
        Ok(PublicationPointCacheIdentity {
            ca_cert_sha256,
            manifest_sha256,
            ta_context_digest: ta_context_digest_for_ca(ca),
            ca_validation_context_digest: ca_validation_context_digest_for_ca(ca),
            policy_fingerprint: publication_point_cache_policy_fingerprint(self.policy),
        })
    }

    fn current_hash_for_uri(&self, uri: &str) -> Option<[u8; 32]> {
        if let Some(index) = self.current_repo_index.as_ref() {
            if let Ok(index) = index.read() {
                if let Some(entry) = index.get_by_uri(uri) {
                    return Some(entry.current_hash);
                }
            }
        }
        self.store
            .load_current_object_with_hash_by_uri(uri)
            .ok()
            .flatten()
            .map(|entry| entry.current_hash)
    }

    fn build_publication_point_cache_result(
        &self,
        ca: &CaInstanceHandle,
        projection: PublicationPointCacheProjection,
        repo_sync_source: Option<&str>,
        repo_sync_phase: Option<&str>,
        repo_sync_duration_ms: u64,
        repo_sync_err: Option<&str>,
        warnings: &[Warning],
    ) -> Result<PublicationPointRunResult, String> {
        let build_started = std::time::Instant::now();
        let mut warnings = warnings.to_vec();
        let output_reuse_count = projection.outputs.len() as u64;
        let child_reuse_count = projection.children.len() as u64;
        let related_object_reuse_count = projection.related_objects.len() as u64;
        let build_objects_started = std::time::Instant::now();
        let mut objects = build_objects_output_from_publication_point_cache_projection(
            &projection,
            self.validation_time,
            &mut warnings,
        );
        let build_objects_ms = self.record_publication_point_cache_phase_ms(
            "publication_point_cache_build_objects_total",
            build_objects_started,
        );
        self.record_publication_point_step_ms(
            &ca.manifest_rsync_uri,
            "publication_point_cache_build_objects",
            build_objects_ms,
        );
        let restore_children_started = std::time::Instant::now();
        let child_restore_workers = self.publication_point_cache_child_restore_worker_count();
        let (discovered_children, child_audits) = restore_children_from_publication_point_cache(
            self.store,
            ca,
            &projection,
            self.validation_time,
            &mut warnings,
            child_restore_workers,
            self.timing.as_ref(),
        );
        let restore_children_ms = self.record_publication_point_cache_phase_ms(
            "publication_point_cache_restore_children_total",
            restore_children_started,
        );
        self.record_publication_point_step_ms(
            &ca.manifest_rsync_uri,
            "publication_point_cache_restore_children",
            restore_children_ms,
        );
        let ccr_projection = projection.ccr_manifest_projection.clone();
        let ccr_append_started = std::time::Instant::now();
        self.append_ccr_manifest_projection(&ccr_projection)?;
        let ccr_append_ms = self.record_publication_point_cache_phase_ms(
            "publication_point_cache_ccr_append_total",
            ccr_append_started,
        );
        self.record_publication_point_step_ms(
            &ca.manifest_rsync_uri,
            "publication_point_cache_ccr_append",
            ccr_append_ms,
        );
        let audit_build_started = std::time::Instant::now();
        let audit = build_publication_point_audit_from_publication_point_cache_projection(
            ca,
            PublicationPointSource::PublicationPointCache,
            repo_sync_source,
            repo_sync_phase,
            Some(repo_sync_duration_ms),
            repo_sync_err,
            &projection,
            self.validation_time,
            &warnings,
            &objects,
            &child_audits,
        );
        let audit_build_ms = self.record_publication_point_cache_phase_ms(
            "publication_point_cache_audit_build_total",
            audit_build_started,
        );
        self.record_publication_point_step_ms(
            &ca.manifest_rsync_uri,
            "publication_point_cache_audit_build",
            audit_build_ms,
        );
        let audit_object_count = audit.objects.len() as u64;
        let cir_cached_objects = audit.objects.clone();
        let cir_cached_objects_count = cir_cached_objects.len() as u64;
        objects.local_outputs_cache.clear();
        let total_ms = self.record_publication_point_cache_phase_ms(
            "publication_point_cache_reuse_build_total",
            build_started,
        );
        self.record_publication_point_step_ms(
            &ca.manifest_rsync_uri,
            "publication_point_cache_reuse_build",
            total_ms,
        );
        if let Some(timing) = self.timing.as_ref() {
            timing.record_count("publication_point_cache_outputs_reused", output_reuse_count);
            timing.record_count("publication_point_cache_children_reused", child_reuse_count);
            timing.record_count(
                "publication_point_cache_related_objects_reused",
                related_object_reuse_count,
            );
            timing.record_count(
                "publication_point_cache_audit_objects_reused",
                audit_object_count,
            );
            if total_ms > 0 {
                timing.record_count("publication_point_cache_reuse_nonzero_ms_hits", 1);
                timing.record_count("publication_point_cache_reuse_nonzero_ms_total", total_ms);
            }
        }
        if total_ms >= crate::progress_log::pp_cache_slow_threshold_ms() {
            crate::progress_log::emit(
                "publication_point_cache_reuse_slow",
                serde_json::json!({
                    "manifest_rsync_uri": ca.manifest_rsync_uri.as_str(),
                    "publication_point_rsync_uri": ca.publication_point_rsync_uri.as_str(),
                    "repo_sync_source": repo_sync_source,
                    "repo_sync_phase": repo_sync_phase,
                    "repo_sync_duration_ms": repo_sync_duration_ms,
                    "repo_sync_err": repo_sync_err,
                    "outputs_reused": output_reuse_count,
                    "children_reused": child_reuse_count,
                    "related_objects_reused": related_object_reuse_count,
                    "audit_objects_reused": audit_object_count,
                    "build_objects_ms": build_objects_ms,
                    "restore_children_ms": restore_children_ms,
                    "restore_children_workers": child_restore_workers,
                    "ccr_append_ms": ccr_append_ms,
                    "audit_build_ms": audit_build_ms,
                    "total_ms": total_ms,
                    "cir_cached_objects": cir_cached_objects_count,
                    "slow_threshold_ms": crate::progress_log::pp_cache_slow_threshold_ms(),
                }),
            );
        }
        Ok(PublicationPointRunResult {
            source: PublicationPointSource::PublicationPointCache,
            snapshot: None,
            warnings,
            objects,
            audit,
            cir_fresh_objects: Vec::new(),
            cir_cached_objects,
            discovered_children,
        })
    }

    fn record_publication_point_cache_phase_ms(
        &self,
        phase: &'static str,
        started: std::time::Instant,
    ) -> u64 {
        let nanos = started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64;
        if let Some(timing) = self.timing.as_ref() {
            timing.record_phase_nanos(phase, nanos);
        }
        nanos / 1_000_000
    }

    pub(crate) fn record_publication_point_total_ms(&self, manifest_rsync_uri: &str, ms: u64) {
        if let Some(timing) = self.timing.as_ref() {
            timing.record_publication_point_nanos(manifest_rsync_uri, ms.saturating_mul(1_000_000));
        }
    }

    pub(crate) fn record_publication_point_step_ms(
        &self,
        manifest_rsync_uri: &str,
        step: &'static str,
        ms: u64,
    ) {
        if let Some(timing) = self.timing.as_ref() {
            timing.record_publication_point_step_nanos(
                manifest_rsync_uri,
                step,
                ms.saturating_mul(1_000_000),
            );
        }
    }

    fn publication_point_cache_child_restore_worker_count(&self) -> usize {
        self.parallel_phase2_config
            .as_ref()
            .map(|config| config.object_workers)
            .unwrap_or(1)
            .clamp(1, PUBLICATION_POINT_CACHE_CHILD_RESTORE_MAX_WORKERS)
    }

    pub(crate) fn ccr_accumulator_snapshot(&self) -> Option<CcrAccumulator> {
        self.ccr_accumulator
            .as_ref()
            .and_then(|accumulator| accumulator.lock().ok().map(|guard| guard.clone()))
    }

    pub(crate) fn append_ccr_manifest_projection(
        &self,
        projection: &VcirCcrManifestProjection,
    ) -> Result<(), String> {
        if let Some(accumulator) = self.ccr_accumulator.as_ref() {
            accumulator
                .lock()
                .map_err(|_| "lock CCR accumulator failed".to_string())?
                .append_manifest_projection(projection)?;
        }
        Ok(())
    }

    fn append_ccr_manifest_projection_from_reuse(
        &self,
        projection: &VcirReuseProjection,
    ) -> Result<(), String> {
        match projection.source {
            PublicationPointSource::Fresh => Err(
                "invalid reuse projection source: fresh does not belong to failed-fetch reuse"
                    .to_string(),
            ),
            PublicationPointSource::PublicationPointCache => self.append_ccr_manifest_projection(
                projection.ccr_manifest_projection.as_ref().ok_or_else(|| {
                    "publication-point cache reuse is missing CCR manifest projection".to_string()
                })?,
            ),
            PublicationPointSource::VcirCurrentInstance => self.append_ccr_manifest_projection(
                projection.ccr_manifest_projection.as_ref().ok_or_else(|| {
                    "vcir current-instance reuse is missing CCR manifest projection".to_string()
                })?,
            ),
            PublicationPointSource::FailedFetchNoCache => Ok(()),
        }
    }

    fn current_manifest_hash_hex_for_audit(&self, ca: &CaInstanceHandle) -> Option<String> {
        if let Some(index_handle) = self.current_repo_index.as_ref()
            && let Ok(index) = index_handle.read()
            && let Some(entry) = index.get_by_uri(&ca.manifest_rsync_uri)
        {
            return Some(entry.current_hash_hex.clone());
        }

        self.store
            .load_current_object_with_hash_by_uri(&ca.manifest_rsync_uri)
            .ok()
            .flatten()
            .map(|current| current.current_hash_hex)
    }

    fn rejected_manifest_audit_entry_for_failed_fetch(
        &self,
        ca: &CaInstanceHandle,
        fresh_err: &ManifestFreshError,
    ) -> Option<ObjectAuditEntry> {
        let sha256_hex = self.current_manifest_hash_hex_for_audit(ca)?;
        Some(ObjectAuditEntry {
            rsync_uri: ca.manifest_rsync_uri.clone(),
            sha256_hex,
            kind: AuditObjectKind::Manifest,
            result: AuditObjectResult::Error,
            detail: Some(fresh_err.to_string()),
        })
    }

    fn fresh_failure_audit_entries_for_cir(
        &self,
        ca: &CaInstanceHandle,
        fresh_err: &ManifestFreshError,
    ) -> Vec<ObjectAuditEntry> {
        if !fresh_err.should_warn_when_current_instance_reused() {
            return Vec::new();
        }
        self.rejected_manifest_audit_entry_for_failed_fetch(ca, fresh_err)
            .into_iter()
            .collect()
    }

    pub(crate) fn stage_fresh_publication_point_after_repo_ready(
        &self,
        ca: &CaInstanceHandle,
        repo_sync_ok: bool,
        repo_sync_err: Option<&str>,
    ) -> Result<FreshPublicationPointStage, FreshPublicationPointStageError> {
        if self
            .policy
            .verification_forced_vcir_reuse_manifest_uris
            .contains(&ca.manifest_rsync_uri)
        {
            if let Some(timing) = self.timing.as_ref() {
                timing.record_count("verification_forced_vcir_reuse_publication_points", 1);
            }
            return Err(FreshPublicationPointStageError {
                error: ManifestFreshError::VerificationOnlyForcedCurrentInstanceVcir {
                    manifest_rsync_uri: ca.manifest_rsync_uri.clone(),
                },
                snapshot_prepare_ms: 0,
            });
        }
        let snapshot_prepare_started = std::time::Instant::now();
        let issuer_ca_der = ca_certificate_der_for_validation(ca, self.store, self.timing.as_ref())
            .map_err(|detail| FreshPublicationPointStageError {
                error: ManifestFreshError::IssuerCaLoadFailed { detail },
                snapshot_prepare_ms: snapshot_prepare_started.elapsed().as_millis() as u64,
            })?;
        let issuer_ca_der: Arc<[u8]> = Arc::from(issuer_ca_der.as_ref());
        let fresh_publication_point = {
            let _manifest_total = self
                .timing
                .as_ref()
                .map(|t| t.span_phase("manifest_processing_total"));
            process_manifest_publication_point_fresh_after_repo_sync_with_timing(
                self.store,
                &ca.manifest_rsync_uri,
                &ca.publication_point_rsync_uri,
                self.current_repo_index.as_ref(),
                issuer_ca_der.as_ref(),
                ca.ca_certificate_rsync_uri.as_deref(),
                self.validation_time,
                repo_sync_ok,
                repo_sync_err,
            )
        };
        let snapshot_prepare_ms = snapshot_prepare_started.elapsed().as_millis() as u64;
        let (fresh_point, snapshot_prepare_timing) =
            fresh_publication_point.map_err(|error| FreshPublicationPointStageError {
                error,
                snapshot_prepare_ms,
            })?;
        if let Some(timing) = self.timing.as_ref() {
            timing.record_phase_nanos(
                "fresh_snapshot_prepare_total",
                snapshot_prepare_ms.saturating_mul(1_000_000),
            );
            timing.record_phase_nanos(
                "fresh_snapshot_manifest_load_total",
                snapshot_prepare_timing
                    .manifest_load_ms
                    .saturating_mul(1_000_000),
            );
            timing.record_phase_nanos(
                "fresh_snapshot_manifest_decode_total",
                snapshot_prepare_timing
                    .manifest_decode_ms
                    .saturating_mul(1_000_000),
            );
            timing.record_phase_nanos(
                "fresh_snapshot_replay_guard_total",
                snapshot_prepare_timing
                    .replay_guard_ms
                    .saturating_mul(1_000_000),
            );
            timing.record_phase_nanos(
                "fresh_snapshot_manifest_entries_total",
                snapshot_prepare_timing
                    .manifest_entries_ms
                    .saturating_mul(1_000_000),
            );
            timing.record_phase_nanos(
                "fresh_snapshot_pack_files_total",
                snapshot_prepare_timing
                    .pack_files_ms
                    .saturating_mul(1_000_000),
            );
            timing.record_phase_nanos(
                "fresh_snapshot_ee_path_validate_total",
                snapshot_prepare_timing
                    .ee_path_validate_ms
                    .saturating_mul(1_000_000),
            );
            timing.record_count("fresh_publication_points", 1);
            timing.record_count(
                "fresh_manifest_files_total",
                snapshot_prepare_timing.manifest_file_count as u64,
            );
        }
        self.record_publication_point_step_ms(
            &ca.manifest_rsync_uri,
            "fresh_snapshot_prepare",
            snapshot_prepare_ms,
        );
        self.record_publication_point_step_ms(
            &ca.manifest_rsync_uri,
            "fresh_snapshot_manifest_load",
            snapshot_prepare_timing.manifest_load_ms,
        );
        self.record_publication_point_step_ms(
            &ca.manifest_rsync_uri,
            "fresh_snapshot_manifest_decode",
            snapshot_prepare_timing.manifest_decode_ms,
        );
        self.record_publication_point_step_ms(
            &ca.manifest_rsync_uri,
            "fresh_snapshot_replay_guard",
            snapshot_prepare_timing.replay_guard_ms,
        );
        self.record_publication_point_step_ms(
            &ca.manifest_rsync_uri,
            "fresh_snapshot_manifest_entries",
            snapshot_prepare_timing.manifest_entries_ms,
        );
        self.record_publication_point_step_ms(
            &ca.manifest_rsync_uri,
            "fresh_snapshot_pack_files",
            snapshot_prepare_timing.pack_files_ms,
        );
        self.record_publication_point_step_ms(
            &ca.manifest_rsync_uri,
            "fresh_snapshot_ee_path_validate",
            snapshot_prepare_timing.ee_path_validate_ms,
        );

        let child_discovery_started = std::time::Instant::now();
        let out = {
            let _child_disc_total = self
                .timing
                .as_ref()
                .map(|t| t.span_phase("child_discovery_total"));
            discover_children_from_fresh_snapshot_with_audit_cached_with_issuer_der(
                ca,
                issuer_ca_der.as_ref(),
                &fresh_point,
                self.validation_time,
                self.timing.as_ref(),
                self.policy,
                if self.enable_child_certificate_validation_cache {
                    Some(ChildCertificateValidationCacheContext {
                        store: self.store,
                        issuer_ca_sha256: sha256_digest_32(issuer_ca_der.as_ref()),
                        ca_validation_context_digest: ca_validation_context_digest_for_ca(ca),
                        policy_fingerprint: publication_point_cache_policy_fingerprint(self.policy),
                    })
                } else {
                    None
                },
            )
        };
        let (discovered_children, child_audits, discovered_router_keys, warnings) = match out {
            Ok(out) => (out.children, out.audits, out.router_keys, Vec::new()),
            Err(e) => (
                Vec::new(),
                Vec::new(),
                Vec::new(),
                vec![
                    Warning::new(format!("child CA discovery failed: {e}"))
                        .with_rfc_refs(&[RfcRef("RFC 6487 §7.2")])
                        .with_context(&ca.manifest_rsync_uri),
                ],
            ),
        };
        let child_discovery_ms = child_discovery_started.elapsed().as_millis() as u64;
        if let Some(timing) = self.timing.as_ref() {
            timing.record_phase_nanos(
                "fresh_child_discovery_total",
                child_discovery_ms.saturating_mul(1_000_000),
            );
            timing.record_count(
                "fresh_children_discovered",
                discovered_children.len() as u64,
            );
            timing.record_count("fresh_child_audits", child_audits.len() as u64);
            timing.record_count(
                "fresh_router_keys_discovered",
                discovered_router_keys.len() as u64,
            );
        }
        self.record_publication_point_step_ms(
            &ca.manifest_rsync_uri,
            "fresh_child_discovery",
            child_discovery_ms,
        );

        Ok(FreshPublicationPointStage {
            fresh_point,
            issuer_ca_der,
            snapshot_prepare_timing,
            snapshot_prepare_ms,
            discovered_children,
            child_audits,
            discovered_router_keys,
            child_discovery_ms,
            warnings,
        })
    }

    pub(crate) fn finalize_fresh_publication_point_from_reducer(
        &self,
        ca: &CaInstanceHandle,
        fresh_point: &FreshValidatedPublicationPoint,
        warnings: Vec<Warning>,
        mut objects: crate::validation::objects::ObjectsOutput,
        child_audits: Vec<ObjectAuditEntry>,
        discovered_children: Vec<DiscoveredChildCaInstance>,
        repo_sync_source: Option<&str>,
        repo_sync_phase: Option<&str>,
        repo_sync_duration_ms: u64,
        repo_sync_err: Option<&str>,
    ) -> Result<FreshPublicationPointFinalizeOutput, String> {
        let snapshot_pack_started = std::time::Instant::now();
        let pack = fresh_point.to_publication_point_snapshot();
        let snapshot_pack_ms = snapshot_pack_started.elapsed().as_millis() as u64;
        if let Some(timing) = self.timing.as_ref() {
            timing.record_phase_nanos(
                "fresh_snapshot_pack_total",
                snapshot_pack_ms.saturating_mul(1_000_000),
            );
        }
        self.record_publication_point_step_ms(
            &ca.manifest_rsync_uri,
            "fresh_snapshot_pack",
            snapshot_pack_ms,
        );

        let persist_vcir_started = std::time::Instant::now();
        let persist_vcir_timing = if self.persist_vcir {
            persist_vcir_for_fresh_result_with_timing(
                self.store,
                self.policy,
                ca,
                &pack,
                &mut objects,
                &warnings,
                &child_audits,
                &discovered_children,
                self.validation_time,
                self.publication_point_cache_observe_only
                    || self.enable_publication_point_validation_cache,
            )
            .map_err(|e| format!("persist VCIR failed: {e}"))?
        } else {
            PersistVcirTimingBreakdown::default()
        };
        let persist_vcir_ms = persist_vcir_started.elapsed().as_millis() as u64;
        if let Some(timing) = self.timing.as_ref() {
            timing.record_phase_nanos(
                "fresh_persist_vcir_total",
                persist_vcir_ms.saturating_mul(1_000_000),
            );
            timing.record_phase_nanos(
                "fresh_persist_embedded_store_total",
                persist_vcir_timing
                    .embedded_store_ms
                    .saturating_mul(1_000_000),
            );
            timing.record_phase_nanos(
                "fresh_persist_build_vcir_total",
                persist_vcir_timing.build_vcir_ms.saturating_mul(1_000_000),
            );
            timing.record_phase_nanos(
                "fresh_persist_replace_vcir_total",
                persist_vcir_timing
                    .replace_vcir_ms
                    .saturating_mul(1_000_000),
            );
            timing.record_phase_nanos(
                "fresh_persist_local_outputs_total",
                persist_vcir_timing
                    .build_vcir
                    .local_outputs_ms
                    .saturating_mul(1_000_000),
            );
            timing.record_phase_nanos(
                "fresh_persist_child_entries_total",
                persist_vcir_timing
                    .build_vcir
                    .child_entries_ms
                    .saturating_mul(1_000_000),
            );
            timing.record_phase_nanos(
                "fresh_persist_related_artifacts_total",
                persist_vcir_timing
                    .build_vcir
                    .related_artifacts_ms
                    .saturating_mul(1_000_000),
            );
            if persist_vcir_timing.publication_point_cache_future_notbefore_guarded {
                timing.record_count("publication_point_cache_future_notbefore_guarded", 1);
            }
        }
        self.record_publication_point_step_ms(
            &ca.manifest_rsync_uri,
            "fresh_persist_vcir",
            persist_vcir_ms,
        );
        self.record_publication_point_step_ms(
            &ca.manifest_rsync_uri,
            "fresh_persist_build_vcir",
            persist_vcir_timing.build_vcir_ms,
        );
        self.record_publication_point_step_ms(
            &ca.manifest_rsync_uri,
            "fresh_persist_replace_vcir",
            persist_vcir_timing.replace_vcir_ms,
        );

        // local_outputs_cache only exists to build/persist VCIR. Release it before the
        // publication point result is retained for the rest of the run.
        let _released_local_outputs = std::mem::take(&mut objects.local_outputs_cache);
        let _released_roa_cache_object_meta = std::mem::take(&mut objects.roa_cache_object_meta);

        let mut ccr_projection_build_ms = 0;
        let mut ccr_append_ms = 0;
        if self.ccr_accumulator.is_some() {
            let ccr_projection_build_started = std::time::Instant::now();
            let child_entries =
                build_vcir_child_entries(self.store, &discovered_children, self.validation_time)?;
            let ccr_manifest_projection =
                build_vcir_ccr_manifest_projection_from_fresh(ca, &pack, &child_entries)?;
            ccr_projection_build_ms = ccr_projection_build_started.elapsed().as_millis() as u64;
            let ccr_append_started = std::time::Instant::now();
            self.append_ccr_manifest_projection(&ccr_manifest_projection)?;
            ccr_append_ms = ccr_append_started.elapsed().as_millis() as u64;
        }
        if let Some(timing) = self.timing.as_ref() {
            timing.record_phase_nanos(
                "fresh_ccr_projection_build_total",
                ccr_projection_build_ms.saturating_mul(1_000_000),
            );
            timing.record_phase_nanos(
                "fresh_ccr_append_total",
                ccr_append_ms.saturating_mul(1_000_000),
            );
        }
        self.record_publication_point_step_ms(
            &ca.manifest_rsync_uri,
            "fresh_ccr_projection_build",
            ccr_projection_build_ms,
        );
        self.record_publication_point_step_ms(
            &ca.manifest_rsync_uri,
            "fresh_ccr_append",
            ccr_append_ms,
        );

        let audit_build_started = std::time::Instant::now();
        let audit = build_publication_point_audit_from_snapshot(
            ca,
            PublicationPointSource::Fresh,
            repo_sync_source,
            repo_sync_phase,
            Some(repo_sync_duration_ms),
            repo_sync_err,
            &pack,
            &warnings,
            &objects,
            &child_audits,
        );
        let audit_build_ms = audit_build_started.elapsed().as_millis() as u64;
        if let Some(timing) = self.timing.as_ref() {
            timing.record_phase_nanos(
                "fresh_audit_build_total",
                audit_build_ms.saturating_mul(1_000_000),
            );
        }
        self.record_publication_point_step_ms(
            &ca.manifest_rsync_uri,
            "fresh_audit_build",
            audit_build_ms,
        );

        Ok(FreshPublicationPointFinalizeOutput {
            result: PublicationPointRunResult {
                source: PublicationPointSource::Fresh,
                snapshot: Some(pack),
                warnings,
                objects,
                audit,
                cir_fresh_objects: Vec::new(),
                cir_cached_objects: Vec::new(),
                discovered_children,
            },
            snapshot_pack_ms,
            persist_vcir_ms,
            persist_vcir_timing,
            ccr_projection_build_ms,
            ccr_append_ms,
            audit_build_ms,
        })
    }
}

impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
    fn prefetch_discovered_children(
        &self,
        children: &[DiscoveredChildCaInstance],
    ) -> Result<(), String> {
        if let Some(runtime) = self.repo_sync_runtime.as_ref() {
            runtime.prefetch_discovered_children(children)?;
        }
        Ok(())
    }

    fn run_publication_point(
        &self,
        ca: &CaInstanceHandle,
    ) -> Result<PublicationPointRunResult, String> {
        let publication_point_started = std::time::Instant::now();
        let _pp_total = self
            .timing
            .as_ref()
            .map(|t| t.span_publication_point(&ca.manifest_rsync_uri));
        if let Some(t) = self.timing.as_ref() {
            t.record_count("publication_points_seen", 1);
            if ca.rrdp_notification_uri.is_some() {
                t.record_count("publication_points_rrdp_notify_present_total", 1);
            } else {
                t.record_count("publication_points_rrdp_notify_missing_total", 1);
            }
        }

        let mut warnings: Vec<Warning> = Vec::new();
        crate::progress_log::emit(
            "publication_point_start",
            serde_json::json!({
                "manifest_rsync_uri": ca.manifest_rsync_uri,
                "publication_point_rsync_uri": ca.publication_point_rsync_uri,
                "rsync_base_uri": ca.rsync_base_uri,
                "rrdp_notification_uri": ca.rrdp_notification_uri,
            }),
        );

        let attempted_rrdp =
            self.policy.sync_preference == crate::policy::SyncPreference::RrdpThenRsync;
        let original_notification_uri = ca.rrdp_notification_uri.as_deref();
        let mut effective_notification_uri = if attempted_rrdp {
            original_notification_uri
        } else {
            None
        };
        let mut skip_sync_due_to_dedup = false;

        if attempted_rrdp && self.rrdp_dedup {
            if let Some(notification_uri) = original_notification_uri {
                if let Some(rrdp_ok) = self
                    .rrdp_repo_cache
                    .lock()
                    .expect("rrdp_repo_cache lock")
                    .get(notification_uri)
                    .copied()
                {
                    if let Some(t) = self.timing.as_ref() {
                        t.record_count("rrdp_repo_dedup_hits", 1);
                    }
                    if rrdp_ok {
                        if let Some(t) = self.timing.as_ref() {
                            t.record_count("rrdp_repo_dedup_rrdp_ok_skip", 1);
                        }
                        skip_sync_due_to_dedup = true;
                    } else {
                        if let Some(t) = self.timing.as_ref() {
                            t.record_count("rrdp_repo_dedup_rrdp_failed_skip", 1);
                        }
                        effective_notification_uri = None;
                    }
                } else if let Some(t) = self.timing.as_ref() {
                    t.record_count("rrdp_repo_dedup_misses", 1);
                }
            }
        }

        if !skip_sync_due_to_dedup && effective_notification_uri.is_none() && self.rsync_dedup {
            let base = self.rsync_fetcher.dedup_key(&ca.rsync_base_uri);
            let hit_ok = self
                .rsync_repo_cache
                .lock()
                .expect("rsync_repo_cache lock")
                .get(&base)
                .copied()
                .unwrap_or(false);
            if hit_ok {
                if let Some(t) = self.timing.as_ref() {
                    t.record_count("rsync_repo_dedup_hits", 1);
                    t.record_count("rsync_repo_dedup_skipped_sync", 1);
                }
                skip_sync_due_to_dedup = true;
            } else if let Some(t) = self.timing.as_ref() {
                t.record_count("rsync_repo_dedup_misses", 1);
            }
        }

        let repo_sync_started = std::time::Instant::now();
        let mut runtime_repo_sync_duration_ms = None;
        let (repo_sync_ok, repo_sync_err, repo_sync_source, repo_sync_phase): (
            bool,
            Option<String>,
            Option<String>,
            Option<String>,
        ) = if let Some(runtime) = self.repo_sync_runtime.as_ref() {
            let RepoSyncRuntimeOutcome {
                repo_sync_ok,
                repo_sync_err,
                repo_sync_source,
                repo_sync_phase,
                repo_sync_duration_ms,
                warnings: repo_warnings,
            } = runtime.sync_publication_point_repo(ca)?;
            runtime_repo_sync_duration_ms = Some(repo_sync_duration_ms);
            warnings.extend(repo_warnings);
            (
                repo_sync_ok,
                repo_sync_err,
                repo_sync_source,
                repo_sync_phase,
            )
        } else if skip_sync_due_to_dedup {
            let source = if effective_notification_uri.is_some() {
                Some("rrdp_dedup_skip".to_string())
            } else {
                Some("rsync_dedup_skip".to_string())
            };
            let phase = source.clone();
            (true, None, source, phase)
        } else {
            let repo_key = effective_notification_uri.unwrap_or_else(|| ca.rsync_base_uri.as_str());
            let _repo_total = self
                .timing
                .as_ref()
                .map(|t| t.span_phase("repo_sync_total"));
            let _repo_span = self.timing.as_ref().map(|t| t.span_rrdp_repo(repo_key));

            match if let Some(delta_index) = self.replay_delta_index.as_ref() {
                sync_publication_point_replay_delta(
                    self.store,
                    delta_index,
                    effective_notification_uri,
                    &ca.rsync_base_uri,
                    self.http_fetcher,
                    self.rsync_fetcher,
                    self.timing.as_ref(),
                    self.download_log.as_ref(),
                )
            } else if let Some(replay_index) = self.replay_archive_index.as_ref() {
                sync_publication_point_replay(
                    self.store,
                    replay_index,
                    effective_notification_uri,
                    &ca.rsync_base_uri,
                    self.http_fetcher,
                    self.rsync_fetcher,
                    self.timing.as_ref(),
                    self.download_log.as_ref(),
                )
            } else {
                sync_publication_point(
                    self.store,
                    self.policy,
                    effective_notification_uri,
                    &ca.rsync_base_uri,
                    self.http_fetcher,
                    self.rsync_fetcher,
                    self.timing.as_ref(),
                    self.download_log.as_ref(),
                )
            } {
                Ok(res) => {
                    if self.rsync_dedup && res.source == crate::sync::repo::RepoSyncSource::Rsync {
                        let base = self.rsync_fetcher.dedup_key(&ca.rsync_base_uri);
                        self.rsync_repo_cache
                            .lock()
                            .expect("rsync_repo_cache lock")
                            .insert(base, true);
                        if let Some(t) = self.timing.as_ref() {
                            t.record_count("rsync_repo_dedup_mark_ok", 1);
                        }
                    }

                    if attempted_rrdp && self.rrdp_dedup {
                        if let Some(notification_uri) = original_notification_uri {
                            if effective_notification_uri.is_some() {
                                let rrdp_ok = res.source == crate::sync::repo::RepoSyncSource::Rrdp;
                                self.rrdp_repo_cache
                                    .lock()
                                    .expect("rrdp_repo_cache lock")
                                    .insert(notification_uri.to_string(), rrdp_ok);
                                if let Some(t) = self.timing.as_ref() {
                                    if rrdp_ok {
                                        t.record_count("rrdp_repo_dedup_mark_ok", 1);
                                    } else {
                                        t.record_count("rrdp_repo_dedup_mark_failed", 1);
                                    }
                                }
                            }
                        }
                    }

                    warnings.extend(res.warnings);
                    (
                        true,
                        None,
                        Some(repo_sync_source_label(res.source).to_string()),
                        Some(repo_sync_phase_label(res.phase).to_string()),
                    )
                }
                Err(e) => {
                    if attempted_rrdp && self.rrdp_dedup {
                        if let Some(notification_uri) = original_notification_uri {
                            if effective_notification_uri.is_some() {
                                self.rrdp_repo_cache
                                    .lock()
                                    .expect("rrdp_repo_cache lock")
                                    .insert(notification_uri.to_string(), false);
                            }
                        }
                    }
                    warnings.push(
                        Warning::new(format!("repo sync failed (fresh processing stopped): {e}"))
                            .with_rfc_refs(&[RfcRef("RFC 8182 §3.4.5"), RfcRef("RFC 9286 §6.6")])
                            .with_context(&ca.rsync_base_uri),
                    );
                    (
                        false,
                        Some(e.to_string()),
                        None,
                        Some(
                            repo_sync_failure_phase_label(
                                attempted_rrdp,
                                original_notification_uri,
                                effective_notification_uri,
                            )
                            .to_string(),
                        ),
                    )
                }
            }
        };
        let repo_sync_duration_ms = effective_repo_sync_duration_ms(
            repo_sync_started.elapsed().as_millis() as u64,
            runtime_repo_sync_duration_ms,
            repo_sync_ok,
        );
        crate::progress_log::emit(
            "publication_point_repo_sync_done",
            serde_json::json!({
                "manifest_rsync_uri": ca.manifest_rsync_uri,
                "publication_point_rsync_uri": ca.publication_point_rsync_uri,
                "repo_sync_ok": repo_sync_ok,
                "repo_sync_source": repo_sync_source,
                "repo_sync_phase": repo_sync_phase,
                "repo_sync_error": repo_sync_err,
                "repo_sync_duration_ms": repo_sync_duration_ms,
            }),
        );

        if let Some(result) = self.observe_or_reuse_publication_point_cache(
            ca,
            repo_sync_source.as_deref(),
            repo_sync_phase.as_deref(),
            repo_sync_duration_ms,
            repo_sync_err.as_deref(),
            &warnings,
        ) {
            let total_duration_ms = publication_point_started.elapsed().as_millis() as u64;
            crate::progress_log::emit(
                "publication_point_finish",
                serde_json::json!({
                    "manifest_rsync_uri": ca.manifest_rsync_uri,
                    "publication_point_rsync_uri": ca.publication_point_rsync_uri,
                    "source": source_label(result.source),
                    "repo_sync_source": repo_sync_source,
                    "repo_sync_phase": repo_sync_phase,
                    "repo_sync_duration_ms": repo_sync_duration_ms,
                    "total_duration_ms": total_duration_ms,
                    "post_repo_duration_ms": total_duration_ms.saturating_sub(repo_sync_duration_ms),
                    "warning_count": result.warnings.len(),
                    "vrp_count": result.objects.vrps.len(),
                    "vap_count": result.objects.aspas.len(),
                    "router_key_count": result.objects.router_keys.len(),
                    "child_count": result.discovered_children.len(),
                }),
            );
            return Ok(result);
        }

        let fresh_stage = self.stage_fresh_publication_point_after_repo_ready(
            ca,
            repo_sync_ok,
            repo_sync_err.as_deref(),
        );

        match fresh_stage {
            Ok(stage) => {
                let FreshPublicationPointStage {
                    fresh_point,
                    issuer_ca_der,
                    snapshot_prepare_timing,
                    snapshot_prepare_ms,
                    discovered_children,
                    child_audits,
                    discovered_router_keys,
                    child_discovery_ms,
                    warnings: stage_warnings,
                } = stage;
                warnings.extend(stage_warnings);

                let has_roa = fresh_point
                    .files()
                    .iter()
                    .any(|file| file.rsync_uri.ends_with(".roa"));
                if self.enable_roa_validation_cache {
                    if let Some(timing) = self.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 {
                    self.roa_validation_cache_view_for_fresh_point(fresh_point.manifest_rsync_uri())
                } else {
                    None
                };
                let roa_cache = if self.enable_roa_validation_cache && has_roa {
                    RoaValidationCacheInput::enabled_with_context(
                        roa_cache_view.as_ref(),
                        ca_validation_context_digest_for_ca(ca),
                        publication_point_cache_policy_fingerprint(self.policy),
                    )
                } else {
                    RoaValidationCacheInput::disabled()
                };
                let objects_processing_started = std::time::Instant::now();
                let mut objects = {
                    let _objects_total = self
                        .timing
                        .as_ref()
                        .map(|t| t.span_phase("objects_processing_total"));
                    if let Some(phase2_pool) = self.parallel_roa_worker_pool.as_ref() {
                        process_publication_point_for_issuer_parallel_roa_with_pool_cache_options(
                            &fresh_point,
                            self.policy,
                            issuer_ca_der.as_ref(),
                            ca.ca_certificate_rsync_uri.as_deref(),
                            ca.effective_ip_resources.as_ref(),
                            ca.effective_as_resources.as_ref(),
                            self.validation_time,
                            self.timing.as_ref(),
                            phase2_pool,
                            false,
                            roa_cache,
                        )
                    } else if let Some(phase2_config) = self.parallel_phase2_config.as_ref() {
                        process_publication_point_for_issuer_parallel_roa_with_cache_options(
                            &fresh_point,
                            self.policy,
                            issuer_ca_der.as_ref(),
                            ca.ca_certificate_rsync_uri.as_deref(),
                            ca.effective_ip_resources.as_ref(),
                            ca.effective_as_resources.as_ref(),
                            self.validation_time,
                            self.timing.as_ref(),
                            phase2_config,
                            false,
                            roa_cache,
                        )
                    } else {
                        crate::validation::objects::process_publication_point_for_issuer_with_cache_options(
                            &fresh_point,
                            self.policy,
                            issuer_ca_der.as_ref(),
                            ca.ca_certificate_rsync_uri.as_deref(),
                            ca.effective_ip_resources.as_ref(),
                            ca.effective_as_resources.as_ref(),
                            self.validation_time,
                            self.timing.as_ref(),
                            false,
                            roa_cache,
                        )
                    }
                };
                let objects_processing_ms = objects_processing_started.elapsed().as_millis() as u64;
                self.record_publication_point_step_ms(
                    &ca.manifest_rsync_uri,
                    "fresh_objects_processing",
                    objects_processing_ms,
                );

                objects.router_keys.extend(discovered_router_keys);
                objects
                    .local_outputs_cache
                    .extend(build_router_key_local_outputs(ca, &objects.router_keys));

                let finalized = self.finalize_fresh_publication_point_from_reducer(
                    ca,
                    &fresh_point,
                    warnings,
                    objects,
                    child_audits,
                    discovered_children,
                    repo_sync_source.as_deref(),
                    repo_sync_phase.as_deref(),
                    repo_sync_duration_ms,
                    repo_sync_err.as_deref(),
                )?;
                let FreshPublicationPointFinalizeOutput {
                    result,
                    snapshot_pack_ms,
                    persist_vcir_ms,
                    persist_vcir_timing,
                    ccr_projection_build_ms,
                    ccr_append_ms,
                    audit_build_ms,
                } = finalized;
                let total_duration_ms = publication_point_started.elapsed().as_millis() as u64;
                crate::progress_log::emit(
                    "publication_point_finish",
                    serde_json::json!({
                        "manifest_rsync_uri": ca.manifest_rsync_uri,
                        "publication_point_rsync_uri": ca.publication_point_rsync_uri,
                        "source": "fresh",
                        "repo_sync_source": repo_sync_source,
                        "repo_sync_phase": repo_sync_phase,
                        "repo_sync_duration_ms": repo_sync_duration_ms,
                        "total_duration_ms": total_duration_ms,
                        "post_repo_duration_ms": total_duration_ms.saturating_sub(repo_sync_duration_ms),
                        "snapshot_prepare_ms": snapshot_prepare_ms,
                        "snapshot_manifest_load_ms": snapshot_prepare_timing.manifest_load_ms,
                        "snapshot_manifest_decode_ms": snapshot_prepare_timing.manifest_decode_ms,
                        "snapshot_replay_guard_ms": snapshot_prepare_timing.replay_guard_ms,
                        "snapshot_manifest_entries_ms": snapshot_prepare_timing.manifest_entries_ms,
                        "snapshot_pack_files_ms": snapshot_prepare_timing.pack_files_ms,
                        "snapshot_ee_path_validate_ms": snapshot_prepare_timing.ee_path_validate_ms,
                        "objects_processing_ms": objects_processing_ms,
                        "child_discovery_ms": child_discovery_ms,
                        "snapshot_pack_ms": snapshot_pack_ms,
                        "persist_vcir_ms": persist_vcir_ms,
                        "persist_embedded_collect_ms": persist_vcir_timing.embedded_collect_ms,
                        "persist_embedded_store_ms": persist_vcir_timing.embedded_store_ms,
                        "persist_build_vcir_ms": persist_vcir_timing.build_vcir_ms,
                        "persist_replace_vcir_ms": persist_vcir_timing.replace_vcir_ms,
                        "persist_select_crl_ms": persist_vcir_timing.build_vcir.select_crl_ms,
                        "persist_current_ca_decode_ms": persist_vcir_timing.build_vcir.current_ca_decode_ms,
                        "persist_local_outputs_ms": persist_vcir_timing.build_vcir.local_outputs_ms,
                        "persist_child_entries_ms": persist_vcir_timing.build_vcir.child_entries_ms,
                        "persist_related_artifacts_ms": persist_vcir_timing.build_vcir.related_artifacts_ms,
                        "persist_vcir_struct_ms": persist_vcir_timing.build_vcir.struct_build_ms,
                        "persist_replace_breakdown": &persist_vcir_timing.replace_vcir,
                        "publication_point_cache_future_notbefore_guarded": persist_vcir_timing.publication_point_cache_future_notbefore_guarded,
                        "ccr_projection_build_ms": ccr_projection_build_ms,
                        "ccr_append_ms": ccr_append_ms,
                        "audit_build_ms": audit_build_ms,
                        "warning_count": result.warnings.len(),
                        "vrp_count": result.objects.vrps.len(),
                        "vap_count": result.objects.aspas.len(),
                        "router_key_count": result.objects.router_keys.len(),
                        "child_count": result.discovered_children.len(),
                    }),
                );
                if (total_duration_ms as f64) / 1000.0 >= crate::progress_log::slow_threshold_secs()
                {
                    crate::progress_log::emit(
                        "publication_point_slow",
                        serde_json::json!({
                            "manifest_rsync_uri": ca.manifest_rsync_uri,
                            "publication_point_rsync_uri": ca.publication_point_rsync_uri,
                            "source": "fresh",
                            "repo_sync_source": repo_sync_source,
                            "repo_sync_phase": repo_sync_phase,
                            "repo_sync_duration_ms": repo_sync_duration_ms,
                            "total_duration_ms": total_duration_ms,
                            "post_repo_duration_ms": total_duration_ms.saturating_sub(repo_sync_duration_ms),
                            "snapshot_prepare_ms": snapshot_prepare_ms,
                            "snapshot_manifest_load_ms": snapshot_prepare_timing.manifest_load_ms,
                            "snapshot_manifest_decode_ms": snapshot_prepare_timing.manifest_decode_ms,
                            "snapshot_replay_guard_ms": snapshot_prepare_timing.replay_guard_ms,
                            "snapshot_manifest_entries_ms": snapshot_prepare_timing.manifest_entries_ms,
                            "snapshot_pack_files_ms": snapshot_prepare_timing.pack_files_ms,
                            "snapshot_ee_path_validate_ms": snapshot_prepare_timing.ee_path_validate_ms,
                            "objects_processing_ms": objects_processing_ms,
                            "child_discovery_ms": child_discovery_ms,
                            "snapshot_pack_ms": snapshot_pack_ms,
                            "persist_vcir_ms": persist_vcir_ms,
                            "persist_embedded_collect_ms": persist_vcir_timing.embedded_collect_ms,
                            "persist_embedded_store_ms": persist_vcir_timing.embedded_store_ms,
                            "persist_build_vcir_ms": persist_vcir_timing.build_vcir_ms,
                            "persist_replace_vcir_ms": persist_vcir_timing.replace_vcir_ms,
                            "persist_select_crl_ms": persist_vcir_timing.build_vcir.select_crl_ms,
                            "persist_current_ca_decode_ms": persist_vcir_timing.build_vcir.current_ca_decode_ms,
                            "persist_local_outputs_ms": persist_vcir_timing.build_vcir.local_outputs_ms,
                            "persist_child_entries_ms": persist_vcir_timing.build_vcir.child_entries_ms,
                            "persist_related_artifacts_ms": persist_vcir_timing.build_vcir.related_artifacts_ms,
                            "persist_vcir_struct_ms": persist_vcir_timing.build_vcir.struct_build_ms,
                            "persist_replace_breakdown": &persist_vcir_timing.replace_vcir,
                            "publication_point_cache_future_notbefore_guarded": persist_vcir_timing.publication_point_cache_future_notbefore_guarded,
                            "ccr_projection_build_ms": ccr_projection_build_ms,
                            "ccr_append_ms": ccr_append_ms,
                            "audit_build_ms": audit_build_ms,
                        }),
                    );
                }
                Ok(result)
            }
            Err(stage_err) => {
                let snapshot_prepare_ms = stage_err.snapshot_prepare_ms;
                let fresh_err = stage_err.error;
                match self.policy.ca_failed_fetch_policy {
                    crate::policy::CaFailedFetchPolicy::StopAllOutput => {
                        let total_duration_ms =
                            publication_point_started.elapsed().as_millis() as u64;
                        crate::progress_log::emit(
                            "publication_point_finish",
                            serde_json::json!({
                                "manifest_rsync_uri": ca.manifest_rsync_uri,
                                "publication_point_rsync_uri": ca.publication_point_rsync_uri,
                                "source": "error",
                                "repo_sync_source": repo_sync_source,
                                "repo_sync_phase": repo_sync_phase,
                                "repo_sync_duration_ms": repo_sync_duration_ms,
                                "total_duration_ms": total_duration_ms,
                                "post_repo_duration_ms": total_duration_ms.saturating_sub(repo_sync_duration_ms),
                                "snapshot_prepare_ms": snapshot_prepare_ms,
                                "projection_ms": 0,
                                "audit_build_ms": 0,
                                "error": fresh_err.to_string(),
                            }),
                        );
                        crate::progress_log::emit(
                            "repo_terminal_failure",
                            serde_json::json!({
                                "manifest_rsync_uri": ca.manifest_rsync_uri,
                                "publication_point_rsync_uri": ca.publication_point_rsync_uri,
                                "repo_sync_source": repo_sync_source,
                                "repo_sync_phase": repo_sync_phase,
                                "repo_sync_error": repo_sync_err,
                                "repo_sync_duration_ms": repo_sync_duration_ms,
                                "terminal_state": "stop_all_output",
                                "error": fresh_err.to_string(),
                            }),
                        );
                        Err(format!("{fresh_err}"))
                    }
                    crate::policy::CaFailedFetchPolicy::ReuseCurrentInstanceVcir => {
                        let projection_started = std::time::Instant::now();
                        let projection = project_current_instance_vcir_on_failed_fetch(
                            self.store,
                            ca,
                            &fresh_err,
                            self.policy,
                            self.validation_time,
                        )
                        .map_err(|e| format!("failed fetch VCIR projection failed: {e}"))?;
                        let fresh_failure_audits =
                            self.fresh_failure_audit_entries_for_cir(ca, &fresh_err);
                        self.append_ccr_manifest_projection_from_reuse(&projection)?;
                        let projection_ms = projection_started.elapsed().as_millis() as u64;
                        warnings.extend(projection.warnings.clone());
                        let audit_build_started = std::time::Instant::now();
                        let audit = build_publication_point_audit_from_vcir(
                            ca,
                            projection.source,
                            repo_sync_source.as_deref(),
                            repo_sync_phase.as_deref(),
                            Some(repo_sync_duration_ms),
                            repo_sync_err.as_deref(),
                            projection.vcir.as_ref(),
                            projection.snapshot.as_ref(),
                            &warnings,
                            &projection.objects,
                            &projection.child_audits,
                            &fresh_failure_audits,
                        );
                        let audit_build_ms = audit_build_started.elapsed().as_millis() as u64;
                        let cir_cached_objects =
                            if projection.source == PublicationPointSource::VcirCurrentInstance {
                                audit
                                    .objects
                                    .iter()
                                    .filter(|entry| {
                                        !fresh_failure_audits.iter().any(|fresh| fresh == *entry)
                                    })
                                    .cloned()
                                    .collect()
                            } else {
                                Vec::new()
                            };
                        let result = PublicationPointRunResult {
                            source: projection.source,
                            snapshot: projection.snapshot,
                            warnings,
                            objects: projection.objects,
                            audit,
                            cir_fresh_objects: if projection.source
                                == PublicationPointSource::VcirCurrentInstance
                            {
                                fresh_failure_audits
                            } else {
                                Vec::new()
                            },
                            cir_cached_objects,
                            discovered_children: projection.discovered_children,
                        };
                        let total_duration_ms =
                            publication_point_started.elapsed().as_millis() as u64;
                        crate::progress_log::emit(
                            "publication_point_finish",
                            serde_json::json!({
                                "manifest_rsync_uri": ca.manifest_rsync_uri,
                                "publication_point_rsync_uri": ca.publication_point_rsync_uri,
                                "source": source_label(result.source),
                                "repo_sync_source": repo_sync_source,
                                "repo_sync_phase": repo_sync_phase,
                                "repo_sync_duration_ms": repo_sync_duration_ms,
                                "total_duration_ms": total_duration_ms,
                                "post_repo_duration_ms": total_duration_ms.saturating_sub(repo_sync_duration_ms),
                                "snapshot_prepare_ms": snapshot_prepare_ms,
                                "projection_ms": projection_ms,
                                "audit_build_ms": audit_build_ms,
                                "warning_count": result.warnings.len(),
                                "vrp_count": result.objects.vrps.len(),
                                "vap_count": result.objects.aspas.len(),
                                "router_key_count": result.objects.router_keys.len(),
                                "child_count": result.discovered_children.len(),
                            }),
                        );
                        match result.source {
                            PublicationPointSource::VcirCurrentInstance if !repo_sync_ok => {
                                crate::progress_log::emit(
                                    "rsync_failed_fallback_current_instance",
                                    serde_json::json!({
                                        "manifest_rsync_uri": ca.manifest_rsync_uri,
                                        "publication_point_rsync_uri": ca.publication_point_rsync_uri,
                                        "repo_sync_source": repo_sync_source,
                                        "repo_sync_phase": repo_sync_phase,
                                        "repo_sync_error": repo_sync_err,
                                        "repo_sync_duration_ms": repo_sync_duration_ms,
                                        "terminal_state": "fallback_current_instance",
                                    }),
                                );
                            }
                            PublicationPointSource::FailedFetchNoCache => {
                                if !repo_sync_ok {
                                    crate::progress_log::emit(
                                        "rsync_failed_no_cache",
                                        serde_json::json!({
                                            "manifest_rsync_uri": ca.manifest_rsync_uri,
                                            "publication_point_rsync_uri": ca.publication_point_rsync_uri,
                                            "repo_sync_source": repo_sync_source,
                                            "repo_sync_phase": repo_sync_phase,
                                            "repo_sync_error": repo_sync_err,
                                            "repo_sync_duration_ms": repo_sync_duration_ms,
                                            "terminal_state": "failed_no_cache",
                                        }),
                                    );
                                }
                                crate::progress_log::emit(
                                    "repo_terminal_failure",
                                    serde_json::json!({
                                        "manifest_rsync_uri": ca.manifest_rsync_uri,
                                        "publication_point_rsync_uri": ca.publication_point_rsync_uri,
                                        "repo_sync_source": repo_sync_source,
                                        "repo_sync_phase": repo_sync_phase,
                                        "repo_sync_error": repo_sync_err,
                                        "repo_sync_duration_ms": repo_sync_duration_ms,
                                        "terminal_state": "failed_no_cache",
                                    }),
                                );
                            }
                            PublicationPointSource::Fresh => {}
                            PublicationPointSource::PublicationPointCache => {}
                            PublicationPointSource::VcirCurrentInstance => {}
                        }
                        if (total_duration_ms as f64) / 1000.0
                            >= crate::progress_log::slow_threshold_secs()
                        {
                            crate::progress_log::emit(
                                "publication_point_slow",
                                serde_json::json!({
                                    "manifest_rsync_uri": ca.manifest_rsync_uri,
                                    "publication_point_rsync_uri": ca.publication_point_rsync_uri,
                                    "source": source_label(result.source),
                                    "repo_sync_source": repo_sync_source,
                                    "repo_sync_phase": repo_sync_phase,
                                    "repo_sync_duration_ms": repo_sync_duration_ms,
                                    "total_duration_ms": total_duration_ms,
                                    "post_repo_duration_ms": total_duration_ms.saturating_sub(repo_sync_duration_ms),
                                    "snapshot_prepare_ms": snapshot_prepare_ms,
                                    "projection_ms": projection_ms,
                                    "audit_build_ms": audit_build_ms,
                                }),
                            );
                        }
                        Ok(result)
                    }
                }
            }
        }
    }
}

struct ChildDiscoveryOutput {
    children: Vec<DiscoveredChildCaInstance>,
    audits: Vec<ObjectAuditEntry>,
    router_keys: Vec<RouterKeyPayload>,
}

#[derive(Clone, Debug)]
struct VerifiedIssuerCrl {
    crl: crate::data_model::crl::RpkixCrl,
    revoked_serials: std::collections::HashSet<Vec<u8>>,
    sha256_hex: String,
}

#[derive(Clone, Debug)]
enum CachedIssuerCrl {
    Pending {
        bytes: Vec<u8>,
        sha256_hex: Option<String>,
    },
    Ok(VerifiedIssuerCrl),
}

impl CachedIssuerCrl {
    fn current_sha256_hex(&mut self) -> &str {
        match self {
            CachedIssuerCrl::Pending { bytes, sha256_hex } => {
                if sha256_hex.is_none() {
                    *sha256_hex = Some(crate::audit::sha256_hex(bytes));
                }
                sha256_hex
                    .as_deref()
                    .expect("pending CRL sha256 must be populated")
            }
            CachedIssuerCrl::Ok(verified) => verified.sha256_hex.as_str(),
        }
    }
}

struct PublicationPointCacheIdentity {
    ca_cert_sha256: [u8; 32],
    manifest_sha256: [u8; 32],
    ta_context_digest: [u8; 32],
    ca_validation_context_digest: [u8; 32],
    policy_fingerprint: [u8; 32],
}

fn sha256_digest_32(bytes: impl AsRef<[u8]>) -> [u8; 32] {
    let digest = sha2::Sha256::digest(bytes.as_ref());
    let mut out = [0u8; 32];
    out.copy_from_slice(&digest);
    out
}

fn hash_serialized_parts(parts: &[(&str, Vec<u8>)]) -> [u8; 32] {
    let mut hasher = sha2::Sha256::new();
    for (label, value) in parts {
        hasher.update((label.len() as u64).to_be_bytes());
        hasher.update(label.as_bytes());
        hasher.update((value.len() as u64).to_be_bytes());
        hasher.update(value);
    }
    let digest = hasher.finalize();
    let mut out = [0u8; 32];
    out.copy_from_slice(&digest);
    out
}

fn cbor_or_debug_bytes<T: serde::Serialize + std::fmt::Debug>(value: &T) -> Vec<u8> {
    serde_cbor::to_vec(value).unwrap_or_else(|_| format!("{value:?}").into_bytes())
}

fn ta_context_digest_for_ca(ca: &CaInstanceHandle) -> [u8; 32] {
    hash_serialized_parts(&[
        ("version", b"publication-point-cache-ta-v1".to_vec()),
        ("tal_id", ca.tal_id.as_bytes().to_vec()),
    ])
}

pub(crate) fn ca_validation_context_digest_for_ca(ca: &CaInstanceHandle) -> [u8; 32] {
    hash_serialized_parts(&[
        (
            "version",
            b"publication-point-cache-parent-context-v1".to_vec(),
        ),
        ("tal_id", ca.tal_id.as_bytes().to_vec()),
        (
            "parent_manifest",
            ca.parent_manifest_rsync_uri
                .as_deref()
                .unwrap_or("")
                .as_bytes()
                .to_vec(),
        ),
        (
            "effective_ip",
            cbor_or_debug_bytes(&ca.effective_ip_resources),
        ),
        (
            "effective_as",
            cbor_or_debug_bytes(&ca.effective_as_resources),
        ),
    ])
}

pub(crate) fn publication_point_cache_policy_fingerprint(policy: &Policy) -> [u8; 32] {
    hash_serialized_parts(&[
        ("version", b"publication-point-cache-policy-v1".to_vec()),
        ("policy", cbor_or_debug_bytes(policy)),
    ])
}

fn publication_point_cache_projection_items_valid(
    projection: &PublicationPointCacheProjection,
    validation_time: time::OffsetDateTime,
) -> Result<(), &'static str> {
    for output in &projection.outputs {
        if !pack_time_window_contains(
            &output.item_effective_not_before,
            &output.item_effective_until,
            validation_time,
        ) {
            return Err("output_time_gate_miss");
        }
    }
    for child in &projection.children {
        if !pack_time_window_contains(
            &child.child_effective_not_before,
            &child.child_effective_until,
            validation_time,
        ) {
            return Err("child_time_gate_miss");
        }
    }
    Ok(())
}

fn pack_time_window_contains(
    not_before: &PackTime,
    until: &PackTime,
    validation_time: time::OffsetDateTime,
) -> bool {
    let Ok(not_before) = parse_snapshot_time_value(not_before) else {
        return false;
    };
    let Ok(until) = parse_snapshot_time_value(until) else {
        return false;
    };
    validation_time >= not_before && validation_time < until
}

#[derive(Clone, Copy)]
struct ChildCertificateValidationCacheContext<'a> {
    store: &'a RocksStore,
    issuer_ca_sha256: [u8; 32],
    ca_validation_context_digest: [u8; 32],
    policy_fingerprint: [u8; 32],
}

fn child_certificate_cache_key_sha256_hex(
    child_cert_uri: &str,
    child_cert_sha256: &[u8; 32],
    issuer_ca_sha256: &[u8; 32],
    ca_validation_context_digest: &[u8; 32],
    policy_fingerprint: &[u8; 32],
) -> String {
    let digest = hash_serialized_parts(&[
        ("version", b"child-certificate-cache-key-v1".to_vec()),
        ("child_cert_uri", child_cert_uri.as_bytes().to_vec()),
        ("child_cert_sha256", child_cert_sha256.to_vec()),
        ("issuer_ca_sha256", issuer_ca_sha256.to_vec()),
        // Keep the persisted cache-key label stable; this change only renames Rust identifiers.
        (
            "parent_context_digest",
            ca_validation_context_digest.to_vec(),
        ),
        ("policy_fingerprint", policy_fingerprint.to_vec()),
    ]);
    sha256_hex_from_32(&digest)
}

#[derive(Clone, Debug)]
struct ChildCertificateCacheCandidate {
    projection: Option<ChildCertificateCacheProjection>,
}

fn remember_child_certificate_cache_dirty_projection(
    dirty_projections: &mut Option<HashMap<String, ChildCertificateCacheProjection>>,
    projection: &ChildCertificateCacheProjection,
) {
    if let Some(dirty_projections) = dirty_projections {
        dirty_projections.insert(projection.cache_key_sha256_hex.clone(), projection.clone());
    }
}

fn load_child_certificate_der_for_discovery<'a>(
    file: &'a PackFile,
    elapsed_nanos: &mut u64,
    count: &mut u64,
) -> Result<&'a [u8], String> {
    let started = std::time::Instant::now();
    let bytes = file
        .bytes()
        .map_err(|e| format!("child certificate bytes load failed: {e}"))?;
    *elapsed_nanos =
        elapsed_nanos.saturating_add(started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64);
    *count = count.saturating_add(1);
    Ok(bytes)
}

fn ca_certificate_der_for_validation<'a>(
    ca: &'a CaInstanceHandle,
    store: &RocksStore,
    timing: Option<&TimingHandle>,
) -> Result<std::borrow::Cow<'a, [u8]>, String> {
    let started = std::time::Instant::now();
    let was_lazy = ca.ca_certificate_sha256_hex().is_some();
    let der = ca.ca_certificate_der(store)?;
    if was_lazy {
        let elapsed = started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64;
        if let Some(timing) = timing {
            timing.record_count("ca_certificate_lazy_load_count", 1);
            timing.record_count("ca_certificate_lazy_load_bytes", der.len() as u64);
            timing.record_phase_nanos("ca_certificate_lazy_load_total", elapsed);
        }
    }
    Ok(der)
}

fn child_certificate_cache_certificate_window(
    child_not_before: time::OffsetDateTime,
    child_not_after: time::OffsetDateTime,
    issuer_not_before: time::OffsetDateTime,
    issuer_not_after: time::OffsetDateTime,
) -> (PackTime, PackTime) {
    let effective_not_before = child_not_before.max(issuer_not_before);
    let effective_until = child_not_after.min(issuer_not_after);
    (
        PackTime::from_utc_offset_datetime(effective_not_before),
        PackTime::from_utc_offset_datetime(effective_until),
    )
}

fn get_current_crl_sha256_hex(
    crl_rsync_uri: &str,
    crl_cache: &mut std::collections::HashMap<String, CachedIssuerCrl>,
) -> Option<String> {
    crl_cache
        .get_mut(crl_rsync_uri)
        .map(|entry| entry.current_sha256_hex().to_string())
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ChildCertificateCacheCrlGate {
    Unchanged,
    ChangedValid,
    Expired,
    Invalid,
    Missing,
}

#[derive(Default)]
struct ChildCertificateCacheCrlGateSet {
    gates_by_uri_and_expected_hash:
        std::collections::HashMap<(String, String), ChildCertificateCacheCrlGate>,
}

impl ChildCertificateCacheCrlGateSet {
    fn evaluate(
        &mut self,
        projection: &ChildCertificateCacheProjection,
        crl_cache: &mut std::collections::HashMap<String, CachedIssuerCrl>,
        issuer_ca_der: &[u8],
        validation_time: time::OffsetDateTime,
    ) -> (ChildCertificateCacheCrlGate, bool) {
        let key = (
            projection.issuer_crl_uri.clone(),
            projection.issuer_crl_sha256_hex.clone(),
        );
        if let Some(gate) = self.gates_by_uri_and_expected_hash.get(&key) {
            return (*gate, true);
        }

        let gate = evaluate_child_certificate_cache_crl_gate(
            projection,
            crl_cache,
            issuer_ca_der,
            validation_time,
        );
        self.gates_by_uri_and_expected_hash.insert(key, gate);
        (gate, false)
    }
}

fn evaluate_child_certificate_cache_crl_gate(
    projection: &ChildCertificateCacheProjection,
    crl_cache: &mut std::collections::HashMap<String, CachedIssuerCrl>,
    issuer_ca_der: &[u8],
    validation_time: time::OffsetDateTime,
) -> ChildCertificateCacheCrlGate {
    let Some(current_crl_hash) = get_current_crl_sha256_hex(&projection.issuer_crl_uri, crl_cache)
    else {
        return ChildCertificateCacheCrlGate::Missing;
    };
    if current_crl_hash == projection.issuer_crl_sha256_hex {
        let verified_crl = match ensure_issuer_crl_verified(
            &projection.issuer_crl_uri,
            crl_cache,
            issuer_ca_der,
        ) {
            Ok(verified_crl) => verified_crl,
            Err(_) => return ChildCertificateCacheCrlGate::Invalid,
        };
        if !crl_valid_at_time_for_cache(&verified_crl.crl, validation_time) {
            return ChildCertificateCacheCrlGate::Expired;
        }
        return ChildCertificateCacheCrlGate::Unchanged;
    }

    let verified_crl =
        match ensure_issuer_crl_verified(&projection.issuer_crl_uri, crl_cache, issuer_ca_der) {
            Ok(verified_crl) => verified_crl,
            Err(_) => return ChildCertificateCacheCrlGate::Invalid,
        };
    if !crl_valid_at_time_for_cache(&verified_crl.crl, validation_time) {
        return ChildCertificateCacheCrlGate::Expired;
    }
    ChildCertificateCacheCrlGate::ChangedValid
}

fn crl_valid_at_time_for_cache(
    crl: &crate::data_model::crl::RpkixCrl,
    validation_time: time::OffsetDateTime,
) -> bool {
    let this_update = crl.this_update.utc.to_offset(time::UtcOffset::UTC);
    let next_update = crl.next_update.utc.to_offset(time::UtcOffset::UTC);
    validation_time >= this_update && validation_time < next_update
}

#[derive(Clone, Debug)]
struct VcirReuseProjection {
    source: PublicationPointSource,
    vcir: Option<ValidatedCaInstanceResult>,
    ccr_manifest_projection: Option<VcirCcrManifestProjection>,
    snapshot: Option<PublicationPointSnapshot>,
    objects: crate::validation::objects::ObjectsOutput,
    child_audits: Vec<ObjectAuditEntry>,
    discovered_children: Vec<DiscoveredChildCaInstance>,
    warnings: Vec<Warning>,
}

fn discover_children_from_fresh_snapshot_with_audit<P: PublicationPointData>(
    issuer: &CaInstanceHandle,
    publication_point: &P,
    validation_time: time::OffsetDateTime,
    timing: Option<&TimingHandle>,
) -> Result<ChildDiscoveryOutput, String> {
    let issuer_ca_der = match &issuer.ca_certificate {
        CaCertificateRef::InlineDer(bytes) => bytes.as_slice(),
        CaCertificateRef::RepoBytes { .. } => {
            return Err("lazy CA certificate requires store-backed child discovery".to_string());
        }
    };
    let default_policy = Policy::default();
    discover_children_from_fresh_snapshot_with_audit_cached_with_issuer_der(
        issuer,
        issuer_ca_der,
        publication_point,
        validation_time,
        timing,
        &default_policy,
        None,
    )
}

fn discover_children_from_fresh_snapshot_with_audit_cached<P: PublicationPointData>(
    issuer: &CaInstanceHandle,
    publication_point: &P,
    validation_time: time::OffsetDateTime,
    timing: Option<&TimingHandle>,
    cache_context: Option<ChildCertificateValidationCacheContext<'_>>,
) -> Result<ChildDiscoveryOutput, String> {
    let issuer_ca_der = match &issuer.ca_certificate {
        CaCertificateRef::InlineDer(bytes) => bytes.as_slice(),
        CaCertificateRef::RepoBytes { .. } => {
            return Err("lazy CA certificate requires store-backed child discovery".to_string());
        }
    };
    let default_policy = Policy::default();
    discover_children_from_fresh_snapshot_with_audit_cached_with_issuer_der(
        issuer,
        issuer_ca_der,
        publication_point,
        validation_time,
        timing,
        &default_policy,
        cache_context,
    )
}

fn discover_children_from_fresh_snapshot_with_audit_cached_with_issuer_der<
    P: PublicationPointData,
>(
    issuer: &CaInstanceHandle,
    issuer_ca_der: &[u8],
    publication_point: &P,
    validation_time: time::OffsetDateTime,
    timing: Option<&TimingHandle>,
    policy: &Policy,
    cache_context: Option<ChildCertificateValidationCacheContext<'_>>,
) -> Result<ChildDiscoveryOutput, String> {
    let locked_files = publication_point.files();
    // Issuer CA is only required when we actually attempt to validate a subordinate CA. For some
    // audit-only error paths (e.g., missing CRL in the snapshot), we still want discovery to succeed.
    let issuer_ca_decode_error: Option<String>;
    let issuer_ca = match crate::data_model::rc::ResourceCertificate::decode_der(issuer_ca_der) {
        Ok(v) => {
            match v.validate_rfc6487_profile(crate::data_model::rc::ResourceCertificateRole::Ca) {
                Ok(()) => {
                    issuer_ca_decode_error = None;
                    Some(v)
                }
                Err(e) => {
                    issuer_ca_decode_error = Some(format!(
                        "issuer CA profile validation failed: {e} (RFC 6487 §4.8)"
                    ));
                    None
                }
            }
        }
        Err(e) => {
            issuer_ca_decode_error = Some(format!(
                "issuer CA decode failed: {e} (RFC 5280 §4.1; RFC 6487 §4)"
            ));
            None
        }
    };

    let issuer_spki_error: Option<String>;
    let issuer_spki: Option<SubjectPublicKeyInfo<'_>> = if let Some(ca) = issuer_ca.as_ref() {
        match SubjectPublicKeyInfo::from_der(&ca.tbs.subject_public_key_info) {
            Ok((rem, spki)) if rem.is_empty() => {
                issuer_spki_error = None;
                Some(spki)
            }
            Ok((rem, _)) => {
                issuer_spki_error = Some(format!(
                    "trailing bytes after issuer SubjectPublicKeyInfo DER: {} bytes (DER; RFC 5280 §4.1.2.7)",
                    rem.len()
                ));
                None
            }
            Err(e) => {
                issuer_spki_error = Some(format!(
                    "issuer SubjectPublicKeyInfo parse error: {e} (RFC 5280 §4.1.2.7)"
                ));
                None
            }
        }
    } else {
        issuer_spki_error = issuer_ca_decode_error.clone();
        None
    };

    let mut crl_cache: std::collections::HashMap<String, CachedIssuerCrl> = locked_files
        .iter()
        .filter(|f| f.rsync_uri.ends_with(".crl"))
        .map(|f| -> Result<(String, CachedIssuerCrl), String> {
            let bytes = f
                .bytes_cloned()
                .map_err(|e| format!("snapshot CRL bytes load failed: {e}"))?;
            Ok((
                f.rsync_uri.clone(),
                CachedIssuerCrl::Pending {
                    bytes,
                    sha256_hex: Some(sha256_hex_from_32(&f.sha256)),
                },
            ))
        })
        .collect::<Result<_, String>>()?;

    let mut out: Vec<DiscoveredChildCaInstance> = Vec::new();
    let mut audits: Vec<ObjectAuditEntry> = Vec::new();
    let mut router_keys: Vec<RouterKeyPayload> = Vec::new();
    let issuer_resources_index = IssuerEffectiveResourcesIndex::from_effective_resources(
        issuer.effective_ip_resources.as_ref(),
        issuer.effective_as_resources.as_ref(),
    )
    .map_err(|e| format!("build issuer effective resources index failed: {e}"))?;

    let mut cer_seen: u64 = 0;
    let mut ca_skipped_not_ca: u64 = 0;
    let mut ca_ok: u64 = 0;
    let mut ca_error: u64 = 0;
    let mut router_ok: u64 = 0;
    let mut router_error: u64 = 0;
    let mut router_skipped_non_router: u64 = 0;
    let mut crl_select_error: u64 = 0;
    let mut uri_discovery_error: u64 = 0;
    let mut child_cert_cache_lookup: u64 = 0;
    let mut child_cert_cache_hit: u64 = 0;
    let mut child_cert_cache_hit_ca: u64 = 0;
    let mut child_cert_cache_hit_router: u64 = 0;
    let mut child_cert_cache_miss_not_found: u64 = 0;
    let mut child_cert_cache_miss_time_gate: u64 = 0;
    let mut child_cert_cache_miss_crl_missing: u64 = 0;
    let mut child_cert_cache_miss_crl_invalid: u64 = 0;
    let mut child_cert_cache_miss_crl_expired: u64 = 0;
    let mut child_cert_cache_miss_revoked: u64 = 0;
    let mut child_cert_cache_crl_recheck_hit: u64 = 0;
    let mut child_cert_cache_crl_gate_reused: u64 = 0;
    let mut child_cert_cache_load_error: u64 = 0;
    let mut child_cert_cache_write_ok: u64 = 0;
    let mut child_cert_cache_write_error: u64 = 0;

    let mut select_crl_nanos: u64 = 0;
    let mut child_decode_nanos: u64 = 0;
    let mut validate_sub_ca_nanos: u64 = 0;
    let mut validate_router_nanos: u64 = 0;
    let mut uri_discovery_nanos: u64 = 0;
    let mut enqueue_nanos: u64 = 0;
    let mut child_cert_cache_lookup_nanos: u64 = 0;
    let mut child_cert_cache_write_nanos: u64 = 0;
    let child_cert_der_load_cache_hit_nanos: u64 = 0;
    let mut child_cert_der_load_fresh_nanos: u64 = 0;
    let child_cert_der_load_cache_hit_count: u64 = 0;
    let mut child_cert_der_load_fresh_count: u64 = 0;

    let mut eff_ip_items_bucket_le_10: u64 = 0;
    let mut eff_ip_items_bucket_le_100: u64 = 0;
    let mut eff_ip_items_bucket_gt_100: u64 = 0;
    let mut eff_as_items_bucket_le_10: u64 = 0;
    let mut eff_as_items_bucket_le_100: u64 = 0;
    let mut eff_as_items_bucket_gt_100: u64 = 0;

    let mut child_cache_candidates: HashMap<String, ChildCertificateCacheCandidate> =
        HashMap::new();
    let mut child_cache_segment_keys = Vec::<String>::new();
    let mut child_cache_segment_dirty_projections: Option<
        HashMap<String, ChildCertificateCacheProjection>,
    > = None;
    let mut child_cert_cache_batch_lookup_publication_points: u64 = 0;
    let mut child_cert_cache_batch_lookup_entries: u64 = 0;
    let mut child_cert_cache_batch_lookup_errors: u64 = 0;
    let mut child_cert_cache_batch_lookup_nanos: u64 = 0;
    let mut child_cert_cache_mmap_lookup_publication_points: u64 = 0;
    let mut child_cert_cache_mmap_lookup_entries: u64 = 0;
    let mut child_cert_cache_mmap_lookup_hits: u64 = 0;
    let mut child_cert_cache_mmap_lookup_misses: u64 = 0;
    let mut child_cert_cache_mmap_lookup_missing_segments: u64 = 0;
    let mut child_cert_cache_mmap_lookup_errors: u64 = 0;
    let mut child_cert_cache_mmap_lookup_file_bytes: u64 = 0;
    let mut child_cert_cache_mmap_lookup_nanos: u64 = 0;
    let mut child_cert_cache_mmap_write_entries: u64 = 0;
    let mut child_cert_cache_mmap_write_errors: u64 = 0;
    let mut child_cert_cache_mmap_write_file_bytes: u64 = 0;
    let mut child_cert_cache_mmap_write_nanos: u64 = 0;

    if let Some(cache) = cache_context {
        let mut uris = Vec::new();
        let mut keys = Vec::new();
        for f in locked_files
            .iter()
            .filter(|file| file.rsync_uri.ends_with(".cer"))
        {
            uris.push(f.rsync_uri.clone());
            keys.push(child_certificate_cache_key_sha256_hex(
                &f.rsync_uri,
                &f.sha256,
                &cache.issuer_ca_sha256,
                &cache.ca_validation_context_digest,
                &cache.policy_fingerprint,
            ));
        }
        let use_mmap_segment = keys.len() >= CHILD_CERTIFICATE_CACHE_MMAP_MIN_CER_COUNT;
        if use_mmap_segment {
            child_cache_segment_keys = keys.clone();
            child_cache_segment_dirty_projections = Some(HashMap::new());
        }

        let mut db_lookup_indices: Vec<usize> = Vec::new();
        if !keys.is_empty() {
            if use_mmap_segment {
                let mmap_lookup_started = std::time::Instant::now();
                child_cert_cache_mmap_lookup_publication_points = 1;
                child_cert_cache_mmap_lookup_entries = keys.len() as u64;
                match cache
                    .store
                    .get_child_certificate_cache_projections_mmap_segment(
                        publication_point.manifest_rsync_uri(),
                        &keys,
                    ) {
                    Ok(Some(lookup)) => {
                        child_cert_cache_mmap_lookup_hits = lookup.hits as u64;
                        child_cert_cache_mmap_lookup_misses = lookup.misses as u64;
                        child_cert_cache_mmap_lookup_file_bytes = lookup.file_bytes;
                        for (idx, ((uri, _key), projection)) in uris
                            .iter()
                            .zip(keys.iter())
                            .zip(lookup.projections.into_iter())
                            .enumerate()
                        {
                            if let Some(projection) = projection {
                                child_cache_candidates.insert(
                                    uri.clone(),
                                    ChildCertificateCacheCandidate {
                                        projection: Some(projection),
                                    },
                                );
                            } else {
                                db_lookup_indices.push(idx);
                            }
                        }
                    }
                    Ok(None) => {
                        child_cert_cache_mmap_lookup_missing_segments = 1;
                        db_lookup_indices.extend(0..keys.len());
                    }
                    Err(_) => {
                        child_cert_cache_mmap_lookup_errors =
                            child_cert_cache_mmap_lookup_errors.saturating_add(keys.len() as u64);
                        db_lookup_indices.extend(0..keys.len());
                    }
                }
                child_cert_cache_mmap_lookup_nanos = mmap_lookup_started
                    .elapsed()
                    .as_nanos()
                    .min(u128::from(u64::MAX))
                    as u64;
            } else {
                db_lookup_indices.extend(0..keys.len());
            }

            if !db_lookup_indices.is_empty() {
                let batch_lookup_started = std::time::Instant::now();
                child_cert_cache_batch_lookup_publication_points = 1;
                child_cert_cache_batch_lookup_entries = db_lookup_indices.len() as u64;
                let db_keys = db_lookup_indices
                    .iter()
                    .map(|idx| keys[*idx].clone())
                    .collect::<Vec<_>>();
                match cache
                    .store
                    .get_child_certificate_cache_projections_batch(&db_keys)
                {
                    Ok(projections) => {
                        for (idx, projection) in db_lookup_indices
                            .iter()
                            .copied()
                            .zip(projections.into_iter())
                        {
                            if let Some(projection) = projection {
                                remember_child_certificate_cache_dirty_projection(
                                    &mut child_cache_segment_dirty_projections,
                                    &projection,
                                );
                                child_cache_candidates.insert(
                                    uris[idx].clone(),
                                    ChildCertificateCacheCandidate {
                                        projection: Some(projection),
                                    },
                                );
                            }
                        }
                    }
                    Err(_) => {
                        child_cert_cache_batch_lookup_errors = child_cert_cache_batch_lookup_errors
                            .saturating_add(child_cert_cache_batch_lookup_entries);
                    }
                }
                child_cert_cache_batch_lookup_nanos = child_cert_cache_batch_lookup_nanos
                    .saturating_add(
                        batch_lookup_started
                            .elapsed()
                            .as_nanos()
                            .min(u128::from(u64::MAX)) as u64,
                    );
            }
        }
    }

    fn bucketize(v: usize) -> u8 {
        if v <= 10 {
            0
        } else if v <= 100 {
            1
        } else {
            2
        }
    }

    fn ip_item_count(ip: Option<&crate::data_model::rc::IpResourceSet>) -> usize {
        let Some(ip) = ip else { return 0 };
        ip.families
            .iter()
            .map(|f| match &f.choice {
                crate::data_model::rc::IpAddressChoice::Inherit => 0usize,
                crate::data_model::rc::IpAddressChoice::AddressesOrRanges(items) => items.len(),
            })
            .sum()
    }

    fn as_item_count(asr: Option<&crate::data_model::rc::AsResourceSet>) -> usize {
        let Some(asr) = asr else { return 0 };
        let mut n = 0usize;
        if let Some(c) = asr.asnum.as_ref() {
            if let crate::data_model::rc::AsIdentifierChoice::AsIdsOrRanges(items) = c {
                n = n.saturating_add(items.len());
            }
        }
        if let Some(c) = asr.rdi.as_ref() {
            if let crate::data_model::rc::AsIdentifierChoice::AsIdsOrRanges(items) = c {
                n = n.saturating_add(items.len());
            }
        }
        n
    }

    let mut child_cert_crl_gate_set = ChildCertificateCacheCrlGateSet::default();

    for f in locked_files {
        if !f.rsync_uri.ends_with(".cer") {
            continue;
        }
        cer_seen = cer_seen.saturating_add(1);
        let child_cert_sha256_hex = sha256_hex_from_32(&f.sha256);

        if cache_context.is_some() {
            let lookup_started = std::time::Instant::now();
            child_cert_cache_lookup = child_cert_cache_lookup.saturating_add(1);
            let cache_lookup_failed = child_cert_cache_batch_lookup_errors > 0
                && !child_cache_candidates.contains_key(&f.rsync_uri);
            if cache_lookup_failed {
                child_cert_cache_load_error = child_cert_cache_load_error.saturating_add(1);
            } else {
                match child_cache_candidates
                    .get(&f.rsync_uri)
                    .and_then(|candidate| candidate.projection.as_ref())
                {
                    Some(projection) => {
                        let time_gate_ok = pack_time_window_contains(
                            &projection.effective_not_before,
                            &projection.effective_until,
                            validation_time,
                        );
                        if !time_gate_ok {
                            child_cert_cache_miss_time_gate =
                                child_cert_cache_miss_time_gate.saturating_add(1);
                        } else {
                            let (crl_gate, gate_reused) = child_cert_crl_gate_set.evaluate(
                                projection,
                                &mut crl_cache,
                                issuer_ca_der,
                                validation_time,
                            );
                            if gate_reused {
                                child_cert_cache_crl_gate_reused =
                                    child_cert_cache_crl_gate_reused.saturating_add(1);
                            }
                            let mut crl_gate_allows_reuse = false;
                            match crl_gate {
                                ChildCertificateCacheCrlGate::Unchanged => {
                                    crl_gate_allows_reuse = true;
                                }
                                ChildCertificateCacheCrlGate::ChangedValid => {
                                    match ensure_issuer_crl_verified(
                                        &projection.issuer_crl_uri,
                                        &mut crl_cache,
                                        issuer_ca_der,
                                    ) {
                                        Ok(verified_crl) => {
                                            if verified_crl
                                                .revoked_serials
                                                .contains(&projection.child_cert_serial)
                                            {
                                                child_cert_cache_miss_revoked =
                                                    child_cert_cache_miss_revoked.saturating_add(1);
                                            } else {
                                                child_cert_cache_crl_recheck_hit =
                                                    child_cert_cache_crl_recheck_hit
                                                        .saturating_add(1);
                                                crl_gate_allows_reuse = true;
                                            }
                                        }
                                        Err(_) => {
                                            child_cert_cache_miss_crl_invalid =
                                                child_cert_cache_miss_crl_invalid.saturating_add(1);
                                        }
                                    }
                                }
                                ChildCertificateCacheCrlGate::Expired => {
                                    child_cert_cache_miss_crl_expired =
                                        child_cert_cache_miss_crl_expired.saturating_add(1);
                                }
                                ChildCertificateCacheCrlGate::Invalid => {
                                    child_cert_cache_miss_crl_invalid =
                                        child_cert_cache_miss_crl_invalid.saturating_add(1);
                                }
                                ChildCertificateCacheCrlGate::Missing => {
                                    child_cert_cache_miss_crl_missing =
                                        child_cert_cache_miss_crl_missing.saturating_add(1);
                                }
                            }

                            if crl_gate_allows_reuse {
                                match &projection.payload {
                                    ChildCertificateCachePayload::ChildCa {
                                        child_manifest_rsync_uri,
                                        child_ski,
                                        child_rsync_base_uri,
                                        child_publication_point_rsync_uri,
                                        child_rrdp_notification_uri,
                                        child_effective_ip_resources,
                                        child_effective_as_resources,
                                        ..
                                    } => {
                                        child_cert_cache_lookup_nanos =
                                            child_cert_cache_lookup_nanos.saturating_add(
                                                lookup_started
                                                    .elapsed()
                                                    .as_nanos()
                                                    .min(u128::from(u64::MAX))
                                                    as u64,
                                            );
                                        out.push(DiscoveredChildCaInstance {
                                            handle: CaInstanceHandle {
                                                depth: 0,
                                                tal_id: issuer.tal_id.clone(),
                                                parent_manifest_rsync_uri: Some(
                                                    issuer.manifest_rsync_uri.clone(),
                                                ),
                                                ca_certificate: CaCertificateRef::repo_bytes(
                                                    child_cert_sha256_hex.clone(),
                                                ),
                                                ca_certificate_rsync_uri: Some(f.rsync_uri.clone()),
                                                effective_ip_resources:
                                                    child_effective_ip_resources.clone(),
                                                effective_as_resources:
                                                    child_effective_as_resources.clone(),
                                                rsync_base_uri: child_rsync_base_uri.clone(),
                                                manifest_rsync_uri: child_manifest_rsync_uri
                                                    .clone(),
                                                publication_point_rsync_uri:
                                                    child_publication_point_rsync_uri.clone(),
                                                rrdp_notification_uri: child_rrdp_notification_uri
                                                    .clone(),
                                            },
                                            discovered_from: crate::audit::DiscoveredFrom {
                                                parent_manifest_rsync_uri: issuer
                                                    .manifest_rsync_uri
                                                    .clone(),
                                                child_ca_certificate_rsync_uri: f.rsync_uri.clone(),
                                                child_ca_certificate_sha256_hex:
                                                    child_cert_sha256_hex.clone(),
                                            },
                                            child_entry_projection: Some(
                                                DiscoveredChildEntryProjection {
                                                    child_ski: child_ski.clone(),
                                                },
                                            ),
                                        });
                                        ca_ok = ca_ok.saturating_add(1);
                                        child_cert_cache_hit =
                                            child_cert_cache_hit.saturating_add(1);
                                        child_cert_cache_hit_ca =
                                            child_cert_cache_hit_ca.saturating_add(1);
                                        audits.push(ObjectAuditEntry {
                                        rsync_uri: f.rsync_uri.clone(),
                                        sha256_hex: child_cert_sha256_hex.clone(),
                                        kind: AuditObjectKind::Certificate,
                                        result: AuditObjectResult::Ok,
                                        detail: Some(
                                            "restored subordinate CA discovery from child certificate validation cache"
                                                .to_string(),
                                        ),
                                    });
                                        continue;
                                    }
                                    ChildCertificateCachePayload::Router {
                                        router_keys: cached_keys,
                                    } => {
                                        for cached_key in cached_keys {
                                            router_keys.push(RouterKeyPayload {
                                                as_id: cached_key.as_id,
                                                ski: cached_key.ski.clone(),
                                                spki_der: cached_key.spki_der.clone(),
                                                source_object_uri: f.rsync_uri.clone(),
                                                source_object_hash: child_cert_sha256_hex.clone(),
                                                source_ee_cert_hash: child_cert_sha256_hex.clone(),
                                                item_effective_until: cached_key
                                                    .item_effective_until
                                                    .clone(),
                                            });
                                        }
                                        router_ok = router_ok.saturating_add(1);
                                        child_cert_cache_hit =
                                            child_cert_cache_hit.saturating_add(1);
                                        child_cert_cache_hit_router =
                                            child_cert_cache_hit_router.saturating_add(1);
                                        audits.push(ObjectAuditEntry {
                                        rsync_uri: f.rsync_uri.clone(),
                                        sha256_hex: child_cert_sha256_hex.clone(),
                                        kind: AuditObjectKind::RouterCertificate,
                                        result: AuditObjectResult::Ok,
                                        detail: Some(
                                            "restored BGPsec router certificate from child certificate validation cache"
                                                .to_string(),
                                        ),
                                    });
                                        child_cert_cache_lookup_nanos =
                                            child_cert_cache_lookup_nanos.saturating_add(
                                                lookup_started
                                                    .elapsed()
                                                    .as_nanos()
                                                    .min(u128::from(u64::MAX))
                                                    as u64,
                                            );
                                        continue;
                                    }
                                }
                            }
                        }
                    }
                    None => {
                        child_cert_cache_miss_not_found =
                            child_cert_cache_miss_not_found.saturating_add(1);
                    }
                }
            }
            child_cert_cache_lookup_nanos = child_cert_cache_lookup_nanos.saturating_add(
                lookup_started
                    .elapsed()
                    .as_nanos()
                    .min(u128::from(u64::MAX)) as u64,
            );
        }

        let child_der = load_child_certificate_der_for_discovery(
            f,
            &mut child_cert_der_load_fresh_nanos,
            &mut child_cert_der_load_fresh_count,
        )?;

        let tdecode = std::time::Instant::now();
        let child_cert = match crate::data_model::rc::ResourceCertificate::decode_der(child_der) {
            Ok(v) => v,
            Err(e) => {
                ca_error = ca_error.saturating_add(1);
                audits.push(ObjectAuditEntry {
                    rsync_uri: f.rsync_uri.clone(),
                    sha256_hex: sha256_hex_from_32(&f.sha256),
                    kind: AuditObjectKind::Certificate,
                    result: AuditObjectResult::Error,
                    detail: Some(format!("child certificate decode failed: {e}")),
                });
                continue;
            }
        };
        child_decode_nanos = child_decode_nanos
            .saturating_add(tdecode.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64);

        let t0 = std::time::Instant::now();
        let issuer_crl_uri = match select_issuer_crl_uri_for_child(&child_cert, &crl_cache) {
            Ok(v) => v.to_string(),
            Err(e) => {
                crl_select_error = crl_select_error.saturating_add(1);
                audits.push(ObjectAuditEntry {
                    rsync_uri: f.rsync_uri.clone(),
                    sha256_hex: sha256_hex_from_32(&f.sha256),
                    kind: AuditObjectKind::Certificate,
                    result: AuditObjectResult::Error,
                    detail: Some(format!(
                        "cannot select issuer CRL for child certificate: {e}"
                    )),
                });
                continue;
            }
        };
        select_crl_nanos = select_crl_nanos
            .saturating_add(t0.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64);

        let t1 = std::time::Instant::now();
        let Some(issuer_ca_ref) = issuer_ca.as_ref() else {
            ca_error = ca_error.saturating_add(1);
            audits.push(ObjectAuditEntry {
                rsync_uri: f.rsync_uri.clone(),
                sha256_hex: sha256_hex_from_32(&f.sha256),
                kind: AuditObjectKind::Certificate,
                result: AuditObjectResult::Error,
                detail: Some(
                    issuer_ca_decode_error
                        .clone()
                        .unwrap_or_else(|| "issuer CA decode failed".to_string()),
                ),
            });
            continue;
        };
        let Some(issuer_spki_ref) = issuer_spki.as_ref() else {
            ca_error = ca_error.saturating_add(1);
            audits.push(ObjectAuditEntry {
                rsync_uri: f.rsync_uri.clone(),
                sha256_hex: sha256_hex_from_32(&f.sha256),
                kind: AuditObjectKind::Certificate,
                result: AuditObjectResult::Error,
                detail: Some(
                    issuer_spki_error
                        .clone()
                        .unwrap_or_else(|| "issuer SubjectPublicKeyInfo unavailable".to_string()),
                ),
            });
            continue;
        };
        let validated = match validate_subordinate_ca_cert_with_cached_issuer(
            child_der,
            child_cert,
            issuer_ca_der,
            issuer_ca_ref,
            issuer_spki_ref,
            issuer_crl_uri.as_str(),
            &mut crl_cache,
            issuer.ca_certificate_rsync_uri.as_deref(),
            issuer.effective_ip_resources.as_ref(),
            issuer.effective_as_resources.as_ref(),
            &issuer_resources_index,
            validation_time,
            policy.resource_validation_mode,
        ) {
            Ok(v) => v,
            Err(CaPathError::ChildNotCa) => {
                let tr = std::time::Instant::now();
                let router_result = match ensure_issuer_crl_verified(
                    issuer_crl_uri.as_str(),
                    &mut crl_cache,
                    issuer_ca_der,
                ) {
                    Ok(verified_crl) => {
                        BgpsecRouterCertificate::validate_path_with_prevalidated_issuer(
                            child_der,
                            issuer_ca_ref,
                            issuer_spki_ref,
                            &verified_crl.crl,
                            &verified_crl.revoked_serials,
                            issuer.ca_certificate_rsync_uri.as_deref(),
                            Some(issuer_crl_uri.as_str()),
                            validation_time,
                        )
                    }
                    Err(err) => {
                        validate_router_nanos = validate_router_nanos.saturating_add(
                            tr.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
                        );
                        router_error = router_error.saturating_add(1);
                        audits.push(ObjectAuditEntry {
                            rsync_uri: f.rsync_uri.clone(),
                            sha256_hex: sha256_hex_from_32(&f.sha256),
                            kind: AuditObjectKind::RouterCertificate,
                            result: AuditObjectResult::Error,
                            detail: Some(format!(
                                "router certificate issuer CRL validation failed: {err}"
                            )),
                        });
                        continue;
                    }
                };
                validate_router_nanos = validate_router_nanos
                    .saturating_add(tr.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64);

                match router_result {
                    Ok(router) => {
                        let router_asns = match router_asns_for_resource_mode(
                            &router.asns,
                            issuer.effective_as_resources.as_ref(),
                            policy.resource_validation_mode,
                        ) {
                            Ok(v) => v,
                            Err(detail) => {
                                router_error = router_error.saturating_add(1);
                                audits.push(ObjectAuditEntry {
                                    rsync_uri: f.rsync_uri.clone(),
                                    sha256_hex: sha256_hex_from_32(&f.sha256),
                                    kind: AuditObjectKind::RouterCertificate,
                                    result: AuditObjectResult::Error,
                                    detail: Some(format!(
                                        "router certificate AS resource validation failed: {detail}"
                                    )),
                                });
                                continue;
                            }
                        };
                        router_ok = router_ok.saturating_add(1);
                        let source_object_hash = sha256_hex_from_32(&f.sha256);
                        let item_effective_until = PackTime::from_utc_offset_datetime(
                            router.resource_cert.tbs.validity_not_after,
                        );
                        for as_id in &router_asns {
                            router_keys.push(RouterKeyPayload {
                                as_id: *as_id,
                                ski: router.subject_key_identifier.clone(),
                                spki_der: router.spki_der.clone(),
                                source_object_uri: f.rsync_uri.clone(),
                                source_object_hash: source_object_hash.clone(),
                                source_ee_cert_hash: source_object_hash.clone(),
                                item_effective_until: item_effective_until.clone(),
                            });
                        }
                        if let Some(cache) = cache_context {
                            let write_started = std::time::Instant::now();
                            if let Ok(verified_crl) = ensure_issuer_crl_verified(
                                issuer_crl_uri.as_str(),
                                &mut crl_cache,
                                issuer_ca_der,
                            ) {
                                let (effective_not_before, effective_until) =
                                    child_certificate_cache_certificate_window(
                                        router.resource_cert.tbs.validity_not_before,
                                        router.resource_cert.tbs.validity_not_after,
                                        issuer_ca_ref.tbs.validity_not_before,
                                        issuer_ca_ref.tbs.validity_not_after,
                                    );
                                let cache_key_sha256_hex = child_certificate_cache_key_sha256_hex(
                                    &f.rsync_uri,
                                    &f.sha256,
                                    &cache.issuer_ca_sha256,
                                    &cache.ca_validation_context_digest,
                                    &cache.policy_fingerprint,
                                );
                                let projection = ChildCertificateCacheProjection {
                                    schema_version: CHILD_CERTIFICATE_CACHE_SCHEMA_VERSION,
                                    algorithm_version: CHILD_CERTIFICATE_CACHE_ALGORITHM_VERSION,
                                    cache_key_sha256_hex,
                                    child_cert_uri: f.rsync_uri.clone(),
                                    child_cert_sha256_hex: child_cert_sha256_hex.clone(),
                                    child_cert_serial: BigUnsigned::from_biguint(
                                        &router.resource_cert.tbs.serial_number,
                                    )
                                    .bytes_be,
                                    issuer_ca_sha256_hex: sha256_hex_from_32(
                                        &cache.issuer_ca_sha256,
                                    ),
                                    issuer_crl_uri: issuer_crl_uri.clone(),
                                    issuer_crl_sha256_hex: verified_crl.sha256_hex.clone(),
                                    ca_validation_context_digest: cache
                                        .ca_validation_context_digest,
                                    validation_policy_fingerprint: cache.policy_fingerprint,
                                    effective_not_before,
                                    effective_until,
                                    payload: ChildCertificateCachePayload::Router {
                                        router_keys: router_asns
                                            .iter()
                                            .map(|as_id| ChildCertificateCacheRouterKeyProjection {
                                                as_id: *as_id,
                                                ski: router.subject_key_identifier.clone(),
                                                spki_der: router.spki_der.clone(),
                                                item_effective_until: item_effective_until.clone(),
                                            })
                                            .collect(),
                                    },
                                };
                                if cache
                                    .store
                                    .put_child_certificate_cache_projection(&projection)
                                    .is_ok()
                                {
                                    remember_child_certificate_cache_dirty_projection(
                                        &mut child_cache_segment_dirty_projections,
                                        &projection,
                                    );
                                    child_cert_cache_write_ok =
                                        child_cert_cache_write_ok.saturating_add(1);
                                } else {
                                    child_cert_cache_write_error =
                                        child_cert_cache_write_error.saturating_add(1);
                                }
                            } else {
                                child_cert_cache_write_error =
                                    child_cert_cache_write_error.saturating_add(1);
                            }
                            child_cert_cache_write_nanos = child_cert_cache_write_nanos
                                .saturating_add(
                                    write_started.elapsed().as_nanos().min(u128::from(u64::MAX))
                                        as u64,
                                );
                        }
                        audits.push(ObjectAuditEntry {
                            rsync_uri: f.rsync_uri.clone(),
                            sha256_hex: sha256_hex_from_32(&f.sha256),
                            kind: AuditObjectKind::RouterCertificate,
                            result: AuditObjectResult::Ok,
                            detail: Some(
                                "validated BGPsec router certificate (RFC 8209); no child CA instance enqueued"
                                    .to_string(),
                            ),
                        });
                    }
                    Err(err) if is_non_router_certificate(&err) => {
                        ca_skipped_not_ca = ca_skipped_not_ca.saturating_add(1);
                        router_skipped_non_router = router_skipped_non_router.saturating_add(1);
                        audits.push(ObjectAuditEntry {
                            rsync_uri: f.rsync_uri.clone(),
                            sha256_hex: sha256_hex_from_32(&f.sha256),
                            kind: AuditObjectKind::Certificate,
                            result: AuditObjectResult::Skipped,
                            detail: Some(
                                "skipped: not a CA resource certificate or BGPsec router certificate"
                                    .to_string(),
                            ),
                        });
                    }
                    Err(err) => {
                        router_error = router_error.saturating_add(1);
                        audits.push(ObjectAuditEntry {
                            rsync_uri: f.rsync_uri.clone(),
                            sha256_hex: sha256_hex_from_32(&f.sha256),
                            kind: AuditObjectKind::RouterCertificate,
                            result: AuditObjectResult::Error,
                            detail: Some(format!("router certificate validation failed: {err}")),
                        });
                    }
                }
                continue;
            }
            Err(e) => {
                ca_error = ca_error.saturating_add(1);
                audits.push(ObjectAuditEntry {
                    rsync_uri: f.rsync_uri.clone(),
                    sha256_hex: sha256_hex_from_32(&f.sha256),
                    kind: AuditObjectKind::Certificate,
                    result: AuditObjectResult::Error,
                    detail: Some(format!("child CA validation failed: {e}")),
                });
                continue;
            }
        };
        validate_sub_ca_nanos = validate_sub_ca_nanos
            .saturating_add(t1.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64);

        if !validated.resource_warnings.is_empty() {
            audits.push(ObjectAuditEntry {
                rsync_uri: f.rsync_uri.clone(),
                sha256_hex: sha256_hex_from_32(&f.sha256),
                kind: AuditObjectKind::Certificate,
                result: AuditObjectResult::Ok,
                detail: Some(format!(
                    "resource validation warning ({:?}): {}",
                    policy.resource_validation_mode,
                    validated.resource_warnings.summary()
                )),
            });
        }

        let eff_ip_items = ip_item_count(validated.effective_ip_resources.as_ref());
        match bucketize(eff_ip_items) {
            0 => eff_ip_items_bucket_le_10 = eff_ip_items_bucket_le_10.saturating_add(1),
            1 => eff_ip_items_bucket_le_100 = eff_ip_items_bucket_le_100.saturating_add(1),
            _ => eff_ip_items_bucket_gt_100 = eff_ip_items_bucket_gt_100.saturating_add(1),
        }
        let eff_as_items = as_item_count(validated.effective_as_resources.as_ref());
        match bucketize(eff_as_items) {
            0 => eff_as_items_bucket_le_10 = eff_as_items_bucket_le_10.saturating_add(1),
            1 => eff_as_items_bucket_le_100 = eff_as_items_bucket_le_100.saturating_add(1),
            _ => eff_as_items_bucket_gt_100 = eff_as_items_bucket_gt_100.saturating_add(1),
        }

        let t2 = std::time::Instant::now();
        let uris = match ca_instance_uris_from_ca_certificate(&validated.child_ca) {
            Ok(v) => v,
            Err(e) => {
                uri_discovery_error = uri_discovery_error.saturating_add(1);
                audits.push(ObjectAuditEntry {
                    rsync_uri: f.rsync_uri.clone(),
                    sha256_hex: sha256_hex_from_32(&f.sha256),
                    kind: AuditObjectKind::Certificate,
                    result: AuditObjectResult::Error,
                    detail: Some(format!("CA instance URI discovery failed: {e}")),
                });
                continue;
            }
        };
        uri_discovery_nanos = uri_discovery_nanos
            .saturating_add(t2.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64);

        let t3 = std::time::Instant::now();
        let child_rsync_base_uri = uris.rsync_base_uri.clone();
        let child_manifest_rsync_uri = uris.manifest_rsync_uri.clone();
        let child_publication_point_rsync_uri = uris.publication_point_rsync_uri.clone();
        let child_rrdp_notification_uri = uris.rrdp_notification_uri.clone();
        out.push(DiscoveredChildCaInstance {
            handle: CaInstanceHandle {
                depth: 0,
                tal_id: issuer.tal_id.clone(),
                parent_manifest_rsync_uri: Some(issuer.manifest_rsync_uri.clone()),
                ca_certificate: CaCertificateRef::inline_der(child_der.to_vec()),
                ca_certificate_rsync_uri: Some(f.rsync_uri.clone()),
                effective_ip_resources: validated.effective_ip_resources.clone(),
                effective_as_resources: validated.effective_as_resources.clone(),
                rsync_base_uri: child_rsync_base_uri.clone(),
                manifest_rsync_uri: child_manifest_rsync_uri.clone(),
                publication_point_rsync_uri: child_publication_point_rsync_uri.clone(),
                rrdp_notification_uri: child_rrdp_notification_uri.clone(),
            },
            discovered_from: crate::audit::DiscoveredFrom {
                parent_manifest_rsync_uri: issuer.manifest_rsync_uri.clone(),
                child_ca_certificate_rsync_uri: f.rsync_uri.clone(),
                child_ca_certificate_sha256_hex: child_cert_sha256_hex.clone(),
            },
            child_entry_projection: validated
                .child_ca
                .tbs
                .extensions
                .subject_key_identifier
                .as_ref()
                .map(|child_ski| DiscoveredChildEntryProjection {
                    child_ski: hex::encode(child_ski),
                }),
        });
        enqueue_nanos =
            enqueue_nanos.saturating_add(t3.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64);

        if let Some(cache) = cache_context {
            let write_started = std::time::Instant::now();
            if let Ok(verified_crl) =
                ensure_issuer_crl_verified(issuer_crl_uri.as_str(), &mut crl_cache, issuer_ca_der)
            {
                if let Some(child_ski) = validated
                    .child_ca
                    .tbs
                    .extensions
                    .subject_key_identifier
                    .as_ref()
                {
                    let (effective_not_before, effective_until) =
                        child_certificate_cache_certificate_window(
                            validated.child_ca.tbs.validity_not_before,
                            validated.child_ca.tbs.validity_not_after,
                            issuer_ca_ref.tbs.validity_not_before,
                            issuer_ca_ref.tbs.validity_not_after,
                        );
                    let cache_key_sha256_hex = child_certificate_cache_key_sha256_hex(
                        &f.rsync_uri,
                        &f.sha256,
                        &cache.issuer_ca_sha256,
                        &cache.ca_validation_context_digest,
                        &cache.policy_fingerprint,
                    );
                    let projection = ChildCertificateCacheProjection {
                        schema_version: CHILD_CERTIFICATE_CACHE_SCHEMA_VERSION,
                        algorithm_version: CHILD_CERTIFICATE_CACHE_ALGORITHM_VERSION,
                        cache_key_sha256_hex,
                        child_cert_uri: f.rsync_uri.clone(),
                        child_cert_sha256_hex: child_cert_sha256_hex.clone(),
                        child_cert_serial: BigUnsigned::from_biguint(
                            &validated.child_ca.tbs.serial_number,
                        )
                        .bytes_be,
                        issuer_ca_sha256_hex: sha256_hex_from_32(&cache.issuer_ca_sha256),
                        issuer_crl_uri: issuer_crl_uri.clone(),
                        issuer_crl_sha256_hex: verified_crl.sha256_hex.clone(),
                        ca_validation_context_digest: cache.ca_validation_context_digest,
                        validation_policy_fingerprint: cache.policy_fingerprint,
                        effective_not_before,
                        effective_until,
                        payload: ChildCertificateCachePayload::ChildCa {
                            child_manifest_rsync_uri,
                            child_ski: hex::encode(child_ski),
                            child_rsync_base_uri,
                            child_publication_point_rsync_uri,
                            child_rrdp_notification_uri,
                            child_effective_ip_resources: validated.effective_ip_resources.clone(),
                            child_effective_as_resources: validated.effective_as_resources.clone(),
                        },
                    };
                    if cache
                        .store
                        .put_child_certificate_cache_projection(&projection)
                        .is_ok()
                    {
                        remember_child_certificate_cache_dirty_projection(
                            &mut child_cache_segment_dirty_projections,
                            &projection,
                        );
                        child_cert_cache_write_ok = child_cert_cache_write_ok.saturating_add(1);
                    } else {
                        child_cert_cache_write_error =
                            child_cert_cache_write_error.saturating_add(1);
                    }
                } else {
                    child_cert_cache_write_error = child_cert_cache_write_error.saturating_add(1);
                }
            } else {
                child_cert_cache_write_error = child_cert_cache_write_error.saturating_add(1);
            }
            child_cert_cache_write_nanos =
                child_cert_cache_write_nanos.saturating_add(
                    write_started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
                );
        }

        ca_ok = ca_ok.saturating_add(1);
        audits.push(ObjectAuditEntry {
            rsync_uri: f.rsync_uri.clone(),
            sha256_hex: child_cert_sha256_hex.clone(),
            kind: AuditObjectKind::Certificate,
            result: AuditObjectResult::Ok,
            detail: Some("validated subordinate CA certificate; enqueued CA instance".to_string()),
        });
    }

    if let (Some(cache), Some(dirty_projections)) =
        (cache_context, child_cache_segment_dirty_projections.take())
    {
        if !dirty_projections.is_empty() {
            let write_started = std::time::Instant::now();
            let projections = dirty_projections.into_values().collect::<Vec<_>>();
            child_cert_cache_mmap_write_entries = projections.len() as u64;
            match cache
                .store
                .write_child_certificate_cache_mmap_segment_overlay(
                    publication_point.manifest_rsync_uri(),
                    &child_cache_segment_keys,
                    &projections,
                ) {
                Ok(stats) => {
                    child_cert_cache_mmap_write_file_bytes = stats.file_bytes;
                }
                Err(_) => {
                    child_cert_cache_mmap_write_errors =
                        child_cert_cache_mmap_write_errors.saturating_add(1);
                }
            }
            child_cert_cache_mmap_write_nanos =
                write_started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64;
        }
    }

    if let Some(t) = timing {
        t.record_count("child_cer_seen", cer_seen);
        t.record_count("child_ca_ok", ca_ok);
        t.record_count("child_ca_error", ca_error);
        t.record_count("child_ca_skipped_not_ca", ca_skipped_not_ca);
        t.record_count("child_router_ok", router_ok);
        t.record_count("child_router_error", router_error);
        t.record_count("child_router_skipped_non_router", router_skipped_non_router);
        t.record_count("child_crl_select_error", crl_select_error);
        t.record_count("child_uri_discovery_error", uri_discovery_error);
        t.record_count("child_certificate_cache_lookup", child_cert_cache_lookup);
        t.record_count("child_certificate_cache_hit", child_cert_cache_hit);
        t.record_count("child_certificate_cache_hit_ca", child_cert_cache_hit_ca);
        t.record_count(
            "child_certificate_cache_hit_router",
            child_cert_cache_hit_router,
        );
        t.record_count(
            "child_certificate_cache_miss_not_found",
            child_cert_cache_miss_not_found,
        );
        t.record_count(
            "child_certificate_cache_miss_time_gate",
            child_cert_cache_miss_time_gate,
        );
        t.record_count(
            "child_certificate_cache_miss_crl_missing",
            child_cert_cache_miss_crl_missing,
        );
        t.record_count(
            "child_certificate_cache_miss_crl_invalid",
            child_cert_cache_miss_crl_invalid,
        );
        t.record_count(
            "child_certificate_cache_miss_crl_expired",
            child_cert_cache_miss_crl_expired,
        );
        t.record_count(
            "child_certificate_cache_miss_revoked",
            child_cert_cache_miss_revoked,
        );
        t.record_count(
            "child_certificate_cache_crl_recheck_hit",
            child_cert_cache_crl_recheck_hit,
        );
        t.record_count(
            "child_certificate_cache_crl_gate_reused",
            child_cert_cache_crl_gate_reused,
        );
        t.record_count(
            "child_certificate_cache_load_error",
            child_cert_cache_load_error,
        );
        t.record_count(
            "child_certificate_cache_write_ok",
            child_cert_cache_write_ok,
        );
        t.record_count(
            "child_certificate_cache_write_error",
            child_cert_cache_write_error,
        );
        t.record_count(
            "child_certificate_cache_batch_lookup_publication_points",
            child_cert_cache_batch_lookup_publication_points,
        );
        t.record_count(
            "child_certificate_cache_batch_lookup_entries",
            child_cert_cache_batch_lookup_entries,
        );
        t.record_count(
            "child_certificate_cache_batch_lookup_errors",
            child_cert_cache_batch_lookup_errors,
        );
        t.record_count(
            "child_certificate_cache_mmap_lookup_publication_points",
            child_cert_cache_mmap_lookup_publication_points,
        );
        t.record_count(
            "child_certificate_cache_mmap_lookup_entries",
            child_cert_cache_mmap_lookup_entries,
        );
        t.record_count(
            "child_certificate_cache_mmap_lookup_hits",
            child_cert_cache_mmap_lookup_hits,
        );
        t.record_count(
            "child_certificate_cache_mmap_lookup_misses",
            child_cert_cache_mmap_lookup_misses,
        );
        t.record_count(
            "child_certificate_cache_mmap_lookup_missing_segments",
            child_cert_cache_mmap_lookup_missing_segments,
        );
        t.record_count(
            "child_certificate_cache_mmap_lookup_errors",
            child_cert_cache_mmap_lookup_errors,
        );
        t.record_count(
            "child_certificate_cache_mmap_lookup_file_bytes",
            child_cert_cache_mmap_lookup_file_bytes,
        );
        t.record_count(
            "child_certificate_cache_mmap_write_entries",
            child_cert_cache_mmap_write_entries,
        );
        t.record_count(
            "child_certificate_cache_mmap_write_errors",
            child_cert_cache_mmap_write_errors,
        );
        t.record_count(
            "child_certificate_cache_mmap_write_file_bytes",
            child_cert_cache_mmap_write_file_bytes,
        );
        t.record_count(
            "child_certificate_der_load_cache_hit_count",
            child_cert_der_load_cache_hit_count,
        );
        t.record_count(
            "child_certificate_der_load_fresh_count",
            child_cert_der_load_fresh_count,
        );

        t.record_count("child_effective_ip_items_le_10", eff_ip_items_bucket_le_10);
        t.record_count(
            "child_effective_ip_items_le_100",
            eff_ip_items_bucket_le_100,
        );
        t.record_count(
            "child_effective_ip_items_gt_100",
            eff_ip_items_bucket_gt_100,
        );
        t.record_count("child_effective_as_items_le_10", eff_as_items_bucket_le_10);
        t.record_count(
            "child_effective_as_items_le_100",
            eff_as_items_bucket_le_100,
        );
        t.record_count(
            "child_effective_as_items_gt_100",
            eff_as_items_bucket_gt_100,
        );

        t.record_phase_nanos("child_select_issuer_crl_total", select_crl_nanos);
        t.record_phase_nanos("child_decode_certificate_total", child_decode_nanos);
        t.record_phase_nanos("child_validate_subordinate_total", validate_sub_ca_nanos);
        t.record_phase_nanos(
            "child_validate_router_certificate_total",
            validate_router_nanos,
        );
        t.record_phase_nanos("child_ca_instance_uri_discovery_total", uri_discovery_nanos);
        t.record_phase_nanos("child_enqueue_total", enqueue_nanos);
        t.record_phase_nanos(
            "child_certificate_cache_lookup_total",
            child_cert_cache_lookup_nanos,
        );
        t.record_phase_nanos(
            "child_certificate_cache_write_total",
            child_cert_cache_write_nanos,
        );
        t.record_phase_nanos(
            "child_certificate_cache_batch_lookup_total",
            child_cert_cache_batch_lookup_nanos,
        );
        t.record_phase_nanos(
            "child_certificate_cache_mmap_lookup_total",
            child_cert_cache_mmap_lookup_nanos,
        );
        t.record_phase_nanos(
            "child_certificate_cache_mmap_write_total",
            child_cert_cache_mmap_write_nanos,
        );
        t.record_phase_nanos(
            "child_certificate_der_load_cache_hit_total",
            child_cert_der_load_cache_hit_nanos,
        );
        t.record_phase_nanos(
            "child_certificate_der_load_fresh_total",
            child_cert_der_load_fresh_nanos,
        );
        t.record_phase_nanos(
            "child_certificate_der_load_total",
            child_cert_der_load_cache_hit_nanos.saturating_add(child_cert_der_load_fresh_nanos),
        );
    }

    Ok(ChildDiscoveryOutput {
        children: out,
        audits,
        router_keys,
    })
}

fn is_non_router_certificate(err: &BgpsecRouterCertificatePathError) -> bool {
    matches!(
        err,
        BgpsecRouterCertificatePathError::Decode(BgpsecRouterCertificateDecodeError::Validate(
            BgpsecRouterCertificateProfileError::NotEe
                | BgpsecRouterCertificateProfileError::MissingExtendedKeyUsage
                | BgpsecRouterCertificateProfileError::MissingBgpsecRouterEku
        ))
    )
}

fn router_asns_for_resource_mode(
    router_asns: &[u32],
    issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
    mode: ResourceValidationMode,
) -> Result<Vec<u32>, String> {
    let Some(issuer_effective_as) = issuer_effective_as else {
        return Err("issuer has no effective AS resources".to_string());
    };

    match mode {
        ResourceValidationMode::Rfc6487 => {
            let outside: Vec<u32> = router_asns
                .iter()
                .copied()
                .filter(|asn| !as_resource_set_contains_asn(issuer_effective_as, *asn))
                .collect();
            if outside.is_empty() {
                Ok(router_asns.to_vec())
            } else {
                Err(format!(
                    "router AS resources are not a subset of issuer effective AS resources: {outside:?}"
                ))
            }
        }
        ResourceValidationMode::ValidationUpdate03 => {
            let filtered: Vec<u32> = router_asns
                .iter()
                .copied()
                .filter(|asn| as_resource_set_contains_asn(issuer_effective_as, *asn))
                .collect();
            if filtered.is_empty() {
                Err("router AS resources have empty validated resource set".to_string())
            } else {
                Ok(filtered)
            }
        }
    }
}

fn as_resource_set_contains_asn(
    resources: &crate::data_model::rc::AsResourceSet,
    asn: u32,
) -> bool {
    let Some(choice) = resources.asnum.as_ref() else {
        return false;
    };
    match choice {
        crate::data_model::rc::AsIdentifierChoice::Inherit => false,
        crate::data_model::rc::AsIdentifierChoice::AsIdsOrRanges(items) => {
            items.iter().any(|item| match item {
                crate::data_model::rc::AsIdOrRange::Id(id) => *id == asn,
                crate::data_model::rc::AsIdOrRange::Range { min, max } => {
                    *min <= asn && asn <= *max
                }
            })
        }
    }
}

fn select_issuer_crl_uri_for_child<'a>(
    child: &'a crate::data_model::rc::ResourceCertificate,
    crl_cache: &std::collections::HashMap<String, CachedIssuerCrl>,
) -> Result<&'a str, String> {
    if crl_cache.is_empty() {
        return Err(
            "no CRL available in publication point snapshot (cannot validate certificates) (RFC 9286 §7; RFC 6487 §4.8.6)"
                .to_string(),
        );
    }
    let Some(crldp_uris) = child.tbs.extensions.crl_distribution_points_uris.as_ref() else {
        return Err(
            "child certificate CRLDistributionPoints missing (RFC 6487 §4.8.6)".to_string(),
        );
    };

    for u in crldp_uris {
        let s = u.as_str();
        if crl_cache.contains_key(s) {
            return Ok(s);
        }
    }

    Err(format!(
        "CRL referenced by child certificate CRLDistributionPoints not found in publication point snapshot: {} (RFC 6487 §4.8.6; RFC 9286 §4.2.1)",
        crldp_uris
            .iter()
            .map(|u| u.as_str())
            .collect::<Vec<_>>()
            .join(", ")
    ))
}

fn ensure_issuer_crl_verified<'a>(
    crl_rsync_uri: &str,
    crl_cache: &'a mut std::collections::HashMap<String, CachedIssuerCrl>,
    issuer_ca_der: &[u8],
) -> Result<&'a VerifiedIssuerCrl, CaPathError> {
    let entry = crl_cache
        .get_mut(crl_rsync_uri)
        .expect("CRL must exist in cache");
    match entry {
        CachedIssuerCrl::Ok(v) => Ok(v),
        CachedIssuerCrl::Pending {
            bytes,
            sha256_hex: cached_sha256_hex,
        } => {
            let der = std::mem::take(bytes);
            let crl = crate::data_model::crl::RpkixCrl::decode_der(&der)?;
            crl.verify_signature_with_issuer_certificate_der(issuer_ca_der)?;
            let sha256_hex = cached_sha256_hex
                .take()
                .unwrap_or_else(|| crate::audit::sha256_hex(&der));

            let mut revoked_serials: std::collections::HashSet<Vec<u8>> =
                std::collections::HashSet::with_capacity(crl.revoked_certs.len());
            for rc in &crl.revoked_certs {
                revoked_serials.insert(rc.serial_number.bytes_be.clone());
            }

            *entry = CachedIssuerCrl::Ok(VerifiedIssuerCrl {
                crl,
                revoked_serials,
                sha256_hex,
            });
            match entry {
                CachedIssuerCrl::Ok(v) => Ok(v),
                _ => unreachable!(),
            }
        }
    }
}

fn validate_subordinate_ca_cert_with_cached_issuer(
    child_ca_der: &[u8],
    child_ca: crate::data_model::rc::ResourceCertificate,
    issuer_ca_der: &[u8],
    issuer_ca: &crate::data_model::rc::ResourceCertificate,
    issuer_spki: &SubjectPublicKeyInfo<'_>,
    issuer_crl_rsync_uri: &str,
    crl_cache: &mut std::collections::HashMap<String, CachedIssuerCrl>,
    issuer_ca_rsync_uri: Option<&str>,
    issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
    issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
    issuer_resources_index: &IssuerEffectiveResourcesIndex,
    validation_time: time::OffsetDateTime,
    resource_validation_mode: crate::policy::ResourceValidationMode,
) -> Result<ValidatedSubordinateCaLite, CaPathError> {
    let verified_crl = ensure_issuer_crl_verified(issuer_crl_rsync_uri, crl_cache, issuer_ca_der)?;

    validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources(
        child_ca_der,
        child_ca,
        issuer_ca,
        issuer_spki,
        &verified_crl.crl,
        &verified_crl.revoked_serials,
        issuer_ca_rsync_uri,
        issuer_crl_rsync_uri,
        issuer_effective_ip,
        issuer_effective_as,
        issuer_resources_index,
        validation_time,
        resource_validation_mode,
    )
}

#[cfg(test)]
fn select_issuer_crl_from_snapshot<'a>(
    child_cert_der: &[u8],
    pack: &'a PublicationPointSnapshot,
) -> Result<(&'a str, &'a [u8]), String> {
    let child = crate::data_model::rc::ResourceCertificate::decode_der(child_cert_der)
        .map_err(|e| format!("child certificate decode failed: {e}"))?;
    let Some(crldp_uris) = child.tbs.extensions.crl_distribution_points_uris.as_ref() else {
        return Err(
            "child certificate CRLDistributionPoints missing (RFC 6487 §4.8.6)".to_string(),
        );
    };

    for u in crldp_uris {
        let s = u.as_str();
        if let Some(f) = pack.files.iter().find(|f| f.rsync_uri == s) {
            let bytes = f
                .bytes()
                .map_err(|e| format!("snapshot CRL bytes load failed: {e}"))?;
            return Ok((f.rsync_uri.as_str(), bytes));
        }
    }

    Err(format!(
        "CRL referenced by child certificate CRLDistributionPoints not found in publication point snapshot: {} (RFC 6487 §4.8.6; RFC 9286 §4.2.1)",
        crldp_uris
            .iter()
            .map(|u| u.as_str())
            .collect::<Vec<_>>()
            .join(", ")
    ))
}

fn kind_from_rsync_uri(uri: &str) -> AuditObjectKind {
    if uri.ends_with(".crl") {
        AuditObjectKind::Crl
    } else if uri.ends_with(".cer") {
        AuditObjectKind::Certificate
    } else if uri.ends_with(".roa") {
        AuditObjectKind::Roa
    } else if uri.ends_with(".asa") {
        AuditObjectKind::Aspa
    } else {
        AuditObjectKind::Other
    }
}

fn source_label(source: PublicationPointSource) -> String {
    match source {
        PublicationPointSource::Fresh => "fresh".to_string(),
        PublicationPointSource::PublicationPointCache => "publication_point_cache".to_string(),
        PublicationPointSource::VcirCurrentInstance => "vcir_current_instance".to_string(),
        PublicationPointSource::FailedFetchNoCache => "failed_fetch_no_cache".to_string(),
    }
}

fn repo_sync_phase_label(phase: crate::sync::repo::RepoSyncPhase) -> &'static str {
    match phase {
        crate::sync::repo::RepoSyncPhase::RrdpOk => "rrdp_ok",
        crate::sync::repo::RepoSyncPhase::RrdpFailedRsyncOk => "rrdp_failed_rsync_ok",
        crate::sync::repo::RepoSyncPhase::RsyncOnlyOk => "rsync_only_ok",
        crate::sync::repo::RepoSyncPhase::ReplayRrdpOk => "replay_rrdp_ok",
        crate::sync::repo::RepoSyncPhase::ReplayRsyncOk => "replay_rsync_ok",
        crate::sync::repo::RepoSyncPhase::ReplayNoopRrdp => "replay_noop_rrdp",
        crate::sync::repo::RepoSyncPhase::ReplayNoopRsync => "replay_noop_rsync",
    }
}

fn repo_sync_failure_phase_label(
    attempted_rrdp: bool,
    original_notification_uri: Option<&str>,
    effective_notification_uri: Option<&str>,
) -> &'static str {
    if attempted_rrdp && original_notification_uri.is_some() && effective_notification_uri.is_some()
    {
        "rrdp_failed_rsync_failed"
    } else if attempted_rrdp
        && original_notification_uri.is_some()
        && effective_notification_uri.is_none()
    {
        "rsync_only_failed_after_rrdp_dedup"
    } else {
        "rsync_only_failed"
    }
}

fn terminal_state_label(source: PublicationPointSource) -> &'static str {
    match source {
        PublicationPointSource::Fresh => "fresh",
        PublicationPointSource::PublicationPointCache => "publication_point_cache",
        PublicationPointSource::VcirCurrentInstance => "fallback_current_instance",
        PublicationPointSource::FailedFetchNoCache => "failed_no_cache",
    }
}

fn repo_sync_source_label(source: crate::sync::repo::RepoSyncSource) -> &'static str {
    match source {
        crate::sync::repo::RepoSyncSource::Rrdp => "rrdp",
        crate::sync::repo::RepoSyncSource::Rsync => "rsync",
    }
}

fn effective_repo_sync_duration_ms(
    elapsed_ms: u64,
    runtime_reported_duration_ms: Option<u64>,
    repo_sync_ok: bool,
) -> u64 {
    if repo_sync_ok {
        return elapsed_ms;
    }
    runtime_reported_duration_ms
        .map(|runtime_ms| elapsed_ms.max(runtime_ms))
        .unwrap_or(elapsed_ms)
}

fn kind_from_vcir_artifact_kind(kind: VcirArtifactKind) -> AuditObjectKind {
    match kind {
        VcirArtifactKind::Mft => AuditObjectKind::Manifest,
        VcirArtifactKind::Crl => AuditObjectKind::Crl,
        VcirArtifactKind::Cer => AuditObjectKind::Certificate,
        VcirArtifactKind::Roa => AuditObjectKind::Roa,
        VcirArtifactKind::Aspa => AuditObjectKind::Aspa,
        VcirArtifactKind::Gbr | VcirArtifactKind::Tal | VcirArtifactKind::Other => {
            AuditObjectKind::Other
        }
    }
}

fn audit_result_from_vcir_status(status: VcirArtifactValidationStatus) -> AuditObjectResult {
    match status {
        VcirArtifactValidationStatus::Accepted => AuditObjectResult::Ok,
        VcirArtifactValidationStatus::Rejected => AuditObjectResult::Error,
        VcirArtifactValidationStatus::WarningOnly => AuditObjectResult::Skipped,
    }
}

/// Fallback detail for cached artifacts rejected by an earlier run whose cache
/// entry predates reject-reason recording (field added 2026-07-27).
const CACHED_REJECT_REASON_NOT_RECORDED: &str =
    "rejected in an earlier validation run (reject reason not recorded in cache)";

/// Detail for an audit entry rebuilt from a cached artifact: the real reject
/// reason when the cache recorded one, an explicit fallback for legacy cache
/// entries, and `None` for non-rejected artifacts.
fn audit_detail_from_vcir_status(
    status: VcirArtifactValidationStatus,
    reject_reason: Option<&str>,
) -> Option<String> {
    match status {
        VcirArtifactValidationStatus::Rejected => Some(
            reject_reason
                .map(str::to_string)
                .unwrap_or_else(|| CACHED_REJECT_REASON_NOT_RECORDED.to_string()),
        ),
        VcirArtifactValidationStatus::Accepted | VcirArtifactValidationStatus::WarningOnly => None,
    }
}

fn build_publication_point_audit_from_snapshot(
    ca: &CaInstanceHandle,
    source: PublicationPointSource,
    repo_sync_source: Option<&str>,
    repo_sync_phase: Option<&str>,
    repo_sync_duration_ms: Option<u64>,
    repo_sync_error: Option<&str>,
    pack: &PublicationPointSnapshot,
    runner_warnings: &[Warning],
    objects: &crate::validation::objects::ObjectsOutput,
    child_audits: &[ObjectAuditEntry],
) -> PublicationPointAudit {
    use crate::data_model::crl::RpkixCrl;
    use std::collections::HashMap;

    let locked_files = &pack.files;
    let mut audit_by_uri: HashMap<String, ObjectAuditEntry> = HashMap::new();
    for f in locked_files {
        audit_by_uri.insert(
            f.rsync_uri.clone(),
            ObjectAuditEntry {
                rsync_uri: f.rsync_uri.clone(),
                sha256_hex: sha256_hex_from_32(&f.sha256),
                kind: kind_from_rsync_uri(&f.rsync_uri),
                result: AuditObjectResult::Skipped,
                detail: Some("skipped: not processed in stage2".to_string()),
            },
        );
    }

    for f in locked_files {
        if !f.rsync_uri.ends_with(".crl") {
            continue;
        }
        let ok = f
            .bytes()
            .ok()
            .and_then(|bytes| RpkixCrl::decode_der(bytes).ok())
            .is_some();
        audit_by_uri.insert(
            f.rsync_uri.clone(),
            ObjectAuditEntry {
                rsync_uri: f.rsync_uri.clone(),
                sha256_hex: sha256_hex_from_32(&f.sha256),
                kind: AuditObjectKind::Crl,
                result: if ok {
                    AuditObjectResult::Ok
                } else {
                    AuditObjectResult::Error
                },
                detail: if ok {
                    None
                } else {
                    Some("CRL decode failed".to_string())
                },
            },
        );
    }

    for e in child_audits {
        audit_by_uri.insert(e.rsync_uri.clone(), e.clone());
    }
    for e in &objects.audit {
        audit_by_uri.insert(e.rsync_uri.clone(), e.clone());
    }

    let mut objects_out: Vec<ObjectAuditEntry> = Vec::with_capacity(pack.files.len() + 1);
    objects_out.push(ObjectAuditEntry {
        rsync_uri: pack.manifest_rsync_uri.clone(),
        sha256_hex: sha256_hex(&pack.manifest_bytes),
        kind: AuditObjectKind::Manifest,
        result: AuditObjectResult::Ok,
        detail: None,
    });
    for f in locked_files {
        if let Some(e) = audit_by_uri.remove(&f.rsync_uri) {
            objects_out.push(e);
        } else {
            objects_out.push(ObjectAuditEntry {
                rsync_uri: f.rsync_uri.clone(),
                sha256_hex: sha256_hex_from_32(&f.sha256),
                kind: kind_from_rsync_uri(&f.rsync_uri),
                result: AuditObjectResult::Skipped,
                detail: Some("skipped: no audit entry".to_string()),
            });
        }
    }

    let mut warnings = Vec::new();
    warnings.extend(runner_warnings.iter().map(AuditWarning::from));
    warnings.extend(objects.warnings.iter().map(AuditWarning::from));

    PublicationPointAudit {
        node_id: None,
        parent_node_id: None,
        discovered_from: None,
        rsync_base_uri: ca.rsync_base_uri.clone(),
        manifest_rsync_uri: ca.manifest_rsync_uri.clone(),
        publication_point_rsync_uri: ca.publication_point_rsync_uri.clone(),
        rrdp_notification_uri: ca.rrdp_notification_uri.clone(),
        source: source_label(source),
        repo_sync_source: repo_sync_source.map(ToString::to_string),
        repo_sync_phase: repo_sync_phase.map(ToString::to_string),
        repo_sync_duration_ms,
        repo_sync_error: repo_sync_error.map(ToString::to_string),
        repo_terminal_state: terminal_state_label(source).to_string(),
        this_update_rfc3339_utc: pack.this_update.rfc3339_utc.clone(),
        next_update_rfc3339_utc: pack.next_update.rfc3339_utc.clone(),
        verified_at_rfc3339_utc: pack.verified_at.rfc3339_utc.clone(),
        warnings,
        objects: objects_out,
    }
}

fn build_publication_point_audit_from_vcir(
    ca: &CaInstanceHandle,
    source: PublicationPointSource,
    repo_sync_source: Option<&str>,
    repo_sync_phase: Option<&str>,
    repo_sync_duration_ms: Option<u64>,
    repo_sync_error: Option<&str>,
    vcir: Option<&ValidatedCaInstanceResult>,
    pack: Option<&PublicationPointSnapshot>,
    runner_warnings: &[Warning],
    objects: &crate::validation::objects::ObjectsOutput,
    child_audits: &[ObjectAuditEntry],
    fresh_failure_audits: &[ObjectAuditEntry],
) -> PublicationPointAudit {
    if let Some(pack) = pack {
        return build_publication_point_audit_from_snapshot(
            ca,
            source,
            repo_sync_source,
            repo_sync_phase,
            repo_sync_duration_ms,
            repo_sync_error,
            pack,
            runner_warnings,
            objects,
            child_audits,
        );
    }

    let mut warnings = Vec::new();
    warnings.extend(runner_warnings.iter().map(AuditWarning::from));
    warnings.extend(objects.warnings.iter().map(AuditWarning::from));

    let Some(vcir) = vcir else {
        return PublicationPointAudit {
            node_id: None,
            parent_node_id: None,
            discovered_from: None,
            rsync_base_uri: ca.rsync_base_uri.clone(),
            manifest_rsync_uri: ca.manifest_rsync_uri.clone(),
            publication_point_rsync_uri: ca.publication_point_rsync_uri.clone(),
            rrdp_notification_uri: ca.rrdp_notification_uri.clone(),
            source: source_label(source),
            repo_sync_source: repo_sync_source.map(ToString::to_string),
            repo_sync_phase: repo_sync_phase.map(ToString::to_string),
            repo_sync_duration_ms,
            repo_sync_error: repo_sync_error.map(ToString::to_string),
            repo_terminal_state: terminal_state_label(source).to_string(),
            this_update_rfc3339_utc: String::new(),
            next_update_rfc3339_utc: String::new(),
            verified_at_rfc3339_utc: String::new(),
            warnings,
            objects: fresh_failure_audits.to_vec(),
        };
    };

    if source == PublicationPointSource::FailedFetchNoCache {
        let mut objects_out = Vec::with_capacity(
            objects.audit.len() + child_audits.len() + fresh_failure_audits.len(),
        );
        objects_out.extend(child_audits.iter().cloned());
        objects_out.extend(objects.audit.iter().cloned());
        objects_out.extend(fresh_failure_audits.iter().cloned());
        return PublicationPointAudit {
            node_id: None,
            parent_node_id: None,
            discovered_from: None,
            rsync_base_uri: ca.rsync_base_uri.clone(),
            manifest_rsync_uri: ca.manifest_rsync_uri.clone(),
            publication_point_rsync_uri: ca.publication_point_rsync_uri.clone(),
            rrdp_notification_uri: ca.rrdp_notification_uri.clone(),
            source: source_label(source),
            repo_sync_source: repo_sync_source.map(ToString::to_string),
            repo_sync_phase: repo_sync_phase.map(ToString::to_string),
            repo_sync_duration_ms,
            repo_sync_error: repo_sync_error.map(ToString::to_string),
            repo_terminal_state: terminal_state_label(source).to_string(),
            this_update_rfc3339_utc: vcir
                .validated_manifest_meta
                .validated_manifest_this_update
                .rfc3339_utc
                .clone(),
            next_update_rfc3339_utc: vcir
                .validated_manifest_meta
                .validated_manifest_next_update
                .rfc3339_utc
                .clone(),
            verified_at_rfc3339_utc: vcir.last_successful_validation_time.rfc3339_utc.clone(),
            warnings,
            objects: objects_out,
        };
    }

    let mut audit_by_uri: HashMap<String, ObjectAuditEntry> = HashMap::new();
    for artifact in &vcir.related_artifacts {
        let Some(uri) = artifact.uri.as_ref() else {
            continue;
        };
        audit_by_uri.insert(
            uri.clone(),
            ObjectAuditEntry {
                rsync_uri: uri.clone(),
                sha256_hex: artifact.sha256.clone(),
                kind: kind_from_vcir_artifact_kind(artifact.artifact_kind),
                result: audit_result_from_vcir_status(artifact.validation_status),
                detail: audit_detail_from_vcir_status(
                    artifact.validation_status,
                    artifact.reject_reason.as_deref(),
                ),
            },
        );
    }
    for e in child_audits {
        audit_by_uri.insert(e.rsync_uri.clone(), e.clone());
    }
    for e in &objects.audit {
        audit_by_uri.insert(e.rsync_uri.clone(), e.clone());
    }

    let mut ordered_uris: Vec<String> = vcir
        .related_artifacts
        .iter()
        .filter_map(|artifact| artifact.uri.clone())
        .collect();
    ordered_uris.sort();
    ordered_uris.dedup();

    let mut objects_out: Vec<ObjectAuditEntry> = Vec::new();
    if let Some(entry) = audit_by_uri.remove(&vcir.current_manifest_rsync_uri) {
        objects_out.push(entry);
    } else {
        objects_out.push(ObjectAuditEntry {
            rsync_uri: vcir.current_manifest_rsync_uri.clone(),
            sha256_hex: vcir
                .related_artifacts
                .iter()
                .find(|artifact| {
                    artifact.artifact_role == VcirArtifactRole::Manifest
                        && artifact.uri.as_deref() == Some(vcir.current_manifest_rsync_uri.as_str())
                })
                .map(|artifact| artifact.sha256.clone())
                .unwrap_or_default(),
            kind: AuditObjectKind::Manifest,
            result: AuditObjectResult::Ok,
            detail: None,
        });
    }

    for uri in ordered_uris {
        if uri == vcir.current_manifest_rsync_uri {
            continue;
        }
        if let Some(entry) = audit_by_uri.remove(&uri) {
            objects_out.push(entry);
        }
    }

    let mut audit_objects = objects_out.clone();
    audit_objects.extend(fresh_failure_audits.iter().cloned());

    PublicationPointAudit {
        node_id: None,
        parent_node_id: None,
        discovered_from: None,
        rsync_base_uri: ca.rsync_base_uri.clone(),
        manifest_rsync_uri: ca.manifest_rsync_uri.clone(),
        publication_point_rsync_uri: ca.publication_point_rsync_uri.clone(),
        rrdp_notification_uri: ca.rrdp_notification_uri.clone(),
        source: source_label(source),
        repo_sync_source: repo_sync_source.map(ToString::to_string),
        repo_sync_phase: repo_sync_phase.map(ToString::to_string),
        repo_sync_duration_ms,
        repo_sync_error: repo_sync_error.map(ToString::to_string),
        repo_terminal_state: terminal_state_label(source).to_string(),
        this_update_rfc3339_utc: vcir
            .validated_manifest_meta
            .validated_manifest_this_update
            .rfc3339_utc
            .clone(),
        next_update_rfc3339_utc: vcir
            .validated_manifest_meta
            .validated_manifest_next_update
            .rfc3339_utc
            .clone(),
        verified_at_rfc3339_utc: vcir.last_successful_validation_time.rfc3339_utc.clone(),
        warnings,
        objects: audit_objects,
    }
}

fn build_publication_point_audit_from_publication_point_cache_projection(
    ca: &CaInstanceHandle,
    source: PublicationPointSource,
    repo_sync_source: Option<&str>,
    repo_sync_phase: Option<&str>,
    repo_sync_duration_ms: Option<u64>,
    repo_sync_error: Option<&str>,
    projection: &PublicationPointCacheProjection,
    validation_time: time::OffsetDateTime,
    runner_warnings: &[Warning],
    objects: &crate::validation::objects::ObjectsOutput,
    child_audits: &[ObjectAuditEntry],
) -> PublicationPointAudit {
    let mut warnings = Vec::new();
    warnings.extend(runner_warnings.iter().map(AuditWarning::from));
    warnings.extend(objects.warnings.iter().map(AuditWarning::from));

    let mut audit_by_uri: HashMap<String, ObjectAuditEntry> = HashMap::new();
    for artifact in &projection.related_objects {
        let Some(uri) = artifact.uri.as_ref() else {
            continue;
        };
        audit_by_uri.insert(
            uri.clone(),
            ObjectAuditEntry {
                rsync_uri: uri.clone(),
                sha256_hex: artifact.sha256.clone(),
                kind: kind_from_vcir_artifact_kind(artifact.artifact_kind),
                result: audit_result_from_vcir_status(artifact.validation_status),
                detail: audit_detail_from_vcir_status(
                    artifact.validation_status,
                    artifact.reject_reason.as_deref(),
                ),
            },
        );
    }
    for entry in child_audits {
        audit_by_uri.insert(entry.rsync_uri.clone(), entry.clone());
    }
    for entry in &objects.audit {
        audit_by_uri.insert(entry.rsync_uri.clone(), entry.clone());
    }

    let mut ordered_uris: Vec<String> = projection
        .related_objects
        .iter()
        .filter_map(|artifact| artifact.uri.clone())
        .collect();
    ordered_uris.sort();
    ordered_uris.dedup();

    let mut objects_out: Vec<ObjectAuditEntry> = Vec::with_capacity(ordered_uris.len().max(1));
    if let Some(entry) = audit_by_uri.remove(&projection.manifest_rsync_uri) {
        objects_out.push(entry);
    } else {
        objects_out.push(ObjectAuditEntry {
            rsync_uri: projection.manifest_rsync_uri.clone(),
            sha256_hex: hex::encode(projection.manifest_sha256),
            kind: AuditObjectKind::Manifest,
            result: AuditObjectResult::Ok,
            detail: None,
        });
    }

    for uri in ordered_uris {
        if uri == projection.manifest_rsync_uri {
            continue;
        }
        if let Some(entry) = audit_by_uri.remove(&uri) {
            objects_out.push(entry);
        }
    }

    PublicationPointAudit {
        node_id: None,
        parent_node_id: None,
        discovered_from: None,
        rsync_base_uri: ca.rsync_base_uri.clone(),
        manifest_rsync_uri: ca.manifest_rsync_uri.clone(),
        publication_point_rsync_uri: ca.publication_point_rsync_uri.clone(),
        rrdp_notification_uri: ca.rrdp_notification_uri.clone(),
        source: source_label(source),
        repo_sync_source: repo_sync_source.map(ToString::to_string),
        repo_sync_phase: repo_sync_phase.map(ToString::to_string),
        repo_sync_duration_ms,
        repo_sync_error: repo_sync_error.map(ToString::to_string),
        repo_terminal_state: terminal_state_label(source).to_string(),
        this_update_rfc3339_utc: projection.manifest_this_update.rfc3339_utc.clone(),
        next_update_rfc3339_utc: projection.manifest_next_update.rfc3339_utc.clone(),
        verified_at_rfc3339_utc: PackTime::from_utc_offset_datetime(validation_time).rfc3339_utc,
        warnings,
        objects: objects_out,
    }
}

fn parse_snapshot_time_value(pack_time: &PackTime) -> Result<time::OffsetDateTime, String> {
    time::OffsetDateTime::parse(
        &pack_time.rfc3339_utc,
        &time::format_description::well_known::Rfc3339,
    )
    .map_err(|e| format!("invalid RFC3339 time '{}': {e}", pack_time.rfc3339_utc))
}

fn empty_objects_output() -> crate::validation::objects::ObjectsOutput {
    crate::validation::objects::ObjectsOutput {
        vrps: Vec::new(),
        aspas: Vec::new(),
        router_keys: Vec::new(),
        local_outputs_cache: Vec::new(),
        warnings: Vec::new(),
        stats: crate::validation::objects::ObjectsStats::default(),
        audit: Vec::new(),
        roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(),
        roa_cache_object_meta: Vec::new(),
    }
}

fn reuse_ccr_manifest_projection_from_vcir(
    ca: &CaInstanceHandle,
    vcir: &ValidatedCaInstanceResult,
) -> Result<VcirCcrManifestProjection, String> {
    if vcir.ccr_manifest_projection.manifest_rsync_uri != ca.manifest_rsync_uri {
        return Err(format!(
            "vcir CCR manifest projection URI mismatch: expected {}, got {}",
            ca.manifest_rsync_uri, vcir.ccr_manifest_projection.manifest_rsync_uri
        ));
    }
    Ok(vcir.ccr_manifest_projection.clone())
}

fn project_current_instance_vcir_on_failed_fetch(
    store: &RocksStore,
    ca: &CaInstanceHandle,
    fresh_err: &ManifestFreshError,
    policy: &Policy,
    validation_time: time::OffsetDateTime,
) -> Result<VcirReuseProjection, String> {
    let mut warnings = Vec::new();

    let Some(vcir) = store
        .get_vcir(&ca.manifest_rsync_uri)
        .map_err(|e| format!("load VCIR failed: {e}"))?
    else {
        return Ok(failed_fetch_no_cache_projection(
            ca,
            fresh_err,
            None,
            "no latest validated result for current CA instance; no cached output reused",
        ));
    };

    if !vcir.audit_summary.failed_fetch_eligible {
        return Ok(failed_fetch_no_cache_projection(
            ca,
            fresh_err,
            Some(vcir),
            "latest VCIR is not marked failed-fetch eligible; no cached output reused",
        ));
    }

    let reuse_identity = match store.get_vcir_failed_fetch_reuse_identity(&ca.manifest_rsync_uri) {
        Ok(Some(identity)) => identity,
        Ok(None) => {
            return Ok(failed_fetch_no_cache_projection(
                ca,
                fresh_err,
                Some(vcir),
                "latest VCIR reuse identity is missing; no cached output or child reused",
            ));
        }
        Err(error) => {
            return Ok(failed_fetch_no_cache_projection(
                ca,
                fresh_err,
                Some(vcir),
                &format!(
                    "latest VCIR reuse identity is invalid ({error}); no cached output or child reused"
                ),
            ));
        }
    };
    if !failed_fetch_reuse_identity_matches_current(
        &reuse_identity,
        &vcir,
        ca,
        policy,
        validation_time,
    ) {
        return Ok(failed_fetch_no_cache_projection(
            ca,
            fresh_err,
            Some(vcir),
            "latest VCIR reuse identity does not match the current CA context or time window; no cached output or child reused",
        ));
    }

    let ccr_manifest_projection = reuse_ccr_manifest_projection_from_vcir(ca, &vcir)?;
    if fresh_err.should_warn_when_current_instance_reused() {
        warnings.push(
            Warning::new(format!("manifest failed fetch: {fresh_err}"))
                .with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
                .with_context(&ca.manifest_rsync_uri),
        );
    }
    // Current-instance reuse is fully described by VCIR projections; rebuilding a
    // byte-backed snapshot here only duplicates repo-byte I/O and creates warning noise.
    let snapshot = None;
    let objects = build_objects_output_from_vcir(&vcir, validation_time, &mut warnings);
    let (discovered_children, child_audits) =
        restore_children_from_vcir(store, ca, &vcir, &mut warnings);

    Ok(VcirReuseProjection {
        source: PublicationPointSource::VcirCurrentInstance,
        vcir: Some(vcir),
        ccr_manifest_projection: Some(ccr_manifest_projection),
        snapshot,
        objects,
        child_audits,
        discovered_children,
        warnings,
    })
}

fn failed_fetch_no_cache_projection(
    ca: &CaInstanceHandle,
    fresh_err: &ManifestFreshError,
    vcir: Option<ValidatedCaInstanceResult>,
    reason: &str,
) -> VcirReuseProjection {
    let warnings = vec![
        Warning::new(format!("manifest failed fetch: {fresh_err}"))
            .with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
            .with_context(&ca.manifest_rsync_uri),
        Warning::new(reason)
            .with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
            .with_context(&ca.manifest_rsync_uri),
    ];
    VcirReuseProjection {
        source: PublicationPointSource::FailedFetchNoCache,
        vcir,
        ccr_manifest_projection: None,
        snapshot: None,
        objects: empty_objects_output(),
        child_audits: Vec::new(),
        discovered_children: Vec::new(),
        warnings,
    }
}

fn failed_fetch_reuse_identity_for_fresh_result(
    ca: &CaInstanceHandle,
    policy: &Policy,
    validation_time: time::OffsetDateTime,
    effective_until: PackTime,
) -> Result<VcirFailedFetchReuseIdentity, String> {
    let current_ca_sha256 = ca.ca_certificate_sha256_32().ok_or_else(|| {
        "current CA certificate hash unavailable for VCIR reuse identity".to_string()
    })?;
    let identity = VcirFailedFetchReuseIdentity {
        current_ca_sha256,
        ta_context_digest: ta_context_digest_for_ca(ca),
        ca_validation_context_digest: ca_validation_context_digest_for_ca(ca),
        policy_fingerprint: publication_point_cache_policy_fingerprint(policy),
        effective_not_before: PackTime::from_utc_offset_datetime(validation_time),
        effective_until,
    };
    identity
        .validate_internal()
        .map_err(|error| error.to_string())?;
    Ok(identity)
}

fn failed_fetch_reuse_identity_matches_current(
    cached: &VcirFailedFetchReuseIdentity,
    vcir: &ValidatedCaInstanceResult,
    ca: &CaInstanceHandle,
    policy: &Policy,
    validation_time: time::OffsetDateTime,
) -> bool {
    let Some(current_ca_sha256) = ca.ca_certificate_sha256_32() else {
        return false;
    };
    cached.current_ca_sha256 == current_ca_sha256
        && cached.ta_context_digest == ta_context_digest_for_ca(ca)
        && cached.ca_validation_context_digest == ca_validation_context_digest_for_ca(ca)
        && cached.policy_fingerprint == publication_point_cache_policy_fingerprint(policy)
        && cached.effective_until == vcir.instance_gate.instance_effective_until
        && cached.contains_validation_time(validation_time)
}

#[cfg(test)]
fn reconstruct_snapshot_from_vcir(
    store: &RocksStore,
    ca: &CaInstanceHandle,
    vcir: &ValidatedCaInstanceResult,
    warnings: &mut Vec<Warning>,
) -> Option<PublicationPointSnapshot> {
    let manifest_artifact = vcir.related_artifacts.iter().find(|artifact| {
        artifact.artifact_role == VcirArtifactRole::Manifest
            && artifact.uri.as_deref() == Some(ca.manifest_rsync_uri.as_str())
    })?;
    let manifest_bytes = match store.get_blob_bytes(&manifest_artifact.sha256) {
        Ok(Some(bytes)) => bytes,
        Ok(None) => {
            warnings.push(
                Warning::new("manifest raw bytes missing for VCIR audit reconstruction")
                    .with_context(&ca.manifest_rsync_uri),
            );
            return None;
        }
        Err(e) => {
            warnings.push(
                Warning::new(format!(
                    "manifest raw bytes load failed for VCIR audit reconstruction: {e}"
                ))
                .with_context(&ca.manifest_rsync_uri),
            );
            return None;
        }
    };

    let mut seen = HashSet::new();
    let mut files = Vec::new();
    for artifact in &vcir.related_artifacts {
        let Some(uri) = artifact.uri.as_ref() else {
            continue;
        };
        if artifact.artifact_role == VcirArtifactRole::Manifest
            || artifact.artifact_role == VcirArtifactRole::IssuerCert
            || artifact.artifact_role == VcirArtifactRole::TrustAnchorCert
            || artifact.artifact_role == VcirArtifactRole::Tal
        {
            continue;
        }
        if !seen.insert(uri.clone()) {
            continue;
        }
        match store.get_blob_bytes(&artifact.sha256) {
            Ok(Some(bytes)) => files.push(PackFile::from_bytes_compute_sha256(uri, bytes)),
            Ok(None) => warnings.push(
                Warning::new("related artifact raw bytes missing for VCIR audit reconstruction")
                    .with_context(uri),
            ),
            Err(e) => warnings.push(
                Warning::new(format!(
                    "related artifact raw bytes load failed for VCIR audit reconstruction: {e}"
                ))
                .with_context(uri),
            ),
        }
    }

    Some(PublicationPointSnapshot {
        format_version: PublicationPointSnapshot::FORMAT_VERSION_V1,
        publication_point_rsync_uri: ca.publication_point_rsync_uri.clone(),
        manifest_rsync_uri: ca.manifest_rsync_uri.clone(),
        manifest_number_be: vcir
            .validated_manifest_meta
            .validated_manifest_number
            .clone(),
        this_update: vcir
            .validated_manifest_meta
            .validated_manifest_this_update
            .clone(),
        next_update: vcir
            .validated_manifest_meta
            .validated_manifest_next_update
            .clone(),
        verified_at: vcir.last_successful_validation_time.clone(),
        manifest_bytes,
        files,
    })
}

fn audit_kind_for_vcir_output_type(output_type: VcirOutputType) -> AuditObjectKind {
    match output_type {
        VcirOutputType::Vrp => AuditObjectKind::Roa,
        VcirOutputType::Aspa => AuditObjectKind::Aspa,
        VcirOutputType::RouterKey => AuditObjectKind::RouterCertificate,
    }
}

fn build_objects_output_from_vcir(
    vcir: &ValidatedCaInstanceResult,
    validation_time: time::OffsetDateTime,
    warnings: &mut Vec<Warning>,
) -> crate::validation::objects::ObjectsOutput {
    let mut output = empty_objects_output();
    let mut audit_by_uri: HashMap<String, ObjectAuditEntry> = HashMap::new();
    let mut roa_total: HashSet<String> = HashSet::new();
    let mut aspa_total: HashSet<String> = HashSet::new();
    let mut roa_ok: HashSet<String> = HashSet::new();
    let mut aspa_ok: HashSet<String> = HashSet::new();

    for artifact in &vcir.related_artifacts {
        if artifact.artifact_role != VcirArtifactRole::SignedObject {
            continue;
        }
        if let Some(uri) = artifact.uri.as_ref() {
            match artifact.artifact_kind {
                VcirArtifactKind::Roa => {
                    roa_total.insert(uri.clone());
                }
                VcirArtifactKind::Aspa => {
                    aspa_total.insert(uri.clone());
                }
                _ => {}
            }
        }
    }

    for local in &vcir.local_outputs {
        let effective_until = match parse_snapshot_time_value(&local.item_effective_until) {
            Ok(v) => v,
            Err(e) => {
                warnings.push(
                    Warning::new(format!(
                        "cached local output has invalid item_effective_until: {e}"
                    ))
                    .with_context(&local.source_object_uri),
                );
                audit_by_uri.insert(
                    local.source_object_uri.clone(),
                    ObjectAuditEntry {
                        rsync_uri: local.source_object_uri.clone(),
                        sha256_hex: local.source_object_hash_hex(),
                        kind: audit_kind_for_vcir_output_type(local.output_type),
                        result: AuditObjectResult::Error,
                        detail: Some(
                            "cached local output has invalid item_effective_until".to_string(),
                        ),
                    },
                );
                continue;
            }
        };
        if validation_time > effective_until {
            audit_by_uri
                .entry(local.source_object_uri.clone())
                .or_insert_with(|| ObjectAuditEntry {
                    rsync_uri: local.source_object_uri.clone(),
                    sha256_hex: local.source_object_hash_hex(),
                    kind: audit_kind_for_vcir_output_type(local.output_type),
                    result: AuditObjectResult::Skipped,
                    detail: Some("skipped: cached local output expired".to_string()),
                });
            continue;
        }

        match local.output_type {
            VcirOutputType::Vrp => match parse_vcir_vrp_output(local) {
                Ok(vrp) => {
                    roa_ok.insert(local.source_object_uri.clone());
                    output.vrps.push(vrp);
                    audit_by_uri.insert(
                        local.source_object_uri.clone(),
                        ObjectAuditEntry {
                            rsync_uri: local.source_object_uri.clone(),
                            sha256_hex: local.source_object_hash_hex(),
                            kind: AuditObjectKind::Roa,
                            result: AuditObjectResult::Ok,
                            detail: None,
                        },
                    );
                }
                Err(e) => {
                    warnings.push(
                        Warning::new(format!("cached ROA local output parse failed: {e}"))
                            .with_context(&local.source_object_uri),
                    );
                    audit_by_uri.insert(
                        local.source_object_uri.clone(),
                        ObjectAuditEntry {
                            rsync_uri: local.source_object_uri.clone(),
                            sha256_hex: local.source_object_hash_hex(),
                            kind: AuditObjectKind::Roa,
                            result: AuditObjectResult::Error,
                            detail: Some(format!("cached ROA local output parse failed: {e}")),
                        },
                    );
                }
            },
            VcirOutputType::Aspa => match parse_vcir_aspa_output(local) {
                Ok(aspa) => {
                    aspa_ok.insert(local.source_object_uri.clone());
                    output.aspas.push(aspa);
                    audit_by_uri.insert(
                        local.source_object_uri.clone(),
                        ObjectAuditEntry {
                            rsync_uri: local.source_object_uri.clone(),
                            sha256_hex: local.source_object_hash_hex(),
                            kind: AuditObjectKind::Aspa,
                            result: AuditObjectResult::Ok,
                            detail: None,
                        },
                    );
                }
                Err(e) => {
                    warnings.push(
                        Warning::new(format!("cached ASPA local output parse failed: {e}"))
                            .with_context(&local.source_object_uri),
                    );
                    audit_by_uri.insert(
                        local.source_object_uri.clone(),
                        ObjectAuditEntry {
                            rsync_uri: local.source_object_uri.clone(),
                            sha256_hex: local.source_object_hash_hex(),
                            kind: AuditObjectKind::Aspa,
                            result: AuditObjectResult::Error,
                            detail: Some(format!("cached ASPA local output parse failed: {e}")),
                        },
                    );
                }
            },
            VcirOutputType::RouterKey => match parse_vcir_router_key_output(local) {
                Ok(router_key) => {
                    output.router_keys.push(router_key);
                    audit_by_uri.insert(
                        local.source_object_uri.clone(),
                        ObjectAuditEntry {
                            rsync_uri: local.source_object_uri.clone(),
                            sha256_hex: local.source_object_hash_hex(),
                            kind: AuditObjectKind::RouterCertificate,
                            result: AuditObjectResult::Ok,
                            detail: Some("cached Router Key local output restored".to_string()),
                        },
                    );
                }
                Err(e) => {
                    warnings.push(
                        Warning::new(format!("cached Router Key local output parse failed: {e}"))
                            .with_context(&local.source_object_uri),
                    );
                    audit_by_uri.insert(
                        local.source_object_uri.clone(),
                        ObjectAuditEntry {
                            rsync_uri: local.source_object_uri.clone(),
                            sha256_hex: local.source_object_hash_hex(),
                            kind: AuditObjectKind::RouterCertificate,
                            result: AuditObjectResult::Error,
                            detail: Some(format!(
                                "cached Router Key local output parse failed: {e}"
                            )),
                        },
                    );
                }
            },
        }
    }

    output.stats.roa_total = roa_total.len();
    output.stats.roa_ok = roa_ok.len();
    output.stats.aspa_total = aspa_total.len();
    output.stats.aspa_ok = aspa_ok.len();
    let mut audit: Vec<_> = audit_by_uri.into_values().collect();
    audit.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri));
    output.audit = audit;
    output
}

fn build_objects_output_from_publication_point_cache_projection(
    projection: &PublicationPointCacheProjection,
    validation_time: time::OffsetDateTime,
    warnings: &mut Vec<Warning>,
) -> crate::validation::objects::ObjectsOutput {
    let mut output = empty_objects_output();
    let mut audit_by_uri: HashMap<String, ObjectAuditEntry> = HashMap::new();
    let mut roa_total: HashSet<String> = HashSet::new();
    let mut aspa_total: HashSet<String> = HashSet::new();
    let mut roa_ok: HashSet<String> = HashSet::new();
    let mut aspa_ok: HashSet<String> = HashSet::new();

    for artifact in &projection.related_objects {
        if artifact.artifact_role != VcirArtifactRole::SignedObject {
            continue;
        }
        if let Some(uri) = artifact.uri.as_ref() {
            match artifact.artifact_kind {
                VcirArtifactKind::Roa => {
                    roa_total.insert(uri.clone());
                }
                VcirArtifactKind::Aspa => {
                    aspa_total.insert(uri.clone());
                }
                _ => {}
            }
        }
    }

    for projected in &projection.outputs {
        let effective_until = match parse_snapshot_time_value(&projected.item_effective_until) {
            Ok(value) => value,
            Err(err) => {
                warnings.push(
                    Warning::new(format!(
                        "publication-point cached local output has invalid item_effective_until: {err}"
                    ))
                    .with_context(&projected.source_object_uri),
                );
                audit_by_uri.insert(
                    projected.source_object_uri.clone(),
                    ObjectAuditEntry {
                        rsync_uri: projected.source_object_uri.clone(),
                        sha256_hex: hex::encode(projected.source_object_hash),
                        kind: audit_kind_for_vcir_output_type(projected.output_type),
                        result: AuditObjectResult::Error,
                        detail: Some(
                            "publication-point cached local output has invalid item_effective_until"
                                .to_string(),
                        ),
                    },
                );
                continue;
            }
        };
        if validation_time > effective_until {
            audit_by_uri
                .entry(projected.source_object_uri.clone())
                .or_insert_with(|| ObjectAuditEntry {
                    rsync_uri: projected.source_object_uri.clone(),
                    sha256_hex: hex::encode(projected.source_object_hash),
                    kind: audit_kind_for_vcir_output_type(projected.output_type),
                    result: AuditObjectResult::Skipped,
                    detail: Some(
                        "skipped: publication-point cached local output expired".to_string(),
                    ),
                });
            continue;
        }

        match projected.output_type {
            VcirOutputType::Vrp => match parse_publication_point_cache_vrp_output(projected) {
                Ok(vrp) => {
                    roa_ok.insert(projected.source_object_uri.clone());
                    output.vrps.push(vrp);
                    audit_by_uri.insert(
                        projected.source_object_uri.clone(),
                        ObjectAuditEntry {
                            rsync_uri: projected.source_object_uri.clone(),
                            sha256_hex: hex::encode(projected.source_object_hash),
                            kind: AuditObjectKind::Roa,
                            result: AuditObjectResult::Ok,
                            detail: None,
                        },
                    );
                }
                Err(err) => {
                    warnings.push(
                        Warning::new(format!(
                            "publication-point cached ROA local output parse failed: {err}"
                        ))
                        .with_context(&projected.source_object_uri),
                    );
                    audit_by_uri.insert(
                        projected.source_object_uri.clone(),
                        ObjectAuditEntry {
                            rsync_uri: projected.source_object_uri.clone(),
                            sha256_hex: hex::encode(projected.source_object_hash),
                            kind: AuditObjectKind::Roa,
                            result: AuditObjectResult::Error,
                            detail: Some(format!(
                                "publication-point cached ROA local output parse failed: {err}"
                            )),
                        },
                    );
                }
            },
            VcirOutputType::Aspa => match parse_publication_point_cache_aspa_output(projected) {
                Ok(aspa) => {
                    aspa_ok.insert(projected.source_object_uri.clone());
                    output.aspas.push(aspa);
                    audit_by_uri.insert(
                        projected.source_object_uri.clone(),
                        ObjectAuditEntry {
                            rsync_uri: projected.source_object_uri.clone(),
                            sha256_hex: hex::encode(projected.source_object_hash),
                            kind: AuditObjectKind::Aspa,
                            result: AuditObjectResult::Ok,
                            detail: None,
                        },
                    );
                }
                Err(err) => {
                    warnings.push(
                        Warning::new(format!(
                            "publication-point cached ASPA local output parse failed: {err}"
                        ))
                        .with_context(&projected.source_object_uri),
                    );
                    audit_by_uri.insert(
                        projected.source_object_uri.clone(),
                        ObjectAuditEntry {
                            rsync_uri: projected.source_object_uri.clone(),
                            sha256_hex: hex::encode(projected.source_object_hash),
                            kind: AuditObjectKind::Aspa,
                            result: AuditObjectResult::Error,
                            detail: Some(format!(
                                "publication-point cached ASPA local output parse failed: {err}"
                            )),
                        },
                    );
                }
            },
            VcirOutputType::RouterKey => {
                match parse_publication_point_cache_router_key_output(projected) {
                    Ok(router_key) => {
                        output.router_keys.push(router_key);
                        audit_by_uri.insert(
                            projected.source_object_uri.clone(),
                            ObjectAuditEntry {
                                rsync_uri: projected.source_object_uri.clone(),
                                sha256_hex: hex::encode(projected.source_object_hash),
                                kind: AuditObjectKind::RouterCertificate,
                                result: AuditObjectResult::Ok,
                                detail: Some(
                                    "publication-point cached Router Key local output restored"
                                        .to_string(),
                                ),
                            },
                        );
                    }
                    Err(err) => {
                        warnings.push(
                            Warning::new(format!(
                                "publication-point cached Router Key local output parse failed: {err}"
                            ))
                            .with_context(&projected.source_object_uri),
                        );
                        audit_by_uri.insert(
                            projected.source_object_uri.clone(),
                            ObjectAuditEntry {
                                rsync_uri: projected.source_object_uri.clone(),
                                sha256_hex: hex::encode(projected.source_object_hash),
                                kind: AuditObjectKind::RouterCertificate,
                                result: AuditObjectResult::Error,
                                detail: Some(format!(
                                    "publication-point cached Router Key local output parse failed: {err}"
                                )),
                            },
                        );
                    }
                }
            }
        }
    }

    output.stats.roa_total = roa_total.len();
    output.stats.roa_ok = roa_ok.len();
    output.stats.aspa_total = aspa_total.len();
    output.stats.aspa_ok = aspa_ok.len();
    let mut audit: Vec<_> = audit_by_uri.into_values().collect();
    audit.sort_by(|left, right| left.rsync_uri.cmp(&right.rsync_uri));
    output.audit = audit;
    output
}

fn parse_vcir_vrp_output(local: &VcirLocalOutput) -> Result<Vrp, String> {
    match &local.payload {
        VcirLocalOutputPayload::Vrp {
            asn,
            afi,
            prefix_len,
            addr,
            max_length,
        } => Ok(Vrp {
            asn: *asn,
            prefix: crate::data_model::roa::IpPrefix {
                afi: *afi,
                prefix_len: *prefix_len,
                addr: *addr,
            },
            max_length: *max_length,
        }),
        _ => Err("VCIR local output payload is not VRP".to_string()),
    }
}

fn parse_vcir_aspa_output(local: &VcirLocalOutput) -> Result<AspaAttestation, String> {
    match &local.payload {
        VcirLocalOutputPayload::Aspa {
            customer_as_id,
            provider_as_ids,
        } => Ok(AspaAttestation {
            customer_as_id: *customer_as_id,
            provider_as_ids: provider_as_ids.clone(),
        }),
        _ => Err("VCIR local output payload is not ASPA".to_string()),
    }
}

fn parse_vcir_router_key_output(local: &VcirLocalOutput) -> Result<RouterKeyPayload, String> {
    match &local.payload {
        VcirLocalOutputPayload::RouterKey {
            as_id,
            ski,
            spki_der,
        } => Ok(RouterKeyPayload {
            as_id: *as_id,
            ski: ski.clone(),
            spki_der: spki_der.clone(),
            source_object_uri: local.source_object_uri.clone(),
            source_object_hash: local.source_object_hash_hex(),
            source_ee_cert_hash: local.source_ee_cert_hash_hex(),
            item_effective_until: local.item_effective_until.clone(),
        }),
        _ => Err("VCIR local output payload is not Router Key".to_string()),
    }
}

fn parse_publication_point_cache_vrp_output(
    projected: &PublicationPointCacheOutput,
) -> Result<Vrp, String> {
    match &projected.payload {
        VcirLocalOutputPayload::Vrp {
            asn,
            afi,
            prefix_len,
            addr,
            max_length,
        } => Ok(Vrp {
            asn: *asn,
            prefix: crate::data_model::roa::IpPrefix {
                afi: *afi,
                prefix_len: *prefix_len,
                addr: *addr,
            },
            max_length: *max_length,
        }),
        _ => Err("publication-point cache output payload is not VRP".to_string()),
    }
}

fn parse_publication_point_cache_aspa_output(
    projected: &PublicationPointCacheOutput,
) -> Result<AspaAttestation, String> {
    match &projected.payload {
        VcirLocalOutputPayload::Aspa {
            customer_as_id,
            provider_as_ids,
        } => Ok(AspaAttestation {
            customer_as_id: *customer_as_id,
            provider_as_ids: provider_as_ids.clone(),
        }),
        _ => Err("publication-point cache output payload is not ASPA".to_string()),
    }
}

fn parse_publication_point_cache_router_key_output(
    projected: &PublicationPointCacheOutput,
) -> Result<RouterKeyPayload, String> {
    match &projected.payload {
        VcirLocalOutputPayload::RouterKey {
            as_id,
            ski,
            spki_der,
        } => Ok(RouterKeyPayload {
            as_id: *as_id,
            ski: ski.clone(),
            spki_der: spki_der.clone(),
            source_object_uri: projected.source_object_uri.clone(),
            source_object_hash: hex::encode(projected.source_object_hash),
            source_ee_cert_hash: hex::encode(projected.source_ee_cert_hash),
            item_effective_until: projected.item_effective_until.clone(),
        }),
        _ => Err("publication-point cache output payload is not Router Key".to_string()),
    }
}

fn restore_children_from_vcir(
    _store: &RocksStore,
    ca: &CaInstanceHandle,
    vcir: &ValidatedCaInstanceResult,
    _warnings: &mut Vec<Warning>,
) -> (Vec<DiscoveredChildCaInstance>, Vec<ObjectAuditEntry>) {
    let mut children = Vec::new();
    let mut audits = Vec::new();
    for child in &vcir.child_entries {
        children.push(DiscoveredChildCaInstance {
            handle: CaInstanceHandle {
                depth: 0,
                tal_id: ca.tal_id.clone(),
                parent_manifest_rsync_uri: Some(ca.manifest_rsync_uri.clone()),
                ca_certificate: CaCertificateRef::repo_bytes(child.child_cert_hash.clone()),
                ca_certificate_rsync_uri: Some(child.child_cert_rsync_uri.clone()),
                effective_ip_resources: child.child_effective_ip_resources.clone(),
                effective_as_resources: child.child_effective_as_resources.clone(),
                rsync_base_uri: child.child_rsync_base_uri.clone(),
                manifest_rsync_uri: child.child_manifest_rsync_uri.clone(),
                publication_point_rsync_uri: child.child_publication_point_rsync_uri.clone(),
                rrdp_notification_uri: child.child_rrdp_notification_uri.clone(),
            },
            discovered_from: crate::audit::DiscoveredFrom {
                parent_manifest_rsync_uri: ca.manifest_rsync_uri.clone(),
                child_ca_certificate_rsync_uri: child.child_cert_rsync_uri.clone(),
                child_ca_certificate_sha256_hex: child.child_cert_hash.clone(),
            },
            child_entry_projection: Some(DiscoveredChildEntryProjection {
                child_ski: child.child_ski.clone(),
            }),
        });
        audits.push(ObjectAuditEntry {
            rsync_uri: child.child_cert_rsync_uri.clone(),
            sha256_hex: child.child_cert_hash.clone(),
            kind: AuditObjectKind::Certificate,
            result: AuditObjectResult::Ok,
            detail: Some("restored child CA instance from VCIR".to_string()),
        });
    }
    (children, audits)
}

fn restore_children_from_publication_point_cache(
    store: &RocksStore,
    ca: &CaInstanceHandle,
    projection: &PublicationPointCacheProjection,
    validation_time: time::OffsetDateTime,
    warnings: &mut Vec<Warning>,
    worker_count: usize,
    timing: Option<&TimingHandle>,
) -> (Vec<DiscoveredChildCaInstance>, Vec<ObjectAuditEntry>) {
    let worker_count = worker_count
        .clamp(1, PUBLICATION_POINT_CACHE_CHILD_RESTORE_MAX_WORKERS)
        .min(projection.children.len().max(1));
    let outcomes = if worker_count > 1
        && projection.children.len() >= PUBLICATION_POINT_CACHE_CHILD_RESTORE_PARALLEL_MIN_CHILDREN
    {
        if let Some(timing) = timing {
            timing.record_count(
                "publication_point_cache_restore_children_parallel_publication_points",
                1,
            );
            timing.record_count(
                "publication_point_cache_restore_children_parallel_children",
                projection.children.len() as u64,
            );
            timing.record_count(
                "publication_point_cache_restore_children_workers_total",
                worker_count as u64,
            );
        }
        restore_publication_point_cache_children_parallel(
            store,
            ca,
            &projection.children,
            validation_time,
            worker_count,
        )
    } else {
        if let Some(timing) = timing {
            timing.record_count(
                "publication_point_cache_restore_children_batch_publication_points",
                1,
            );
            timing.record_count(
                "publication_point_cache_restore_children_batch_children",
                projection.children.len() as u64,
            );
        }
        restore_publication_point_cache_children_chunk(
            store,
            ca,
            &projection.children,
            validation_time,
        )
    };
    collect_publication_point_cache_child_restore_outcomes(outcomes, warnings)
}

fn restore_publication_point_cache_children_parallel(
    _store: &RocksStore,
    ca: &CaInstanceHandle,
    children: &[PublicationPointCacheChild],
    validation_time: time::OffsetDateTime,
    worker_count: usize,
) -> Vec<PublicationPointCacheChildRestoreOutcome> {
    let chunk_size = children.len().div_ceil(worker_count).max(1);
    let mut chunk_results = Vec::new();
    std::thread::scope(|scope| {
        let mut handles = Vec::new();
        for chunk in children.chunks(chunk_size) {
            handles.push(scope.spawn(move || {
                restore_publication_point_cache_children_chunk(_store, ca, chunk, validation_time)
            }));
        }
        for handle in handles {
            chunk_results.extend(
                handle
                    .join()
                    .expect("publication-point cache child restore worker panicked"),
            );
        }
    });
    chunk_results
}

fn restore_publication_point_cache_children_chunk(
    _store: &RocksStore,
    ca: &CaInstanceHandle,
    children: &[PublicationPointCacheChild],
    validation_time: time::OffsetDateTime,
) -> Vec<PublicationPointCacheChildRestoreOutcome> {
    let mut outcomes = vec![None; children.len()];
    for (position, child) in children.iter().enumerate() {
        let effective_not_before =
            match parse_snapshot_time_value(&child.child_effective_not_before) {
                Ok(value) => value,
                Err(e) => {
                    outcomes[position] = Some(PublicationPointCacheChildRestoreOutcome {
                        child: None,
                        audit: None,
                        warning: Some(
                            Warning::new(format!(
                                "publication-point cache child has invalid effective notBefore: {e}"
                            ))
                            .with_context(&child.child_cert_rsync_uri),
                        ),
                    });
                    continue;
                }
            };
        let effective_until = match parse_snapshot_time_value(&child.child_effective_until) {
            Ok(value) => value,
            Err(e) => {
                outcomes[position] = Some(PublicationPointCacheChildRestoreOutcome {
                    child: None,
                    audit: None,
                    warning: Some(
                        Warning::new(format!(
                            "publication-point cache child has invalid effective until: {e}"
                        ))
                        .with_context(&child.child_cert_rsync_uri),
                    ),
                });
                continue;
            }
        };
        if validation_time < effective_not_before || validation_time > effective_until {
            outcomes[position] = Some(PublicationPointCacheChildRestoreOutcome {
                child: None,
                warning: None,
                audit: Some(publication_point_cache_child_audit(
                    child,
                    AuditObjectResult::Skipped,
                    Some("skipped: publication-point cache child expired".to_string()),
                )),
            });
            continue;
        }
        outcomes[position] = Some(PublicationPointCacheChildRestoreOutcome {
            child: Some(publication_point_cache_discovered_child(ca, child)),
            warning: None,
            audit: Some(publication_point_cache_child_audit(
                child,
                AuditObjectResult::Ok,
                Some("restored child CA instance from publication-point cache".to_string()),
            )),
        });
    }

    outcomes
        .into_iter()
        .flatten()
        .collect::<Vec<PublicationPointCacheChildRestoreOutcome>>()
}

fn collect_publication_point_cache_child_restore_outcomes(
    outcomes: Vec<PublicationPointCacheChildRestoreOutcome>,
    warnings: &mut Vec<Warning>,
) -> (Vec<DiscoveredChildCaInstance>, Vec<ObjectAuditEntry>) {
    let mut children = Vec::new();
    let mut audits = Vec::new();
    for outcome in outcomes {
        if let Some(warning) = outcome.warning {
            warnings.push(warning);
        }
        if let Some(audit) = outcome.audit {
            audits.push(audit);
        }
        if let Some(child) = outcome.child {
            children.push(child);
        }
    }
    (children, audits)
}

#[derive(Clone)]
struct PublicationPointCacheChildRestoreOutcome {
    child: Option<DiscoveredChildCaInstance>,
    audit: Option<ObjectAuditEntry>,
    warning: Option<Warning>,
}

fn publication_point_cache_discovered_child(
    ca: &CaInstanceHandle,
    child: &PublicationPointCacheChild,
) -> DiscoveredChildCaInstance {
    DiscoveredChildCaInstance {
        handle: CaInstanceHandle {
            depth: 0,
            tal_id: ca.tal_id.clone(),
            parent_manifest_rsync_uri: Some(ca.manifest_rsync_uri.clone()),
            ca_certificate: CaCertificateRef::repo_bytes(child.child_cert_hash.clone()),
            ca_certificate_rsync_uri: Some(child.child_cert_rsync_uri.clone()),
            effective_ip_resources: child.child_effective_ip_resources.clone(),
            effective_as_resources: child.child_effective_as_resources.clone(),
            rsync_base_uri: child.child_rsync_base_uri.clone(),
            manifest_rsync_uri: child.child_manifest_rsync_uri.clone(),
            publication_point_rsync_uri: child.child_publication_point_rsync_uri.clone(),
            rrdp_notification_uri: child.child_rrdp_notification_uri.clone(),
        },
        discovered_from: crate::audit::DiscoveredFrom {
            parent_manifest_rsync_uri: ca.manifest_rsync_uri.clone(),
            child_ca_certificate_rsync_uri: child.child_cert_rsync_uri.clone(),
            child_ca_certificate_sha256_hex: child.child_cert_hash.clone(),
        },
        child_entry_projection: Some(DiscoveredChildEntryProjection {
            child_ski: child.child_ski.clone(),
        }),
    }
}

fn publication_point_cache_child_audit(
    child: &PublicationPointCacheChild,
    result: AuditObjectResult,
    detail: Option<String>,
) -> ObjectAuditEntry {
    ObjectAuditEntry {
        rsync_uri: child.child_cert_rsync_uri.clone(),
        sha256_hex: child.child_cert_hash.clone(),
        kind: AuditObjectKind::Certificate,
        result,
        detail,
    }
}

fn persist_vcir_for_fresh_result_with_timing(
    store: &RocksStore,
    policy: &Policy,
    ca: &CaInstanceHandle,
    pack: &PublicationPointSnapshot,
    objects: &mut crate::validation::objects::ObjectsOutput,
    warnings: &[Warning],
    child_audits: &[ObjectAuditEntry],
    discovered_children: &[DiscoveredChildCaInstance],
    validation_time: time::OffsetDateTime,
    write_publication_point_cache_projection: bool,
) -> Result<PersistVcirTimingBreakdown, String> {
    let mut timing = PersistVcirTimingBreakdown::default();

    if objects.stats.publication_point_dropped {
        return Ok(timing);
    }

    let embedded_store_started = std::time::Instant::now();
    persist_vcir_non_repository_evidence(store, ca)
        .map_err(|e| format!("store VCIR audit evidence failed: {e}"))?;
    timing.embedded_store_ms = embedded_store_started.elapsed().as_millis() as u64;

    let build_vcir_started = std::time::Instant::now();
    let (vcir, build_vcir_timing) = build_vcir_from_fresh_result_with_timing(
        store,
        ca,
        pack,
        objects,
        warnings,
        child_audits,
        discovered_children,
        validation_time,
    )?;
    timing.build_vcir_ms = build_vcir_started.elapsed().as_millis() as u64;
    timing.build_vcir = build_vcir_timing;

    let replace_vcir_started = std::time::Instant::now();
    let future_not_before_cache_guard = write_publication_point_cache_projection
        && publication_point_cache_has_future_not_before_risk(
            pack,
            objects,
            child_audits,
            validation_time,
            policy,
        );
    timing.publication_point_cache_future_notbefore_guarded = future_not_before_cache_guard;
    let publication_point_cache_projection =
        if write_publication_point_cache_projection && !future_not_before_cache_guard {
            Some(build_publication_point_cache_projection_from_fresh(
                policy, ca, pack, &vcir,
            )?)
        } else {
            None
        };
    let publication_point_cache_projection_action = if !write_publication_point_cache_projection {
        PublicationPointCacheProjectionWriteAction::Keep
    } else if future_not_before_cache_guard {
        PublicationPointCacheProjectionWriteAction::Delete {
            manifest_rsync_uri: &vcir.manifest_rsync_uri,
        }
    } else {
        PublicationPointCacheProjectionWriteAction::Write(
            publication_point_cache_projection
                .as_ref()
                .expect("publication point projection must exist when guard is not active"),
        )
    };
    let failed_fetch_reuse_identity = failed_fetch_reuse_identity_for_fresh_result(
        ca,
        policy,
        validation_time,
        vcir.instance_gate.instance_effective_until.clone(),
    )?;
    let replace_timing = store
        .replace_vcir_manifest_replay_meta_and_projection_action_with_failed_fetch_reuse_identity(
            &vcir,
            Some(&RoaCacheProjectionContext {
                ca_validation_context_digest: ca_validation_context_digest_for_ca(ca),
                policy_fingerprint: publication_point_cache_policy_fingerprint(policy),
                object_meta: objects.roa_cache_object_meta.clone(),
            }),
            publication_point_cache_projection_action,
            Some(&failed_fetch_reuse_identity),
        )
        .map_err(|e| format!("store VCIR and manifest replay meta failed: {e}"))?;
    timing.replace_vcir_ms = replace_vcir_started.elapsed().as_millis() as u64;
    timing.replace_vcir = replace_timing;

    Ok(timing)
}

fn build_publication_point_cache_projection_from_fresh(
    policy: &Policy,
    ca: &CaInstanceHandle,
    pack: &PublicationPointSnapshot,
    vcir: &ValidatedCaInstanceResult,
) -> Result<PublicationPointCacheProjection, String> {
    let ca_cert_sha256 = ca
        .ca_certificate_sha256_32()
        .ok_or_else(|| "current CA certificate hash unavailable".to_string())?;
    let manifest_sha256 = sha256_digest_32(&pack.manifest_bytes);
    PublicationPointCacheProjection::from_vcir_with_context(
        vcir,
        pack.publication_point_rsync_uri.clone(),
        ca.ca_certificate_rsync_uri.clone(),
        ca_cert_sha256,
        manifest_sha256,
        ta_context_digest_for_ca(ca),
        ca_validation_context_digest_for_ca(ca),
        publication_point_cache_policy_fingerprint(policy),
    )
    .map_err(|e| e.to_string())
}

fn publication_point_cache_has_future_not_before_risk(
    pack: &PublicationPointSnapshot,
    objects: &crate::validation::objects::ObjectsOutput,
    child_audits: &[ObjectAuditEntry],
    validation_time: time::OffsetDateTime,
    policy: &Policy,
) -> bool {
    let mut files_by_uri: HashMap<&str, &PackFile> = HashMap::new();
    for file in &pack.files {
        files_by_uri.insert(file.rsync_uri.as_str(), file);
    }

    objects
        .audit
        .iter()
        .chain(child_audits.iter())
        .any(|entry| {
            audit_entry_has_certificate_time_error(entry)
                && files_by_uri
                    .get(entry.rsync_uri.as_str())
                    .map(|file| {
                        audit_entry_has_future_not_before(entry, file, validation_time, policy)
                    })
                    .unwrap_or(true)
        })
}

fn audit_entry_has_certificate_time_error(entry: &ObjectAuditEntry) -> bool {
    entry.result == AuditObjectResult::Error
        && entry
            .detail
            .as_deref()
            .is_some_and(|detail| detail.contains("certificate not valid at validation_time"))
}

fn audit_entry_has_future_not_before(
    entry: &ObjectAuditEntry,
    file: &PackFile,
    validation_time: time::OffsetDateTime,
    policy: &Policy,
) -> bool {
    match entry.kind {
        AuditObjectKind::Roa => signed_object_ee_not_before(file, policy, SignedObjectKind::Roa)
            .map(|not_before| validation_time < not_before)
            .unwrap_or(true),
        AuditObjectKind::Aspa => signed_object_ee_not_before(file, policy, SignedObjectKind::Aspa)
            .map(|not_before| validation_time < not_before)
            .unwrap_or(true),
        AuditObjectKind::Certificate | AuditObjectKind::RouterCertificate => file
            .bytes()
            .ok()
            .and_then(|bytes| ResourceCertificate::decode_der(bytes).ok())
            .map(|cert| validation_time < cert.tbs.validity_not_before)
            .unwrap_or(true),
        _ => true,
    }
}

#[derive(Clone, Copy)]
enum SignedObjectKind {
    Roa,
    Aspa,
}

fn signed_object_ee_not_before(
    file: &PackFile,
    policy: &Policy,
    kind: SignedObjectKind,
) -> Option<time::OffsetDateTime> {
    let bytes = file.bytes().ok()?;
    match kind {
        SignedObjectKind::Roa => {
            let object = RoaObject::decode_der_with_strict_options(
                bytes,
                policy.strict.cms_der,
                policy.strict.name,
            )
            .ok()?;
            Some(
                object.signed_object.signed_data.certificates[0]
                    .resource_cert
                    .tbs
                    .validity_not_before,
            )
        }
        SignedObjectKind::Aspa => {
            let object = AspaObject::decode_der_with_strict_options(
                bytes,
                policy.strict.cms_der,
                policy.strict.name,
            )
            .ok()?;
            Some(
                object.signed_object.signed_data.certificates[0]
                    .resource_cert
                    .tbs
                    .validity_not_before,
            )
        }
    }
}

fn build_vcir_from_fresh_result_with_timing(
    store: &RocksStore,
    ca: &CaInstanceHandle,
    pack: &PublicationPointSnapshot,
    objects: &mut crate::validation::objects::ObjectsOutput,
    warnings: &[Warning],
    child_audits: &[ObjectAuditEntry],
    discovered_children: &[DiscoveredChildCaInstance],
    validation_time: time::OffsetDateTime,
) -> Result<(ValidatedCaInstanceResult, BuildVcirTimingBreakdown), String> {
    let mut timing = BuildVcirTimingBreakdown::default();

    let select_crl_started = std::time::Instant::now();
    let current_crl = select_manifest_current_crl_from_snapshot(pack)?;
    timing.select_crl_ms = select_crl_started.elapsed().as_millis() as u64;

    let current_ca_decode_started = std::time::Instant::now();
    let ca_der = ca.ca_certificate_der(store)?;
    let ca_cert = ResourceCertificate::decode_der(ca_der.as_ref())
        .map_err(|e| format!("decode current CA certificate failed: {e}"))?;
    timing.current_ca_decode_ms = current_ca_decode_started.elapsed().as_millis() as u64;

    let local_outputs_started = std::time::Instant::now();
    let local_outputs = take_or_build_vcir_local_outputs(ca, pack, objects)?;
    timing.local_outputs_ms = local_outputs_started.elapsed().as_millis() as u64;

    let child_entries_started = std::time::Instant::now();
    let child_entries = build_vcir_child_entries(store, discovered_children, validation_time)?;
    timing.child_entries_ms = child_entries_started.elapsed().as_millis() as u64;

    let related_artifacts_started = std::time::Instant::now();
    let related_artifacts = build_vcir_related_artifacts(
        store,
        ca,
        pack,
        current_crl.file.rsync_uri.as_str(),
        objects,
        child_audits,
    );
    timing.related_artifacts_ms = related_artifacts_started.elapsed().as_millis() as u64;
    let ccr_manifest_projection =
        build_vcir_ccr_manifest_projection_from_fresh(ca, pack, &child_entries)?;
    let local_vrp_count = local_outputs
        .iter()
        .filter(|output| output.output_type == VcirOutputType::Vrp)
        .count() as u32;
    let local_aspa_count = local_outputs
        .iter()
        .filter(|output| output.output_type == VcirOutputType::Aspa)
        .count() as u32;
    let local_router_key_count = local_outputs
        .iter()
        .filter(|output| output.output_type == VcirOutputType::RouterKey)
        .count() as u32;
    let accepted_object_count = related_artifacts
        .iter()
        .filter(|artifact| artifact.validation_status == VcirArtifactValidationStatus::Accepted)
        .count() as u32;
    let rejected_object_count = related_artifacts
        .iter()
        .filter(|artifact| artifact.validation_status == VcirArtifactValidationStatus::Rejected)
        .count() as u32;
    let ca_ski = hex::encode(
        ca_cert
            .tbs
            .extensions
            .subject_key_identifier
            .as_ref()
            .ok_or_else(|| "current CA certificate missing SubjectKeyIdentifier".to_string())?,
    );
    let issuer_ski = hex::encode(
        ca_cert
            .tbs
            .extensions
            .authority_key_identifier
            .as_ref()
            .or(ca_cert.tbs.extensions.subject_key_identifier.as_ref())
            .ok_or_else(|| "current CA certificate missing AuthorityKeyIdentifier".to_string())?,
    );

    let struct_build_started = std::time::Instant::now();
    let vcir = ValidatedCaInstanceResult {
        manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
        parent_manifest_rsync_uri: ca.parent_manifest_rsync_uri.clone(),
        tal_id: ca.tal_id.clone(),
        ca_subject_name: ca_cert.tbs.subject_name.to_string(),
        ca_ski,
        issuer_ski,
        last_successful_validation_time: PackTime::from_utc_offset_datetime(validation_time),
        current_manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
        current_crl_rsync_uri: current_crl.file.rsync_uri.clone(),
        validated_manifest_meta: crate::storage::ValidatedManifestMeta {
            validated_manifest_number: pack.manifest_number_be.clone(),
            validated_manifest_this_update: pack.this_update.clone(),
            validated_manifest_next_update: pack.next_update.clone(),
        },
        ccr_manifest_projection,
        instance_gate: VcirInstanceGate {
            manifest_next_update: pack.next_update.clone(),
            current_crl_next_update: PackTime::from_utc_offset_datetime(
                current_crl.crl.next_update.utc,
            ),
            self_ca_not_after: PackTime::from_utc_offset_datetime(ca_cert.tbs.validity_not_after),
            instance_effective_until: PackTime::from_utc_offset_datetime(
                pack.next_update
                    .parse()
                    .map_err(|e| format!("parse snapshot next_update failed: {e}"))?
                    .min(current_crl.crl.next_update.utc)
                    .min(ca_cert.tbs.validity_not_after),
            ),
        },
        child_entries,
        local_outputs,
        related_artifacts,
        summary: VcirSummary {
            local_vrp_count,
            local_aspa_count,
            local_router_key_count,
            child_count: discovered_children.len() as u32,
            accepted_object_count,
            rejected_object_count,
        },
        audit_summary: VcirAuditSummary {
            failed_fetch_eligible: true,
            last_failed_fetch_reason: None,
            warning_count: (warnings.len() + objects.warnings.len()) as u32,
            audit_flags: Vec::new(),
        },
    };
    vcir.validate_internal().map_err(|e| e.to_string())?;
    timing.struct_build_ms = struct_build_started.elapsed().as_millis() as u64;
    Ok((vcir, timing))
}

fn take_or_build_vcir_local_outputs(
    ca: &CaInstanceHandle,
    pack: &PublicationPointSnapshot,
    objects: &mut crate::validation::objects::ObjectsOutput,
) -> Result<Vec<VcirLocalOutput>, String> {
    let mut cached_outputs = std::mem::take(&mut objects.local_outputs_cache);
    if cached_outputs.is_empty() {
        return build_vcir_local_outputs(ca, pack, objects);
    }

    let covered_roa_uris: HashSet<String> = cached_outputs
        .iter()
        .filter(|output| output.source_object_type == VcirSourceObjectType::Roa)
        .map(|output| output.source_object_uri.clone())
        .collect();
    let covered_aspa_uris: HashSet<String> = cached_outputs
        .iter()
        .filter(|output| output.source_object_type == VcirSourceObjectType::Aspa)
        .map(|output| output.source_object_uri.clone())
        .collect();
    cached_outputs.extend(build_vcir_local_outputs_excluding(
        ca,
        pack,
        objects,
        &covered_roa_uris,
        &covered_aspa_uris,
    )?);
    Ok(cached_outputs)
}

fn build_vcir_ccr_manifest_projection_from_fresh(
    ca: &CaInstanceHandle,
    pack: &PublicationPointSnapshot,
    child_entries: &[VcirChildEntry],
) -> Result<VcirCcrManifestProjection, String> {
    let manifest = ManifestObject::decode_der(&pack.manifest_bytes)
        .map_err(|e| format!("decode manifest for VCIR CCR projection failed: {e}"))?;
    let ee = &manifest.signed_object.signed_data.certificates[0].resource_cert;
    let manifest_ee_aki = ee
        .tbs
        .extensions
        .authority_key_identifier
        .clone()
        .ok_or_else(|| "manifest EE certificate missing AuthorityKeyIdentifier".to_string())?;
    let manifest_sia_locations_der = match ee
        .tbs
        .extensions
        .subject_info_access
        .as_ref()
        .ok_or_else(|| "manifest EE certificate missing Subject Information Access".to_string())?
    {
        SubjectInfoAccess::Ee(ee_sia) => vec![select_manifest_signed_object_location(
            &ca.manifest_rsync_uri,
            &ee_sia.access_descriptions,
        )?],
        SubjectInfoAccess::Ca(_) => {
            return Err(
                "manifest EE certificate Subject Information Access has CA variant".to_string(),
            );
        }
    };

    let mut subordinate_skis = child_entries
        .iter()
        .map(|child| {
            hex::decode(&child.child_ski)
                .map_err(|e| format!("decode child_ski for VCIR CCR projection failed: {e}"))
        })
        .collect::<Result<Vec<_>, _>>()?;
    subordinate_skis.sort();
    subordinate_skis.dedup();

    Ok(VcirCcrManifestProjection {
        manifest_rsync_uri: ca.manifest_rsync_uri.clone(),
        manifest_sha256: sha2::Sha256::digest(&pack.manifest_bytes).to_vec(),
        manifest_size: pack.manifest_bytes.len() as u64,
        manifest_ee_aki,
        manifest_number_be: pack.manifest_number_be.clone(),
        manifest_this_update: pack.this_update.clone(),
        manifest_sia_locations_der,
        subordinate_skis,
    })
}

struct CurrentCrlRef<'a> {
    file: &'a PackFile,
    crl: RpkixCrl,
}

fn select_manifest_current_crl_from_snapshot(
    pack: &PublicationPointSnapshot,
) -> Result<CurrentCrlRef<'_>, String> {
    let manifest = ManifestObject::decode_der(&pack.manifest_bytes)
        .map_err(|e| format!("decode snapshot manifest for VCIR failed: {e}"))?;
    let ee = &manifest.signed_object.signed_data.certificates[0].resource_cert;
    let crldp_uris = ee
        .tbs
        .extensions
        .crl_distribution_points_uris
        .as_ref()
        .ok_or_else(|| "manifest EE certificate missing CRLDistributionPoints".to_string())?;
    for uri in crldp_uris {
        if let Some(file) = pack
            .files
            .iter()
            .find(|candidate| candidate.rsync_uri == *uri)
        {
            let crl = RpkixCrl::decode_der(
                file.bytes()
                    .map_err(|e| format!("load current CRL bytes for VCIR failed: {e}"))?,
            )
            .map_err(|e| format!("decode current CRL for VCIR failed: {e}"))?;
            return Ok(CurrentCrlRef { file, crl });
        }
    }
    Err(format!(
        "manifest EE certificate CRLDistributionPoints not found in pack: {}",
        crldp_uris.join(", ")
    ))
}

fn build_vcir_local_outputs(
    _ca: &CaInstanceHandle,
    pack: &PublicationPointSnapshot,
    objects: &crate::validation::objects::ObjectsOutput,
) -> Result<Vec<VcirLocalOutput>, String> {
    build_vcir_local_outputs_excluding(_ca, pack, objects, &HashSet::new(), &HashSet::new())
}

fn build_vcir_local_outputs_excluding(
    _ca: &CaInstanceHandle,
    pack: &PublicationPointSnapshot,
    objects: &crate::validation::objects::ObjectsOutput,
    covered_roa_uris: &HashSet<String>,
    covered_aspa_uris: &HashSet<String>,
) -> Result<Vec<VcirLocalOutput>, String> {
    let accepted_roa_uris: HashSet<&str> = objects
        .audit
        .iter()
        .filter(|entry| entry.kind == AuditObjectKind::Roa && entry.result == AuditObjectResult::Ok)
        .map(|entry| entry.rsync_uri.as_str())
        .collect();
    let accepted_aspa_uris: HashSet<&str> = objects
        .audit
        .iter()
        .filter(|entry| {
            entry.kind == AuditObjectKind::Aspa && entry.result == AuditObjectResult::Ok
        })
        .map(|entry| entry.rsync_uri.as_str())
        .collect();

    let mut out = Vec::new();
    for file in &pack.files {
        let source_object_hash = sha256_hex_from_32(&file.sha256);
        if accepted_roa_uris.contains(file.rsync_uri.as_str())
            && !covered_roa_uris.contains(file.rsync_uri.as_str())
        {
            let roa = RoaObject::decode_der(
                file.bytes()
                    .map_err(|e| format!("load accepted ROA bytes for VCIR failed: {e}"))?,
            )
            .map_err(|e| format!("decode accepted ROA for VCIR failed: {e}"))?;
            let ee = &roa.signed_object.signed_data.certificates[0];
            let source_ee_cert_hash = sha256_hex(ee.raw_der.as_slice());
            let item_effective_until =
                PackTime::from_utc_offset_datetime(ee.resource_cert.tbs.validity_not_after);
            for vrp in roa_to_vrps_for_vcir(&roa) {
                let prefix = vrp_prefix_to_string(&vrp);
                let rule_hash = sha256_hex(
                    format!(
                        "roa-rule:{}:{}:{}:{}",
                        source_object_hash, vrp.asn, prefix, vrp.max_length
                    )
                    .as_bytes(),
                );
                out.push(VcirLocalOutput {
                    output_type: VcirOutputType::Vrp,
                    item_effective_until: item_effective_until.clone(),
                    source_object_uri: file.rsync_uri.clone(),
                    source_object_type: VcirSourceObjectType::Roa,
                    source_object_hash: file.sha256,
                    source_ee_cert_hash: sha256_hex_to_32(&source_ee_cert_hash),
                    payload: VcirLocalOutputPayload::Vrp {
                        asn: vrp.asn,
                        afi: vrp.prefix.afi,
                        prefix_len: vrp.prefix.prefix_len,
                        addr: vrp.prefix.addr,
                        max_length: vrp.max_length,
                    },
                    rule_hash: sha256_hex_to_32(&rule_hash),
                });
            }
        } else if accepted_aspa_uris.contains(file.rsync_uri.as_str())
            && !covered_aspa_uris.contains(file.rsync_uri.as_str())
        {
            let aspa = AspaObject::decode_der(
                file.bytes()
                    .map_err(|e| format!("load accepted ASPA bytes for VCIR failed: {e}"))?,
            )
            .map_err(|e| format!("decode accepted ASPA for VCIR failed: {e}"))?;
            let ee = &aspa.signed_object.signed_data.certificates[0];
            let source_ee_cert_hash = sha256_hex(ee.raw_der.as_slice());
            let item_effective_until =
                PackTime::from_utc_offset_datetime(ee.resource_cert.tbs.validity_not_after);
            let providers = aspa
                .aspa
                .provider_as_ids
                .iter()
                .map(u32::to_string)
                .collect::<Vec<_>>()
                .join(",");
            let rule_hash = sha256_hex(
                format!(
                    "aspa-rule:{}:{}:{}",
                    source_object_hash, aspa.aspa.customer_as_id, providers
                )
                .as_bytes(),
            );
            out.push(VcirLocalOutput {
                output_type: VcirOutputType::Aspa,
                item_effective_until,
                source_object_uri: file.rsync_uri.clone(),
                source_object_type: VcirSourceObjectType::Aspa,
                source_object_hash: file.sha256,
                source_ee_cert_hash: sha256_hex_to_32(&source_ee_cert_hash),
                payload: VcirLocalOutputPayload::Aspa {
                    customer_as_id: aspa.aspa.customer_as_id,
                    provider_as_ids: aspa.aspa.provider_as_ids.clone(),
                },
                rule_hash: sha256_hex_to_32(&rule_hash),
            });
        }
    }
    Ok(out)
}

pub(crate) fn build_router_key_local_outputs(
    _ca: &CaInstanceHandle,
    router_keys: &[RouterKeyPayload],
) -> Vec<VcirLocalOutput> {
    router_keys
        .iter()
        .map(|router_key| {
            let ski_hex = hex::encode(&router_key.ski);
            let spki_der_base64 =
                base64::engine::general_purpose::STANDARD.encode(&router_key.spki_der);
            let rule_hash = sha256_hex(
                format!(
                    "router-key-rule:{}:{}:{}:{}",
                    router_key.source_object_hash, router_key.as_id, ski_hex, spki_der_base64
                )
                .as_bytes(),
            );
            VcirLocalOutput {
                output_type: VcirOutputType::RouterKey,
                item_effective_until: router_key.item_effective_until.clone(),
                source_object_uri: router_key.source_object_uri.clone(),
                source_object_type: VcirSourceObjectType::RouterKey,
                source_object_hash: sha256_hex_to_32(&router_key.source_object_hash),
                source_ee_cert_hash: sha256_hex_to_32(&router_key.source_ee_cert_hash),
                payload: VcirLocalOutputPayload::RouterKey {
                    as_id: router_key.as_id,
                    ski: router_key.ski.clone(),
                    spki_der: router_key.spki_der.clone(),
                },
                rule_hash: sha256_hex_to_32(&rule_hash),
            }
        })
        .collect()
}

fn build_vcir_child_entries(
    store: &RocksStore,
    discovered_children: &[DiscoveredChildCaInstance],
    validation_time: time::OffsetDateTime,
) -> Result<Vec<VcirChildEntry>, String> {
    let mut out = Vec::with_capacity(discovered_children.len());
    for child in discovered_children {
        let child_ski = match child.child_entry_projection.as_ref() {
            Some(projection) => projection.child_ski.clone(),
            None => {
                let child_der = child.handle.ca_certificate_der(store)?;
                let child_cert = ResourceCertificate::decode_der(child_der.as_ref())
                    .map_err(|e| format!("decode child certificate for VCIR failed: {e}"))?;
                let child_ski = child_cert
                    .tbs
                    .extensions
                    .subject_key_identifier
                    .as_ref()
                    .ok_or_else(|| "child certificate missing SubjectKeyIdentifier".to_string())?;
                hex::encode(child_ski)
            }
        };
        out.push(VcirChildEntry {
            child_manifest_rsync_uri: child.handle.manifest_rsync_uri.clone(),
            child_cert_rsync_uri: child.discovered_from.child_ca_certificate_rsync_uri.clone(),
            child_cert_hash: child
                .discovered_from
                .child_ca_certificate_sha256_hex
                .clone(),
            child_ski,
            child_rsync_base_uri: child.handle.rsync_base_uri.clone(),
            child_publication_point_rsync_uri: child.handle.publication_point_rsync_uri.clone(),
            child_rrdp_notification_uri: child.handle.rrdp_notification_uri.clone(),
            child_effective_ip_resources: child.handle.effective_ip_resources.clone(),
            child_effective_as_resources: child.handle.effective_as_resources.clone(),
            accepted_at_validation_time: PackTime::from_utc_offset_datetime(validation_time),
        });
    }
    Ok(out)
}

fn persist_vcir_non_repository_evidence(
    store: &RocksStore,
    ca: &CaInstanceHandle,
) -> Result<(), String> {
    let ca_der = ca.ca_certificate_der(store)?;
    let current_ca_hash = sha256_hex(ca_der.as_ref());
    let mut current_ca_entry = RawByHashEntry::from_bytes(current_ca_hash, ca_der.to_vec());
    if let Some(uri) = ca.ca_certificate_rsync_uri.as_ref() {
        current_ca_entry.origin_uris.push(uri.clone());
    }
    current_ca_entry.object_type = Some("cer".to_string());
    current_ca_entry.encoding = Some("der".to_string());
    upsert_raw_by_hash_entry(store, current_ca_entry)?;
    Ok(())
}

fn upsert_raw_by_hash_entry(store: &RocksStore, entry: RawByHashEntry) -> Result<(), String> {
    match store.get_raw_by_hash_entry(&entry.sha256_hex) {
        Ok(Some(existing)) => {
            if existing.bytes != entry.bytes {
                return Err(format!(
                    "raw_by_hash collision for sha256 {} while storing VCIR audit evidence",
                    entry.sha256_hex
                ));
            }
            let mut merged = existing;
            let mut changed = false;
            for uri in entry.origin_uris {
                if !merged
                    .origin_uris
                    .iter()
                    .any(|existing_uri| existing_uri == &uri)
                {
                    merged.origin_uris.push(uri);
                    changed = true;
                }
            }
            if merged.object_type.is_none() && entry.object_type.is_some() {
                merged.object_type = entry.object_type;
                changed = true;
            }
            if merged.encoding.is_none() && entry.encoding.is_some() {
                merged.encoding = entry.encoding;
                changed = true;
            }
            if changed {
                store
                    .put_raw_by_hash_entry(&merged)
                    .map_err(|e| format!("update raw_by_hash entry failed: {e}"))?;
            }
            Ok(())
        }
        Ok(None) => store
            .put_raw_by_hash_entry(&entry)
            .map_err(|e| format!("store raw_by_hash entry failed: {e}")),
        Err(e) => Err(format!("load raw_by_hash entry failed: {e}")),
    }
}

fn build_vcir_related_artifacts(
    store: &RocksStore,
    ca: &CaInstanceHandle,
    pack: &PublicationPointSnapshot,
    current_crl_rsync_uri: &str,
    objects: &crate::validation::objects::ObjectsOutput,
    child_audits: &[ObjectAuditEntry],
) -> Vec<VcirRelatedArtifact> {
    let mut audit_by_uri: HashMap<&str, &ObjectAuditEntry> = HashMap::new();
    for entry in child_audits.iter().chain(objects.audit.iter()) {
        audit_by_uri.insert(entry.rsync_uri.as_str(), entry);
    }

    let mut artifacts = Vec::with_capacity(pack.files.len() + 2);
    artifacts.push(VcirRelatedArtifact {
        artifact_role: VcirArtifactRole::Manifest,
        artifact_kind: VcirArtifactKind::Mft,
        uri: Some(pack.manifest_rsync_uri.clone()),
        sha256: sha256_hex(&pack.manifest_bytes),
        object_type: Some("mft".to_string()),
        validation_status: VcirArtifactValidationStatus::Accepted,
        reject_reason: None,
    });
    artifacts.push(VcirRelatedArtifact {
        artifact_role: if ca.parent_manifest_rsync_uri.is_none() {
            VcirArtifactRole::TrustAnchorCert
        } else {
            VcirArtifactRole::IssuerCert
        },
        artifact_kind: VcirArtifactKind::Cer,
        uri: ca.ca_certificate_rsync_uri.clone(),
        sha256: ca
            .ca_certificate_sha256_hex()
            .map(str::to_string)
            .unwrap_or_else(|| {
                ca.ca_certificate_der(store)
                    .map(|bytes| sha256_hex(bytes.as_ref()))
                    .unwrap_or_default()
            }),
        object_type: Some("cer".to_string()),
        validation_status: VcirArtifactValidationStatus::Accepted,
        reject_reason: None,
    });

    for file in &pack.files {
        let audit_entry = audit_by_uri.get(file.rsync_uri.as_str()).copied();
        let result = audit_entry
            .map(|entry| entry.result.clone())
            .unwrap_or(AuditObjectResult::Ok);
        let validation_status = audit_result_to_vcir_status(&result);
        let reject_reason = if validation_status == VcirArtifactValidationStatus::Rejected {
            audit_entry.and_then(|entry| entry.detail.clone())
        } else {
            None
        };
        let (artifact_role, artifact_kind) = artifact_role_and_kind(file, current_crl_rsync_uri);
        artifacts.push(VcirRelatedArtifact {
            artifact_role,
            artifact_kind,
            uri: Some(file.rsync_uri.clone()),
            sha256: sha256_hex_from_32(&file.sha256),
            object_type: object_type_from_uri(file.rsync_uri.as_str()),
            validation_status,
            reject_reason,
        });
    }

    artifacts
}

fn artifact_role_and_kind(
    file: &PackFile,
    current_crl_rsync_uri: &str,
) -> (VcirArtifactRole, VcirArtifactKind) {
    if file.rsync_uri == current_crl_rsync_uri {
        (VcirArtifactRole::CurrentCrl, VcirArtifactKind::Crl)
    } else if file.rsync_uri.ends_with(".cer") {
        (VcirArtifactRole::ChildCaCert, VcirArtifactKind::Cer)
    } else if file.rsync_uri.ends_with(".roa") {
        (VcirArtifactRole::SignedObject, VcirArtifactKind::Roa)
    } else if file.rsync_uri.ends_with(".asa") {
        (VcirArtifactRole::SignedObject, VcirArtifactKind::Aspa)
    } else if file.rsync_uri.ends_with(".gbr") {
        (VcirArtifactRole::SignedObject, VcirArtifactKind::Gbr)
    } else if file.rsync_uri.ends_with(".crl") {
        (VcirArtifactRole::Other, VcirArtifactKind::Crl)
    } else if file.rsync_uri.ends_with(".mft") {
        (VcirArtifactRole::Manifest, VcirArtifactKind::Mft)
    } else {
        (VcirArtifactRole::Other, VcirArtifactKind::Other)
    }
}

fn object_type_from_uri(uri: &str) -> Option<String> {
    uri.rsplit_once('.')
        .map(|(_, ext)| ext.to_ascii_lowercase())
}

fn audit_result_to_vcir_status(result: &AuditObjectResult) -> VcirArtifactValidationStatus {
    match result {
        AuditObjectResult::Ok => VcirArtifactValidationStatus::Accepted,
        AuditObjectResult::Error => VcirArtifactValidationStatus::Rejected,
        AuditObjectResult::Skipped => VcirArtifactValidationStatus::WarningOnly,
    }
}

fn roa_to_vrps_for_vcir(roa: &RoaObject) -> Vec<Vrp> {
    let asn = roa.roa.as_id;
    let mut out = Vec::new();
    for fam in &roa.roa.ip_addr_blocks {
        for entry in &fam.addresses {
            let max_length = entry.max_length.unwrap_or(entry.prefix.prefix_len);
            out.push(Vrp {
                asn,
                prefix: entry.prefix.clone(),
                max_length,
            });
        }
    }
    out
}

fn vrp_prefix_to_string(vrp: &Vrp) -> String {
    match vrp.prefix.afi {
        RoaAfi::Ipv4 => {
            let addr = std::net::Ipv4Addr::new(
                vrp.prefix.addr[0],
                vrp.prefix.addr[1],
                vrp.prefix.addr[2],
                vrp.prefix.addr[3],
            );
            format!("{addr}/{}", vrp.prefix.prefix_len)
        }
        RoaAfi::Ipv6 => {
            let addr = std::net::Ipv6Addr::from(vrp.prefix.addr);
            format!("{addr}/{}", vrp.prefix.prefix_len)
        }
    }
}

#[cfg(test)]
#[path = "tree_runner/tests.rs"]
mod tests;
