mod config;
mod keys;
mod pack;
mod pp_cache_index;

use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};

use base64::Engine;
use rocksdb::checkpoint::Checkpoint;
use rocksdb::{ColumnFamily, DB, Direction, IteratorMode, Options, WriteBatch};
use serde::{Deserialize, Serialize};

use crate::blob_store::{ExternalRawStoreDb, ExternalRepoBytesDb, RawObjectStore};
use crate::data_model::rc::{AsResourceSet, IpResourceSet};

use config::*;
pub use config::{
    ALL_COLUMN_FAMILY_NAMES, CF_CHILD_CERTIFICATE_CACHE_PROJECTION, CF_MANIFEST_REPLAY_META,
    CF_PUBLICATION_POINT_CACHE_PROJECTION, CF_RAW_BY_HASH, CF_REPOSITORY_VIEW,
    CF_ROA_CACHE_PROJECTION, CF_RRDP_SOURCE, CF_RRDP_SOURCE_MEMBER, CF_RRDP_URI_OWNER,
    CF_TRANSPORT_PREFETCH, CF_VCIR, CF_VCIR_FAILED_FETCH_REUSE_IDENTITY, column_family_descriptors,
};
use keys::*;
use pack::compute_sha256_32;
pub use pack::{PackBytes, PackFile, PackTime};
pub use pp_cache_index::{PpCacheIndexLoadStats, PpCacheIndexRefreshStats};
use pp_cache_index::{
    PpCacheIndexLookup, PpCacheMmapIndexSet, compact_pp_cache_index, default_pp_cache_index_dir,
    load_pp_cache_mmap_index, load_pp_cache_mmap_index_set, pp_cache_index_directory_stats,
    write_pp_cache_index_atomic, write_pp_cache_index_segment,
};
#[derive(Debug, thiserror::Error)]
pub enum StorageError {
    #[error("rocksdb error: {0}")]
    RocksDb(String),

    #[error("missing column family: {0}")]
    MissingColumnFamily(&'static str),

    #[error("cbor codec error for {entity}: {detail}")]
    Codec {
        entity: &'static str,
        detail: String,
    },

    #[error("invalid {entity}: {detail}")]
    InvalidData {
        entity: &'static str,
        detail: String,
    },
}

pub type StorageResult<T> = Result<T, StorageError>;

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct RepositoryBlobVerificationSummary {
    pub current_objects: u64,
    pub bytes_verified: u64,
    pub batches: u64,
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct ChildCertificateCacheMmapLookup {
    pub projections: Vec<Option<ChildCertificateCacheProjection>>,
    pub hits: usize,
    pub misses: usize,
    pub file_bytes: u64,
}

pub struct RocksStore {
    db: DB,
    external_raw_store: Option<ExternalRawStoreDb>,
    external_repo_bytes: Option<ExternalRepoBytesDb>,
    publication_point_cache_index_dir: PathBuf,
    child_certificate_cache_index_dir: PathBuf,
    publication_point_cache_projection_index: Mutex<PublicationPointCacheProjectionIndexState>,
}

enum PublicationPointCacheProjectionIndexState {
    Uninitialized,
    Disabled,
    BuildingFromEmpty {
        index: HashMap<String, Arc<[u8]>>,
        bytes: usize,
        limit: usize,
    },
    Loaded {
        index: HashMap<String, Arc<[u8]>>,
        bytes: usize,
    },
    LoadedMmap {
        mmap: PpCacheMmapIndexSet,
        dirty: HashMap<String, Arc<[u8]>>,
        dirty_bytes: usize,
        load_stats: PpCacheIndexLoadStats,
    },
}

#[derive(Clone, Copy)]
pub(crate) enum PublicationPointCacheProjectionWriteAction<'a> {
    Keep,
    Write(&'a PublicationPointCacheProjection),
    Delete { manifest_rsync_uri: &'a str },
}

fn process_vm_rss_kb() -> Option<u64> {
    let status = std::fs::read_to_string("/proc/self/status").ok()?;
    status.lines().find_map(|line| {
        let rest = line.strip_prefix("VmRSS:")?;
        rest.split_whitespace().next()?.parse::<u64>().ok()
    })
}

fn default_child_certificate_cache_index_dir(db_path: &Path) -> PathBuf {
    let file_name = db_path
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or("work-db");
    db_path.with_file_name(format!("{file_name}.child-cert-cache-index"))
}

fn child_certificate_cache_segment_file_name(manifest_rsync_uri: &str) -> String {
    format!(
        "{}.idx",
        hex::encode(compute_sha256_32(manifest_rsync_uri.as_bytes()))
    )
}

const ROCKSDB_MEMORY_PROPERTY_NAMES: &[(&str, &str)] = &[
    ("cur_size_all_mem_tables", "rocksdb.cur-size-all-mem-tables"),
    ("size_all_mem_tables", "rocksdb.size-all-mem-tables"),
    (
        "estimate_table_readers_mem",
        "rocksdb.estimate-table-readers-mem",
    ),
    ("block_cache_capacity", "rocksdb.block-cache-capacity"),
    ("block_cache_usage", "rocksdb.block-cache-usage"),
    (
        "block_cache_pinned_usage",
        "rocksdb.block-cache-pinned-usage",
    ),
    ("num_snapshots", "rocksdb.num-snapshots"),
    ("background_errors", "rocksdb.background-errors"),
];

const PP_CACHE_RAW_INDEX_ENV: &str = "RPKI_PP_CACHE_RAW_INDEX";
const PP_CACHE_RAW_INDEX_EMPTY_BUILD_LIMIT_ENV: &str =
    "RPKI_PP_CACHE_RAW_INDEX_EMPTY_BUILD_LIMIT_BYTES";
const DEFAULT_PP_CACHE_RAW_INDEX_EMPTY_BUILD_LIMIT_BYTES: usize = 32 * 1024 * 1024;
const PP_CACHE_INDEX_COMPACTION_SEGMENT_THRESHOLD: usize = 16;
const PP_CACHE_INDEX_COMPACTION_BYTES_THRESHOLD: u64 = 1_610_612_736;

fn pp_cache_raw_index_enabled() -> bool {
    match std::env::var(PP_CACHE_RAW_INDEX_ENV) {
        Ok(value) => !matches!(
            value.trim().to_ascii_lowercase().as_str(),
            "0" | "false" | "off" | "no"
        ),
        Err(_) => true,
    }
}

fn pp_cache_raw_index_empty_build_limit_bytes() -> usize {
    std::env::var(PP_CACHE_RAW_INDEX_EMPTY_BUILD_LIMIT_ENV)
        .ok()
        .and_then(|value| value.trim().parse::<usize>().ok())
        .unwrap_or(DEFAULT_PP_CACHE_RAW_INDEX_EMPTY_BUILD_LIMIT_BYTES)
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct RocksDbMemoryProperties {
    pub cur_size_all_mem_tables: Option<u64>,
    pub size_all_mem_tables: Option<u64>,
    pub estimate_table_readers_mem: Option<u64>,
    pub block_cache_capacity: Option<u64>,
    pub block_cache_usage: Option<u64>,
    pub block_cache_pinned_usage: Option<u64>,
    pub num_snapshots: Option<u64>,
    pub background_errors: Option<u64>,
    pub errors: Vec<String>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct RocksDbColumnFamilyMemoryProperties {
    pub name: String,
    pub properties: RocksDbMemoryProperties,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct RocksDbMemoryDbSnapshot {
    pub label: String,
    pub properties: RocksDbMemoryProperties,
    pub column_families: Vec<RocksDbColumnFamilyMemoryProperties>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct RocksDbMemoryTotals {
    pub cur_size_all_mem_tables: u64,
    pub size_all_mem_tables: u64,
    pub estimate_table_readers_mem: u64,
    pub block_cache_capacity: u64,
    pub block_cache_usage: u64,
    pub block_cache_pinned_usage: u64,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct RocksDbMemorySnapshot {
    pub databases: Vec<RocksDbMemoryDbSnapshot>,
    pub totals: RocksDbMemoryTotals,
}

impl RocksDbMemoryTotals {
    fn add_properties(&mut self, properties: &RocksDbMemoryProperties) {
        self.cur_size_all_mem_tables += properties.cur_size_all_mem_tables.unwrap_or(0);
        self.size_all_mem_tables += properties.size_all_mem_tables.unwrap_or(0);
        self.estimate_table_readers_mem += properties.estimate_table_readers_mem.unwrap_or(0);
        self.block_cache_capacity += properties.block_cache_capacity.unwrap_or(0);
        self.block_cache_usage += properties.block_cache_usage.unwrap_or(0);
        self.block_cache_pinned_usage += properties.block_cache_pinned_usage.unwrap_or(0);
    }
}

fn set_memory_property(properties: &mut RocksDbMemoryProperties, name: &str, value: u64) {
    match name {
        "cur_size_all_mem_tables" => properties.cur_size_all_mem_tables = Some(value),
        "size_all_mem_tables" => properties.size_all_mem_tables = Some(value),
        "estimate_table_readers_mem" => properties.estimate_table_readers_mem = Some(value),
        "block_cache_capacity" => properties.block_cache_capacity = Some(value),
        "block_cache_usage" => properties.block_cache_usage = Some(value),
        "block_cache_pinned_usage" => properties.block_cache_pinned_usage = Some(value),
        "num_snapshots" => properties.num_snapshots = Some(value),
        "background_errors" => properties.background_errors = Some(value),
        _ => {}
    }
}

fn parse_rocksdb_property_int(raw: Option<String>) -> Option<u64> {
    raw.and_then(|value| value.trim().parse::<u64>().ok())
}

fn memory_properties_for_db(db: &DB) -> RocksDbMemoryProperties {
    let mut properties = RocksDbMemoryProperties::default();
    for (field_name, property_name) in ROCKSDB_MEMORY_PROPERTY_NAMES {
        match db.property_value(*property_name) {
            Ok(value) => {
                if let Some(parsed) = parse_rocksdb_property_int(value) {
                    set_memory_property(&mut properties, field_name, parsed);
                }
            }
            Err(err) => properties.errors.push(format!("{property_name}: {}", err)),
        }
    }
    properties
}

fn memory_properties_for_cf(db: &DB, cf_name: &'static str) -> RocksDbColumnFamilyMemoryProperties {
    let mut properties = RocksDbMemoryProperties::default();
    let Some(cf) = db.cf_handle(cf_name) else {
        properties
            .errors
            .push(format!("missing column family: {cf_name}"));
        return RocksDbColumnFamilyMemoryProperties {
            name: cf_name.to_string(),
            properties,
        };
    };
    for (field_name, property_name) in ROCKSDB_MEMORY_PROPERTY_NAMES {
        match db.property_value_cf(cf, *property_name) {
            Ok(value) => {
                if let Some(parsed) = parse_rocksdb_property_int(value) {
                    set_memory_property(&mut properties, field_name, parsed);
                }
            }
            Err(err) => properties.errors.push(format!("{property_name}: {}", err)),
        }
    }
    RocksDbColumnFamilyMemoryProperties {
        name: cf_name.to_string(),
        properties,
    }
}

pub(crate) fn memory_db_snapshot_for_column_families(
    label: impl Into<String>,
    db: &DB,
    column_families: Option<&[&'static str]>,
) -> RocksDbMemoryDbSnapshot {
    RocksDbMemoryDbSnapshot {
        label: label.into(),
        properties: memory_properties_for_db(db),
        column_families: column_families
            .map(|names| {
                names
                    .iter()
                    .map(|name| memory_properties_for_cf(db, name))
                    .collect()
            })
            .unwrap_or_default(),
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RrdpDeltaOp {
    Upsert { rsync_uri: String, bytes: Vec<u8> },
    Delete { rsync_uri: String },
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RepositoryViewState {
    Present,
    Withdrawn,
    Replaced,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RepositoryViewEntry {
    pub rsync_uri: String,
    pub current_hash: Option<String>,
    pub repository_source: Option<String>,
    pub object_type: Option<String>,
    pub state: RepositoryViewState,
}

impl RepositoryViewEntry {
    pub fn validate_internal(&self) -> StorageResult<()> {
        validate_non_empty("repository_view.rsync_uri", &self.rsync_uri)?;
        if let Some(source) = &self.repository_source {
            validate_non_empty("repository_view.repository_source", source)?;
        }
        match self.state {
            RepositoryViewState::Present | RepositoryViewState::Replaced => {
                let hash = self
                    .current_hash
                    .as_deref()
                    .ok_or(StorageError::InvalidData {
                        entity: "repository_view",
                        detail: "current_hash is required when state is present or replaced"
                            .to_string(),
                    })?;
                validate_sha256_hex("repository_view.current_hash", hash)?;
            }
            RepositoryViewState::Withdrawn => {
                if let Some(hash) = &self.current_hash {
                    validate_sha256_hex("repository_view.current_hash", hash)?;
                }
            }
        }
        Ok(())
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RawByHashEntry {
    pub sha256_hex: String,
    pub bytes: Vec<u8>,
    pub origin_uris: Vec<String>,
    pub object_type: Option<String>,
    pub encoding: Option<String>,
}

impl RawByHashEntry {
    pub fn from_bytes(sha256_hex: impl Into<String>, bytes: Vec<u8>) -> Self {
        Self {
            sha256_hex: sha256_hex.into(),
            bytes,
            origin_uris: Vec::new(),
            object_type: None,
            encoding: None,
        }
    }

    pub fn validate_internal(&self) -> StorageResult<()> {
        validate_sha256_hex("raw_by_hash.sha256_hex", &self.sha256_hex)?;
        if self.bytes.is_empty() {
            return Err(StorageError::InvalidData {
                entity: "raw_by_hash",
                detail: "bytes must not be empty".to_string(),
            });
        }
        let computed = hex::encode(compute_sha256_32(&self.bytes));
        if computed != self.sha256_hex.to_ascii_lowercase() {
            return Err(StorageError::InvalidData {
                entity: "raw_by_hash",
                detail: "sha256_hex does not match bytes".to_string(),
            });
        }
        let mut seen = HashSet::with_capacity(self.origin_uris.len());
        for uri in &self.origin_uris {
            validate_non_empty("raw_by_hash.origin_uris[]", uri)?;
            if !seen.insert(uri.as_str()) {
                return Err(StorageError::InvalidData {
                    entity: "raw_by_hash",
                    detail: format!("duplicate origin URI: {uri}"),
                });
            }
        }
        Ok(())
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CurrentObjectWithHash {
    pub current_hash_hex: String,
    pub current_hash: [u8; 32],
    pub bytes: Vec<u8>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ValidatedManifestMeta {
    pub validated_manifest_number: Vec<u8>,
    pub validated_manifest_this_update: PackTime,
    pub validated_manifest_next_update: PackTime,
}

impl ValidatedManifestMeta {
    pub fn validate_internal(&self) -> StorageResult<()> {
        validate_manifest_number_be(
            "validated_manifest_meta.validated_manifest_number",
            &self.validated_manifest_number,
        )?;
        let this_update = parse_time(
            "validated_manifest_meta.validated_manifest_this_update",
            &self.validated_manifest_this_update,
        )?;
        let next_update = parse_time(
            "validated_manifest_meta.validated_manifest_next_update",
            &self.validated_manifest_next_update,
        )?;
        if next_update < this_update {
            return Err(StorageError::InvalidData {
                entity: "validated_manifest_meta",
                detail: "validated_manifest_next_update must be >= validated_manifest_this_update"
                    .to_string(),
            });
        }
        Ok(())
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ManifestReplayMeta {
    pub manifest_rsync_uri: String,
    pub manifest_number_be: Vec<u8>,
    pub manifest_this_update: PackTime,
    pub manifest_sha256: Vec<u8>,
    pub updated_at_validation_time: PackTime,
}

impl ManifestReplayMeta {
    pub fn from_vcir(vcir: &ValidatedCaInstanceResult) -> Self {
        Self {
            manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
            manifest_number_be: vcir
                .validated_manifest_meta
                .validated_manifest_number
                .clone(),
            manifest_this_update: vcir
                .validated_manifest_meta
                .validated_manifest_this_update
                .clone(),
            manifest_sha256: vcir.ccr_manifest_projection.manifest_sha256.clone(),
            updated_at_validation_time: vcir.last_successful_validation_time.clone(),
        }
    }

    pub fn validate_internal(&self) -> StorageResult<()> {
        validate_non_empty(
            "manifest_replay_meta.manifest_rsync_uri",
            &self.manifest_rsync_uri,
        )?;
        validate_manifest_number_be(
            "manifest_replay_meta.manifest_number_be",
            &self.manifest_number_be,
        )?;
        parse_time(
            "manifest_replay_meta.manifest_this_update",
            &self.manifest_this_update,
        )?;
        validate_sha256_digest_bytes(
            "manifest_replay_meta.manifest_sha256",
            &self.manifest_sha256,
        )?;
        parse_time(
            "manifest_replay_meta.updated_at_validation_time",
            &self.updated_at_validation_time,
        )?;
        Ok(())
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VcirCcrManifestProjection {
    pub manifest_rsync_uri: String,
    pub manifest_sha256: Vec<u8>,
    pub manifest_size: u64,
    pub manifest_ee_aki: Vec<u8>,
    pub manifest_number_be: Vec<u8>,
    pub manifest_this_update: PackTime,
    pub manifest_sia_locations_der: Vec<Vec<u8>>,
    pub subordinate_skis: Vec<Vec<u8>>,
}

impl VcirCcrManifestProjection {
    pub fn validate_internal(&self) -> StorageResult<()> {
        validate_non_empty(
            "vcir.ccr_manifest_projection.manifest_rsync_uri",
            &self.manifest_rsync_uri,
        )?;
        validate_sha256_digest_bytes(
            "vcir.ccr_manifest_projection.manifest_sha256",
            &self.manifest_sha256,
        )?;
        if self.manifest_size < 1000 {
            return Err(StorageError::InvalidData {
                entity: "vcir.ccr_manifest_projection.manifest_size",
                detail: format!("must be >= 1000, got {}", self.manifest_size),
            });
        }
        validate_fixed_len_bytes(
            "vcir.ccr_manifest_projection.manifest_ee_aki",
            &self.manifest_ee_aki,
            20,
        )?;
        validate_manifest_number_be(
            "vcir.ccr_manifest_projection.manifest_number_be",
            &self.manifest_number_be,
        )?;
        parse_time(
            "vcir.ccr_manifest_projection.manifest_this_update",
            &self.manifest_this_update,
        )?;
        if self.manifest_sia_locations_der.is_empty() {
            return Err(StorageError::InvalidData {
                entity: "vcir.ccr_manifest_projection.manifest_sia_locations_der",
                detail: "must contain at least one AccessDescription".to_string(),
            });
        }
        for location in &self.manifest_sia_locations_der {
            validate_full_der_with_tag(
                "vcir.ccr_manifest_projection.manifest_sia_locations_der[]",
                location,
                Some(0x30),
            )?;
        }
        validate_sorted_unique_fixed_len_bytes(
            "vcir.ccr_manifest_projection.subordinate_skis",
            &self.subordinate_skis,
            20,
        )?;
        Ok(())
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VcirInstanceGate {
    pub manifest_next_update: PackTime,
    pub current_crl_next_update: PackTime,
    pub self_ca_not_after: PackTime,
    pub instance_effective_until: PackTime,
}

impl VcirInstanceGate {
    pub fn validate_internal(&self) -> StorageResult<()> {
        let manifest_next_update = parse_time(
            "vcir.instance_gate.manifest_next_update",
            &self.manifest_next_update,
        )?;
        let current_crl_next_update = parse_time(
            "vcir.instance_gate.current_crl_next_update",
            &self.current_crl_next_update,
        )?;
        let self_ca_not_after = parse_time(
            "vcir.instance_gate.self_ca_not_after",
            &self.self_ca_not_after,
        )?;
        let instance_effective_until = parse_time(
            "vcir.instance_gate.instance_effective_until",
            &self.instance_effective_until,
        )?;
        let expected = manifest_next_update
            .min(current_crl_next_update)
            .min(self_ca_not_after);
        if instance_effective_until != expected {
            return Err(StorageError::InvalidData {
                entity: "vcir.instance_gate",
                detail: "instance_effective_until must equal min(manifest_next_update, current_crl_next_update, self_ca_not_after)".to_string(),
            });
        }
        Ok(())
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VcirFailedFetchReuseIdentity {
    #[serde(rename = "c")]
    #[serde(with = "serde_bytes_32")]
    pub current_ca_sha256: [u8; 32],
    #[serde(rename = "t")]
    #[serde(with = "serde_bytes_32")]
    pub ta_context_digest: [u8; 32],
    #[serde(rename = "p")]
    #[serde(with = "serde_bytes_32")]
    pub ca_validation_context_digest: [u8; 32],
    #[serde(rename = "f")]
    #[serde(with = "serde_bytes_32")]
    pub policy_fingerprint: [u8; 32],
    #[serde(rename = "nb")]
    pub effective_not_before: PackTime,
    #[serde(rename = "nu")]
    pub effective_until: PackTime,
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct VcirReuseRecordsCleared {
    pub vcir_records: u64,
    pub failed_fetch_identity_records: u64,
}

impl VcirFailedFetchReuseIdentity {
    pub fn validate_internal(&self) -> StorageResult<()> {
        let effective_not_before = parse_time(
            "vcir_failed_fetch_reuse_identity.effective_not_before",
            &self.effective_not_before,
        )?;
        let effective_until = parse_time(
            "vcir_failed_fetch_reuse_identity.effective_until",
            &self.effective_until,
        )?;
        if effective_not_before >= effective_until {
            return Err(StorageError::InvalidData {
                entity: "vcir_failed_fetch_reuse_identity.effective_window",
                detail: "effective_not_before must be before effective_until".to_string(),
            });
        }
        Ok(())
    }

    pub fn contains_validation_time(&self, validation_time: time::OffsetDateTime) -> bool {
        let Ok(effective_not_before) = self.effective_not_before.parse() else {
            return false;
        };
        let Ok(effective_until) = self.effective_until.parse() else {
            return false;
        };
        validation_time >= effective_not_before && validation_time < effective_until
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VcirChildEntry {
    pub child_manifest_rsync_uri: String,
    pub child_cert_rsync_uri: String,
    pub child_cert_hash: String,
    pub child_ski: String,
    pub child_rsync_base_uri: String,
    pub child_publication_point_rsync_uri: String,
    pub child_rrdp_notification_uri: Option<String>,
    pub child_effective_ip_resources: Option<IpResourceSet>,
    pub child_effective_as_resources: Option<AsResourceSet>,
    pub accepted_at_validation_time: PackTime,
}

impl VcirChildEntry {
    pub fn validate_internal(&self) -> StorageResult<()> {
        validate_non_empty(
            "vcir.child_entries[].child_manifest_rsync_uri",
            &self.child_manifest_rsync_uri,
        )?;
        validate_non_empty(
            "vcir.child_entries[].child_cert_rsync_uri",
            &self.child_cert_rsync_uri,
        )?;
        validate_sha256_hex(
            "vcir.child_entries[].child_cert_hash",
            &self.child_cert_hash,
        )?;
        validate_non_empty("vcir.child_entries[].child_ski", &self.child_ski)?;
        validate_non_empty(
            "vcir.child_entries[].child_rsync_base_uri",
            &self.child_rsync_base_uri,
        )?;
        validate_non_empty(
            "vcir.child_entries[].child_publication_point_rsync_uri",
            &self.child_publication_point_rsync_uri,
        )?;
        if let Some(uri) = &self.child_rrdp_notification_uri {
            validate_non_empty("vcir.child_entries[].child_rrdp_notification_uri", uri)?;
        }
        parse_time(
            "vcir.child_entries[].accepted_at_validation_time",
            &self.accepted_at_validation_time,
        )?;
        Ok(())
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VcirOutputType {
    Vrp,
    Aspa,
    RouterKey,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VcirSourceObjectType {
    Roa,
    Aspa,
    RouterKey,
    Other,
}

impl VcirSourceObjectType {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Roa => "roa",
            Self::Aspa => "aspa",
            Self::RouterKey => "router_key",
            Self::Other => "other",
        }
    }
}

struct FixedBytesVisitor<const N: usize>;

impl<'de, const N: usize> serde::de::Visitor<'de> for FixedBytesVisitor<N> {
    type Value = [u8; N];

    fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(formatter, "{N} bytes")
    }

    fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E>
    where
        E: serde::de::Error,
    {
        if value.len() != N {
            return Err(E::invalid_length(value.len(), &self));
        }
        let mut out = [0u8; N];
        out.copy_from_slice(value);
        Ok(out)
    }

    fn visit_byte_buf<E>(self, value: Vec<u8>) -> Result<Self::Value, E>
    where
        E: serde::de::Error,
    {
        self.visit_bytes(&value)
    }

    fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
    where
        A: serde::de::SeqAccess<'de>,
    {
        let mut out = [0u8; N];
        for (idx, slot) in out.iter_mut().enumerate() {
            *slot = seq
                .next_element()?
                .ok_or_else(|| serde::de::Error::invalid_length(idx, &self))?;
        }
        Ok(out)
    }
}

fn deserialize_fixed_bytes<'de, D, const N: usize>(deserializer: D) -> Result<[u8; N], D::Error>
where
    D: serde::Deserializer<'de>,
{
    deserializer.deserialize_bytes(FixedBytesVisitor::<N>)
}

mod serde_bytes_16 {
    pub(super) fn serialize<S>(value: &[u8; 16], serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_bytes(value)
    }

    pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 16], D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        super::deserialize_fixed_bytes::<D, 16>(deserializer)
    }
}

mod serde_bytes_32 {
    pub(super) fn serialize<S>(value: &[u8; 32], serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_bytes(value)
    }

    pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 32], D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        super::deserialize_fixed_bytes::<D, 32>(deserializer)
    }
}

struct ByteVecVisitor;

impl<'de> serde::de::Visitor<'de> for ByteVecVisitor {
    type Value = Vec<u8>;

    fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter.write_str("byte vector")
    }

    fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E>
    where
        E: serde::de::Error,
    {
        Ok(value.to_vec())
    }

    fn visit_byte_buf<E>(self, value: Vec<u8>) -> Result<Self::Value, E>
    where
        E: serde::de::Error,
    {
        Ok(value)
    }

    fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
    where
        A: serde::de::SeqAccess<'de>,
    {
        let mut out = Vec::with_capacity(seq.size_hint().unwrap_or(0));
        while let Some(byte) = seq.next_element()? {
            out.push(byte);
        }
        Ok(out)
    }
}

mod serde_byte_vec {
    pub(super) fn serialize<S>(value: &[u8], serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_bytes(value)
    }

    pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        deserializer.deserialize_bytes(super::ByteVecVisitor)
    }
}

mod serde_optional_byte_vec {
    pub(super) fn serialize<S>(value: &Option<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        match value {
            Some(bytes) => serializer.serialize_some(bytes),
            None => serializer.serialize_none(),
        }
    }

    pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        struct OptionalByteVecVisitor;

        impl<'de> serde::de::Visitor<'de> for OptionalByteVecVisitor {
            type Value = Option<Vec<u8>>;

            fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                formatter.write_str("optional byte vector")
            }

            fn visit_none<E>(self) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                Ok(None)
            }

            fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
            where
                D: serde::Deserializer<'de>,
            {
                deserializer
                    .deserialize_bytes(super::ByteVecVisitor)
                    .map(Some)
            }
        }

        deserializer.deserialize_option(OptionalByteVecVisitor)
    }
}

mod serde_optional_bytes_32 {
    pub(super) fn serialize<S>(value: &Option<[u8; 32]>, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        match value {
            Some(bytes) => serializer.serialize_some(bytes.as_slice()),
            None => serializer.serialize_none(),
        }
    }

    pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Option<[u8; 32]>, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        struct OptionalBytes32Visitor;

        impl<'de> serde::de::Visitor<'de> for OptionalBytes32Visitor {
            type Value = Option<[u8; 32]>;

            fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                formatter.write_str("optional 32-byte array")
            }

            fn visit_none<E>(self) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                Ok(None)
            }

            fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
            where
                D: serde::Deserializer<'de>,
            {
                super::deserialize_fixed_bytes::<D, 32>(deserializer).map(Some)
            }
        }

        deserializer.deserialize_option(OptionalBytes32Visitor)
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VcirLocalOutputPayload {
    Vrp {
        asn: u32,
        afi: crate::data_model::roa::RoaAfi,
        prefix_len: u16,
        #[serde(with = "serde_bytes_16")]
        addr: [u8; 16],
        max_length: u16,
    },
    Aspa {
        customer_as_id: u32,
        provider_as_ids: Vec<u32>,
    },
    RouterKey {
        as_id: u32,
        #[serde(with = "serde_byte_vec")]
        ski: Vec<u8>,
        #[serde(with = "serde_byte_vec")]
        spki_der: Vec<u8>,
    },
}

impl VcirLocalOutputPayload {
    pub fn typed_body_bytes(&self) -> u64 {
        match self {
            Self::Vrp { .. } => 4 + 1 + 2 + 16 + 2,
            Self::Aspa {
                provider_as_ids, ..
            } => 4 + (provider_as_ids.len() as u64 * 4),
            Self::RouterKey { ski, spki_der, .. } => 4 + ski.len() as u64 + spki_der.len() as u64,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VcirLocalOutput {
    #[serde(rename = "t")]
    pub output_type: VcirOutputType,
    #[serde(rename = "e")]
    pub item_effective_until: PackTime,
    #[serde(rename = "u")]
    pub source_object_uri: String,
    #[serde(rename = "k")]
    pub source_object_type: VcirSourceObjectType,
    #[serde(rename = "h")]
    #[serde(with = "serde_bytes_32")]
    pub source_object_hash: [u8; 32],
    #[serde(rename = "c")]
    #[serde(with = "serde_bytes_32")]
    pub source_ee_cert_hash: [u8; 32],
    #[serde(rename = "p")]
    pub payload: VcirLocalOutputPayload,
    #[serde(rename = "r")]
    #[serde(with = "serde_bytes_32")]
    pub rule_hash: [u8; 32],
}

impl VcirLocalOutput {
    pub fn output_id(&self) -> String {
        self.rule_hash_hex()
    }

    pub fn source_object_hash_hex(&self) -> String {
        hex::encode(self.source_object_hash)
    }

    pub fn source_ee_cert_hash_hex(&self) -> String {
        hex::encode(self.source_ee_cert_hash)
    }

    pub fn rule_hash_hex(&self) -> String {
        hex::encode(self.rule_hash)
    }

    pub fn source_object_type_name(&self) -> &'static str {
        self.source_object_type.as_str()
    }

    pub fn payload_json(&self) -> String {
        match &self.payload {
            VcirLocalOutputPayload::Vrp {
                asn,
                afi,
                prefix_len,
                addr,
                max_length,
            } => {
                let prefix = match afi {
                    crate::data_model::roa::RoaAfi::Ipv4 => {
                        let ip = std::net::Ipv4Addr::new(addr[0], addr[1], addr[2], addr[3]);
                        format!("{ip}/{prefix_len}")
                    }
                    crate::data_model::roa::RoaAfi::Ipv6 => {
                        let ip = std::net::Ipv6Addr::from(*addr);
                        format!("{ip}/{prefix_len}")
                    }
                };
                format!(r#"{{"asn":{asn},"max_length":{max_length},"prefix":"{prefix}"}}"#)
            }
            VcirLocalOutputPayload::Aspa {
                customer_as_id,
                provider_as_ids,
            } => {
                let providers = provider_as_ids
                    .iter()
                    .map(u32::to_string)
                    .collect::<Vec<_>>()
                    .join(",");
                format!(r#"{{"customer_as_id":{customer_as_id},"provider_as_ids":[{providers}]}}"#)
            }
            VcirLocalOutputPayload::RouterKey {
                as_id,
                ski,
                spki_der,
            } => {
                let ski_hex = hex::encode(ski);
                let spki_der_base64 = base64::engine::general_purpose::STANDARD.encode(spki_der);
                format!(
                    r#"{{"as_id":{as_id},"ski_hex":"{ski_hex}","spki_der_base64":"{spki_der_base64}"}}"#
                )
            }
        }
    }

    pub fn validate_internal(&self) -> StorageResult<()> {
        parse_time(
            "vcir.local_outputs[].item_effective_until",
            &self.item_effective_until,
        )?;
        validate_non_empty(
            "vcir.local_outputs[].source_object_uri",
            &self.source_object_uri,
        )?;
        validate_local_output_type_matches_payload(self)?;
        Ok(())
    }
}

fn validate_local_output_type_matches_payload(output: &VcirLocalOutput) -> StorageResult<()> {
    let matches_payload = matches!(
        (&output.output_type, &output.payload),
        (VcirOutputType::Vrp, VcirLocalOutputPayload::Vrp { .. })
            | (VcirOutputType::Aspa, VcirLocalOutputPayload::Aspa { .. })
            | (
                VcirOutputType::RouterKey,
                VcirLocalOutputPayload::RouterKey { .. }
            )
    );
    if !matches_payload {
        return Err(StorageError::InvalidData {
            entity: "vcir.local_outputs[]",
            detail: "output_type must match payload variant".to_string(),
        });
    }
    Ok(())
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoaCacheCrlProjection {
    #[serde(rename = "u")]
    pub uri: String,
    #[serde(rename = "h")]
    pub sha256: String,
}

impl RoaCacheCrlProjection {
    pub fn validate_internal(&self) -> StorageResult<()> {
        validate_non_empty("roa_cache_projection.crls[].uri", &self.uri)?;
        validate_sha256_hex("roa_cache_projection.crls[].sha256", &self.sha256)?;
        Ok(())
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RoaCacheObjectMeta {
    pub source_object_uri: String,
    pub source_object_hash: [u8; 32],
    pub ee_serial: Vec<u8>,
    pub crl_uri: String,
    pub earliest_safe_reuse_time: PackTime,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RoaCacheProjectionContext {
    pub ca_validation_context_digest: [u8; 32],
    pub policy_fingerprint: [u8; 32],
    pub object_meta: Vec<RoaCacheObjectMeta>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoaCacheLocalOutputProjection {
    #[serde(rename = "e")]
    pub item_effective_until: PackTime,
    #[serde(rename = "c")]
    #[serde(with = "serde_bytes_32")]
    pub source_ee_cert_hash: [u8; 32],
    #[serde(rename = "p")]
    pub payload: VcirLocalOutputPayload,
    #[serde(rename = "r")]
    #[serde(with = "serde_bytes_32")]
    pub rule_hash: [u8; 32],
}

impl RoaCacheLocalOutputProjection {
    fn from_local_output(output: &VcirLocalOutput) -> Option<Self> {
        if output.output_type != VcirOutputType::Vrp
            || output.source_object_type != VcirSourceObjectType::Roa
        {
            return None;
        }
        Some(Self {
            item_effective_until: output.item_effective_until.clone(),
            source_ee_cert_hash: output.source_ee_cert_hash,
            payload: output.payload.clone(),
            rule_hash: output.rule_hash,
        })
    }

    pub fn validate_internal(&self) -> StorageResult<()> {
        parse_time(
            "roa_cache_projection.entries[].outputs[].item_effective_until",
            &self.item_effective_until,
        )?;
        if !matches!(self.payload, VcirLocalOutputPayload::Vrp { .. }) {
            return Err(StorageError::InvalidData {
                entity: "roa_cache_projection.entries[].outputs[]",
                detail: "payload must be VRP".to_string(),
            });
        }
        Ok(())
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoaCacheObjectProjection {
    #[serde(rename = "u")]
    pub source_object_uri: String,
    #[serde(rename = "h")]
    #[serde(with = "serde_bytes_32")]
    pub source_object_hash: [u8; 32],
    #[serde(rename = "s", default, skip_serializing_if = "Option::is_none")]
    #[serde(with = "serde_optional_byte_vec")]
    pub ee_serial: Option<Vec<u8>>,
    #[serde(rename = "c", default, skip_serializing_if = "Option::is_none")]
    pub crl_uri: Option<String>,
    #[serde(rename = "n", default, skip_serializing_if = "Option::is_none")]
    pub earliest_safe_reuse_time_unix: Option<i64>,
    #[serde(rename = "x")]
    pub outputs_effective_until_unix: i64,
    #[serde(rename = "o")]
    pub outputs: Vec<RoaCacheLocalOutputProjection>,
}

impl RoaCacheObjectProjection {
    pub fn validate_internal(&self) -> StorageResult<()> {
        validate_non_empty(
            "roa_cache_projection.entries[].source_object_uri",
            &self.source_object_uri,
        )?;
        if let Some(serial) = &self.ee_serial {
            if serial.is_empty() {
                return Err(StorageError::InvalidData {
                    entity: "roa_cache_projection.entries[].ee_serial",
                    detail: "must not be empty when present".to_string(),
                });
            }
        }
        if let Some(crl_uri) = &self.crl_uri {
            validate_non_empty("roa_cache_projection.entries[].crl_uri", crl_uri)?;
        }
        if let Some(earliest_safe_reuse_time_unix) = self.earliest_safe_reuse_time_unix
            && earliest_safe_reuse_time_unix >= self.outputs_effective_until_unix
        {
            return Err(StorageError::InvalidData {
                entity: "roa_cache_projection.entries[].effective_window",
                detail: "earliest_safe_reuse_time_unix must be before outputs_effective_until_unix"
                    .to_string(),
            });
        }
        if self.outputs.is_empty() {
            return Err(StorageError::InvalidData {
                entity: "roa_cache_projection.entries[]",
                detail: "outputs must not be empty".to_string(),
            });
        }
        for output in &self.outputs {
            output.validate_internal()?;
        }
        let expected_effective_until = self
            .outputs
            .iter()
            .map(|output| {
                output
                    .item_effective_until
                    .parse()
                    .map(|time| time.unix_timestamp())
                    .map_err(|detail| StorageError::InvalidData {
                        entity: "roa_cache_projection.entries[].outputs_effective_until_unix",
                        detail,
                    })
            })
            .collect::<StorageResult<Vec<_>>>()?
            .into_iter()
            .min()
            .expect("outputs must not be empty");
        if self.outputs_effective_until_unix != expected_effective_until {
            return Err(StorageError::InvalidData {
                entity: "roa_cache_projection.entries[].outputs_effective_until_unix",
                detail: "must equal the earliest output item_effective_until".to_string(),
            });
        }
        Ok(())
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoaCacheProjection {
    #[serde(rename = "m")]
    pub manifest_rsync_uri: String,
    #[serde(rename = "e")]
    pub instance_effective_until: PackTime,
    #[serde(rename = "i")]
    pub issuer_ca_sha256_hex: Option<String>,
    #[serde(rename = "p", default, skip_serializing_if = "Option::is_none")]
    #[serde(with = "serde_optional_bytes_32")]
    pub ca_validation_context_digest: Option<[u8; 32]>,
    #[serde(rename = "f", default, skip_serializing_if = "Option::is_none")]
    #[serde(with = "serde_optional_bytes_32")]
    pub policy_fingerprint: Option<[u8; 32]>,
    #[serde(rename = "c")]
    pub crl_sha256_by_uri: Vec<RoaCacheCrlProjection>,
    #[serde(rename = "r")]
    pub entries: Vec<RoaCacheObjectProjection>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChildCertificateCacheRouterKeyProjection {
    #[serde(rename = "a")]
    pub as_id: u32,
    #[serde(rename = "s")]
    #[serde(with = "serde_byte_vec")]
    pub ski: Vec<u8>,
    #[serde(rename = "p")]
    #[serde(with = "serde_byte_vec")]
    pub spki_der: Vec<u8>,
    #[serde(rename = "e")]
    pub item_effective_until: PackTime,
}

impl ChildCertificateCacheRouterKeyProjection {
    pub fn validate_internal(&self) -> StorageResult<()> {
        if self.ski.is_empty() {
            return Err(StorageError::InvalidData {
                entity: "child_certificate_cache_projection.router_keys[].ski",
                detail: "must not be empty".to_string(),
            });
        }
        if self.spki_der.is_empty() {
            return Err(StorageError::InvalidData {
                entity: "child_certificate_cache_projection.router_keys[].spki_der",
                detail: "must not be empty".to_string(),
            });
        }
        parse_time(
            "child_certificate_cache_projection.router_keys[].item_effective_until",
            &self.item_effective_until,
        )?;
        Ok(())
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ChildCertificateCachePayload {
    ChildCa {
        #[serde(rename = "cm")]
        child_manifest_rsync_uri: String,
        #[serde(rename = "ski")]
        child_ski: String,
        #[serde(rename = "rb")]
        child_rsync_base_uri: String,
        #[serde(rename = "pp")]
        child_publication_point_rsync_uri: String,
        #[serde(rename = "rn")]
        child_rrdp_notification_uri: Option<String>,
        #[serde(rename = "ip")]
        child_effective_ip_resources: Option<IpResourceSet>,
        #[serde(rename = "as")]
        child_effective_as_resources: Option<AsResourceSet>,
    },
    Router {
        #[serde(rename = "r")]
        router_keys: Vec<ChildCertificateCacheRouterKeyProjection>,
    },
}

impl ChildCertificateCachePayload {
    pub fn validate_internal(&self) -> StorageResult<()> {
        match self {
            Self::ChildCa {
                child_manifest_rsync_uri,
                child_ski,
                child_rsync_base_uri,
                child_publication_point_rsync_uri,
                child_rrdp_notification_uri,
                ..
            } => {
                validate_non_empty(
                    "child_certificate_cache_projection.child_manifest_rsync_uri",
                    child_manifest_rsync_uri,
                )?;
                validate_non_empty("child_certificate_cache_projection.child_ski", child_ski)?;
                validate_non_empty(
                    "child_certificate_cache_projection.child_rsync_base_uri",
                    child_rsync_base_uri,
                )?;
                validate_non_empty(
                    "child_certificate_cache_projection.child_publication_point_rsync_uri",
                    child_publication_point_rsync_uri,
                )?;
                if let Some(uri) = child_rrdp_notification_uri {
                    validate_non_empty(
                        "child_certificate_cache_projection.child_rrdp_notification_uri",
                        uri,
                    )?;
                }
                Ok(())
            }
            Self::Router { router_keys } => {
                if router_keys.is_empty() {
                    return Err(StorageError::InvalidData {
                        entity: "child_certificate_cache_projection.router_keys",
                        detail: "must not be empty".to_string(),
                    });
                }
                for router_key in router_keys {
                    router_key.validate_internal()?;
                }
                Ok(())
            }
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChildCertificateCacheProjection {
    #[serde(rename = "sv")]
    pub schema_version: u32,
    #[serde(rename = "av")]
    pub algorithm_version: u32,
    #[serde(rename = "k")]
    pub cache_key_sha256_hex: String,
    #[serde(rename = "cu")]
    pub child_cert_uri: String,
    #[serde(rename = "ch")]
    pub child_cert_sha256_hex: String,
    #[serde(rename = "cs")]
    #[serde(with = "serde_byte_vec")]
    pub child_cert_serial: Vec<u8>,
    #[serde(rename = "ih")]
    pub issuer_ca_sha256_hex: String,
    #[serde(rename = "cru")]
    pub issuer_crl_uri: String,
    #[serde(rename = "crh")]
    pub issuer_crl_sha256_hex: String,
    #[serde(rename = "pc")]
    #[serde(with = "serde_bytes_32")]
    pub ca_validation_context_digest: [u8; 32],
    #[serde(rename = "pf")]
    #[serde(with = "serde_bytes_32")]
    pub validation_policy_fingerprint: [u8; 32],
    #[serde(rename = "nb")]
    pub effective_not_before: PackTime,
    #[serde(rename = "nu")]
    pub effective_until: PackTime,
    #[serde(rename = "p")]
    pub payload: ChildCertificateCachePayload,
}

pub const CHILD_CERTIFICATE_CACHE_SCHEMA_VERSION: u32 = 2;
pub const CHILD_CERTIFICATE_CACHE_ALGORITHM_VERSION: u32 = 3;

impl ChildCertificateCacheProjection {
    pub fn validate_internal(&self) -> StorageResult<()> {
        if self.schema_version != CHILD_CERTIFICATE_CACHE_SCHEMA_VERSION {
            return Err(StorageError::InvalidData {
                entity: "child_certificate_cache_projection.schema_version",
                detail: format!("unsupported schema_version {}", self.schema_version),
            });
        }
        if self.algorithm_version != CHILD_CERTIFICATE_CACHE_ALGORITHM_VERSION {
            return Err(StorageError::InvalidData {
                entity: "child_certificate_cache_projection.algorithm_version",
                detail: format!("unsupported algorithm_version {}", self.algorithm_version),
            });
        }
        validate_sha256_hex(
            "child_certificate_cache_projection.cache_key_sha256_hex",
            &self.cache_key_sha256_hex,
        )?;
        validate_non_empty(
            "child_certificate_cache_projection.child_cert_uri",
            &self.child_cert_uri,
        )?;
        validate_sha256_hex(
            "child_certificate_cache_projection.child_cert_sha256_hex",
            &self.child_cert_sha256_hex,
        )?;
        if self.child_cert_serial.is_empty() {
            return Err(StorageError::InvalidData {
                entity: "child_certificate_cache_projection.child_cert_serial",
                detail: "must not be empty".to_string(),
            });
        }
        validate_sha256_hex(
            "child_certificate_cache_projection.issuer_ca_sha256_hex",
            &self.issuer_ca_sha256_hex,
        )?;
        validate_non_empty(
            "child_certificate_cache_projection.issuer_crl_uri",
            &self.issuer_crl_uri,
        )?;
        validate_sha256_hex(
            "child_certificate_cache_projection.issuer_crl_sha256_hex",
            &self.issuer_crl_sha256_hex,
        )?;
        let effective_not_before = parse_time(
            "child_certificate_cache_projection.effective_not_before",
            &self.effective_not_before,
        )?;
        let effective_until = parse_time(
            "child_certificate_cache_projection.effective_until",
            &self.effective_until,
        )?;
        if effective_not_before >= effective_until {
            return Err(StorageError::InvalidData {
                entity: "child_certificate_cache_projection.effective_window",
                detail: "effective_not_before must be before effective_until".to_string(),
            });
        }
        self.payload.validate_internal()?;
        Ok(())
    }
}

pub const PUBLICATION_POINT_CACHE_SCHEMA_VERSION: u32 = 1;
pub const PUBLICATION_POINT_CACHE_ALGORITHM_VERSION: u32 = 1;

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PublicationPointCacheOutput {
    #[serde(rename = "t")]
    pub output_type: VcirOutputType,
    #[serde(rename = "nb")]
    pub item_effective_not_before: PackTime,
    #[serde(rename = "nu")]
    pub item_effective_until: PackTime,
    #[serde(rename = "u")]
    pub source_object_uri: String,
    #[serde(rename = "k")]
    pub source_object_type: VcirSourceObjectType,
    #[serde(rename = "h")]
    #[serde(with = "serde_bytes_32")]
    pub source_object_hash: [u8; 32],
    #[serde(rename = "c")]
    #[serde(with = "serde_bytes_32")]
    pub source_ee_cert_hash: [u8; 32],
    #[serde(rename = "p")]
    pub payload: VcirLocalOutputPayload,
    #[serde(rename = "r")]
    #[serde(with = "serde_bytes_32")]
    pub rule_hash: [u8; 32],
}

impl PublicationPointCacheOutput {
    fn from_local_output(output: &VcirLocalOutput, default_not_before: &PackTime) -> Self {
        Self {
            output_type: output.output_type,
            item_effective_not_before: default_not_before.clone(),
            item_effective_until: output.item_effective_until.clone(),
            source_object_uri: output.source_object_uri.clone(),
            source_object_type: output.source_object_type,
            source_object_hash: output.source_object_hash,
            source_ee_cert_hash: output.source_ee_cert_hash,
            payload: output.payload.clone(),
            rule_hash: output.rule_hash,
        }
    }

    pub fn to_local_output(&self) -> VcirLocalOutput {
        VcirLocalOutput {
            output_type: self.output_type,
            item_effective_until: self.item_effective_until.clone(),
            source_object_uri: self.source_object_uri.clone(),
            source_object_type: self.source_object_type,
            source_object_hash: self.source_object_hash,
            source_ee_cert_hash: self.source_ee_cert_hash,
            payload: self.payload.clone(),
            rule_hash: self.rule_hash,
        }
    }

    pub fn validate_internal(&self) -> StorageResult<()> {
        parse_time(
            "publication_point_cache_projection.outputs[].item_effective_not_before",
            &self.item_effective_not_before,
        )?;
        parse_time(
            "publication_point_cache_projection.outputs[].item_effective_until",
            &self.item_effective_until,
        )?;
        validate_non_empty(
            "publication_point_cache_projection.outputs[].source_object_uri",
            &self.source_object_uri,
        )?;
        VcirLocalOutput {
            output_type: self.output_type,
            item_effective_until: self.item_effective_until.clone(),
            source_object_uri: self.source_object_uri.clone(),
            source_object_type: self.source_object_type,
            source_object_hash: self.source_object_hash,
            source_ee_cert_hash: self.source_ee_cert_hash,
            payload: self.payload.clone(),
            rule_hash: self.rule_hash,
        }
        .validate_internal()
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PublicationPointCacheChild {
    #[serde(rename = "cm")]
    pub child_manifest_rsync_uri: String,
    #[serde(rename = "cu")]
    pub child_cert_rsync_uri: String,
    #[serde(rename = "ch")]
    pub child_cert_hash: String,
    #[serde(rename = "ski")]
    pub child_ski: String,
    #[serde(rename = "rb")]
    pub child_rsync_base_uri: String,
    #[serde(rename = "pp")]
    pub child_publication_point_rsync_uri: String,
    #[serde(rename = "rn")]
    pub child_rrdp_notification_uri: Option<String>,
    #[serde(rename = "ip")]
    pub child_effective_ip_resources: Option<IpResourceSet>,
    #[serde(rename = "as")]
    pub child_effective_as_resources: Option<AsResourceSet>,
    #[serde(rename = "nb")]
    pub child_effective_not_before: PackTime,
    #[serde(rename = "nu")]
    pub child_effective_until: PackTime,
}

impl PublicationPointCacheChild {
    fn from_child_entry(
        entry: &VcirChildEntry,
        default_not_before: &PackTime,
        default_until: &PackTime,
    ) -> Self {
        Self {
            child_manifest_rsync_uri: entry.child_manifest_rsync_uri.clone(),
            child_cert_rsync_uri: entry.child_cert_rsync_uri.clone(),
            child_cert_hash: entry.child_cert_hash.clone(),
            child_ski: entry.child_ski.clone(),
            child_rsync_base_uri: entry.child_rsync_base_uri.clone(),
            child_publication_point_rsync_uri: entry.child_publication_point_rsync_uri.clone(),
            child_rrdp_notification_uri: entry.child_rrdp_notification_uri.clone(),
            child_effective_ip_resources: entry.child_effective_ip_resources.clone(),
            child_effective_as_resources: entry.child_effective_as_resources.clone(),
            child_effective_not_before: default_not_before.clone(),
            child_effective_until: default_until.clone(),
        }
    }

    pub fn to_child_entry(&self, accepted_at_validation_time: PackTime) -> VcirChildEntry {
        VcirChildEntry {
            child_manifest_rsync_uri: self.child_manifest_rsync_uri.clone(),
            child_cert_rsync_uri: self.child_cert_rsync_uri.clone(),
            child_cert_hash: self.child_cert_hash.clone(),
            child_ski: self.child_ski.clone(),
            child_rsync_base_uri: self.child_rsync_base_uri.clone(),
            child_publication_point_rsync_uri: self.child_publication_point_rsync_uri.clone(),
            child_rrdp_notification_uri: self.child_rrdp_notification_uri.clone(),
            child_effective_ip_resources: self.child_effective_ip_resources.clone(),
            child_effective_as_resources: self.child_effective_as_resources.clone(),
            accepted_at_validation_time,
        }
    }

    pub fn validate_internal(&self) -> StorageResult<()> {
        self.to_child_entry(self.child_effective_not_before.clone())
            .validate_internal()?;
        parse_time(
            "publication_point_cache_projection.children[].child_effective_not_before",
            &self.child_effective_not_before,
        )?;
        parse_time(
            "publication_point_cache_projection.children[].child_effective_until",
            &self.child_effective_until,
        )?;
        Ok(())
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PublicationPointCacheObject {
    #[serde(rename = "r")]
    pub artifact_role: VcirArtifactRole,
    #[serde(rename = "k")]
    pub artifact_kind: VcirArtifactKind,
    #[serde(rename = "u")]
    pub uri: Option<String>,
    #[serde(rename = "h")]
    pub sha256: String,
    #[serde(rename = "t")]
    pub object_type: Option<String>,
    #[serde(rename = "s")]
    pub validation_status: VcirArtifactValidationStatus,
    /// See `VcirRelatedArtifact::reject_reason`.
    #[serde(rename = "e", default, skip_serializing_if = "Option::is_none")]
    pub reject_reason: Option<String>,
}

impl PublicationPointCacheObject {
    fn from_related_artifact(artifact: &VcirRelatedArtifact) -> Self {
        Self {
            artifact_role: artifact.artifact_role,
            artifact_kind: artifact.artifact_kind,
            uri: artifact.uri.clone(),
            sha256: artifact.sha256.clone(),
            object_type: artifact.object_type.clone(),
            validation_status: artifact.validation_status,
            reject_reason: artifact.reject_reason.clone(),
        }
    }

    pub fn to_related_artifact(&self) -> VcirRelatedArtifact {
        VcirRelatedArtifact {
            artifact_role: self.artifact_role,
            artifact_kind: self.artifact_kind,
            uri: self.uri.clone(),
            sha256: self.sha256.clone(),
            object_type: self.object_type.clone(),
            validation_status: self.validation_status,
            reject_reason: self.reject_reason.clone(),
        }
    }

    pub fn validate_internal(&self) -> StorageResult<()> {
        self.to_related_artifact().validate_internal()
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PublicationPointCacheProjection {
    #[serde(rename = "sv")]
    pub schema_version: u32,
    #[serde(rename = "av")]
    pub algorithm_version: u32,
    #[serde(rename = "m")]
    pub manifest_rsync_uri: String,
    #[serde(rename = "pp")]
    pub publication_point_rsync_uri: String,
    #[serde(rename = "cu")]
    pub ca_cert_uri: Option<String>,
    #[serde(rename = "ch")]
    #[serde(with = "serde_bytes_32")]
    pub ca_cert_sha256: [u8; 32],
    #[serde(rename = "mh")]
    #[serde(with = "serde_bytes_32")]
    pub manifest_sha256: [u8; 32],
    #[serde(rename = "tal")]
    pub tal_id: String,
    #[serde(rename = "ta")]
    #[serde(with = "serde_bytes_32")]
    pub ta_context_digest: [u8; 32],
    #[serde(rename = "pc")]
    #[serde(with = "serde_bytes_32")]
    pub ca_validation_context_digest: [u8; 32],
    #[serde(rename = "pf")]
    #[serde(with = "serde_bytes_32")]
    pub validation_policy_fingerprint: [u8; 32],
    #[serde(rename = "nb")]
    pub instance_effective_not_before: PackTime,
    #[serde(rename = "nu")]
    pub instance_effective_until: PackTime,
    #[serde(rename = "mt")]
    pub manifest_this_update: PackTime,
    #[serde(rename = "mn")]
    pub manifest_next_update: PackTime,
    #[serde(rename = "cn")]
    pub current_crl_next_update: PackTime,
    #[serde(rename = "ca")]
    pub self_ca_not_after: PackTime,
    #[serde(rename = "ccr")]
    pub ccr_manifest_projection: VcirCcrManifestProjection,
    #[serde(rename = "o")]
    pub outputs: Vec<PublicationPointCacheOutput>,
    #[serde(rename = "c")]
    pub children: Vec<PublicationPointCacheChild>,
    #[serde(rename = "ra")]
    pub related_objects: Vec<PublicationPointCacheObject>,
    #[serde(rename = "s")]
    pub summary: VcirSummary,
}

impl PublicationPointCacheProjection {
    pub fn from_vcir_with_context(
        vcir: &ValidatedCaInstanceResult,
        publication_point_rsync_uri: String,
        ca_cert_uri: Option<String>,
        ca_cert_sha256: [u8; 32],
        manifest_sha256: [u8; 32],
        ta_context_digest: [u8; 32],
        ca_validation_context_digest: [u8; 32],
        validation_policy_fingerprint: [u8; 32],
    ) -> StorageResult<Self> {
        let projection = Self {
            schema_version: PUBLICATION_POINT_CACHE_SCHEMA_VERSION,
            algorithm_version: PUBLICATION_POINT_CACHE_ALGORITHM_VERSION,
            manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
            publication_point_rsync_uri,
            ca_cert_uri,
            ca_cert_sha256,
            manifest_sha256,
            tal_id: vcir.tal_id.clone(),
            ta_context_digest,
            ca_validation_context_digest,
            validation_policy_fingerprint,
            instance_effective_not_before: vcir.last_successful_validation_time.clone(),
            instance_effective_until: vcir.instance_gate.instance_effective_until.clone(),
            manifest_this_update: vcir
                .validated_manifest_meta
                .validated_manifest_this_update
                .clone(),
            manifest_next_update: vcir.instance_gate.manifest_next_update.clone(),
            current_crl_next_update: vcir.instance_gate.current_crl_next_update.clone(),
            self_ca_not_after: vcir.instance_gate.self_ca_not_after.clone(),
            ccr_manifest_projection: vcir.ccr_manifest_projection.clone(),
            outputs: vcir
                .local_outputs
                .iter()
                .map(|output| {
                    PublicationPointCacheOutput::from_local_output(
                        output,
                        &vcir.last_successful_validation_time,
                    )
                })
                .collect(),
            children: vcir
                .child_entries
                .iter()
                .map(|child| {
                    PublicationPointCacheChild::from_child_entry(
                        child,
                        &vcir.last_successful_validation_time,
                        &vcir.instance_gate.instance_effective_until,
                    )
                })
                .collect(),
            related_objects: vcir
                .related_artifacts
                .iter()
                .map(PublicationPointCacheObject::from_related_artifact)
                .collect(),
            summary: vcir.summary.clone(),
        };
        projection.validate_internal()?;
        Ok(projection)
    }

    pub fn to_vcir_for_reuse(
        &self,
        validation_time: time::OffsetDateTime,
    ) -> ValidatedCaInstanceResult {
        let validation_time = PackTime::from_utc_offset_datetime(validation_time);
        ValidatedCaInstanceResult {
            manifest_rsync_uri: self.manifest_rsync_uri.clone(),
            parent_manifest_rsync_uri: None,
            tal_id: self.tal_id.clone(),
            ca_subject_name: String::from("publication-point-cache"),
            ca_ski: hex::encode(self.ca_cert_sha256),
            issuer_ski: hex::encode(self.ca_validation_context_digest),
            last_successful_validation_time: validation_time.clone(),
            current_manifest_rsync_uri: self.manifest_rsync_uri.clone(),
            current_crl_rsync_uri: self
                .related_objects
                .iter()
                .find(|object| object.artifact_role == VcirArtifactRole::CurrentCrl)
                .and_then(|object| object.uri.clone())
                .unwrap_or_default(),
            validated_manifest_meta: ValidatedManifestMeta {
                validated_manifest_number: self.ccr_manifest_projection.manifest_number_be.clone(),
                validated_manifest_this_update: self.manifest_this_update.clone(),
                validated_manifest_next_update: self.manifest_next_update.clone(),
            },
            ccr_manifest_projection: self.ccr_manifest_projection.clone(),
            instance_gate: VcirInstanceGate {
                manifest_next_update: self.manifest_next_update.clone(),
                current_crl_next_update: self.current_crl_next_update.clone(),
                self_ca_not_after: self.self_ca_not_after.clone(),
                instance_effective_until: self.instance_effective_until.clone(),
            },
            child_entries: self
                .children
                .iter()
                .map(|child| child.to_child_entry(validation_time.clone()))
                .collect(),
            local_outputs: self
                .outputs
                .iter()
                .map(PublicationPointCacheOutput::to_local_output)
                .collect(),
            related_artifacts: self
                .related_objects
                .iter()
                .map(PublicationPointCacheObject::to_related_artifact)
                .collect(),
            summary: self.summary.clone(),
            audit_summary: VcirAuditSummary {
                failed_fetch_eligible: false,
                last_failed_fetch_reason: None,
                warning_count: 0,
                audit_flags: vec!["publication_point_cache_projection".to_string()],
            },
        }
    }

    pub fn validate_internal(&self) -> StorageResult<()> {
        if self.schema_version != PUBLICATION_POINT_CACHE_SCHEMA_VERSION {
            return Err(StorageError::InvalidData {
                entity: "publication_point_cache_projection.schema_version",
                detail: format!("unsupported schema_version {}", self.schema_version),
            });
        }
        if self.algorithm_version != PUBLICATION_POINT_CACHE_ALGORITHM_VERSION {
            return Err(StorageError::InvalidData {
                entity: "publication_point_cache_projection.algorithm_version",
                detail: format!("unsupported algorithm_version {}", self.algorithm_version),
            });
        }
        validate_non_empty(
            "publication_point_cache_projection.manifest_rsync_uri",
            &self.manifest_rsync_uri,
        )?;
        validate_non_empty(
            "publication_point_cache_projection.publication_point_rsync_uri",
            &self.publication_point_rsync_uri,
        )?;
        if let Some(uri) = &self.ca_cert_uri {
            validate_non_empty("publication_point_cache_projection.ca_cert_uri", uri)?;
        }
        validate_non_empty("publication_point_cache_projection.tal_id", &self.tal_id)?;
        parse_time(
            "publication_point_cache_projection.instance_effective_not_before",
            &self.instance_effective_not_before,
        )?;
        parse_time(
            "publication_point_cache_projection.instance_effective_until",
            &self.instance_effective_until,
        )?;
        parse_time(
            "publication_point_cache_projection.manifest_this_update",
            &self.manifest_this_update,
        )?;
        parse_time(
            "publication_point_cache_projection.manifest_next_update",
            &self.manifest_next_update,
        )?;
        parse_time(
            "publication_point_cache_projection.current_crl_next_update",
            &self.current_crl_next_update,
        )?;
        parse_time(
            "publication_point_cache_projection.self_ca_not_after",
            &self.self_ca_not_after,
        )?;
        self.ccr_manifest_projection.validate_internal()?;
        for output in &self.outputs {
            output.validate_internal()?;
        }
        for child in &self.children {
            child.validate_internal()?;
        }
        for object in &self.related_objects {
            object.validate_internal()?;
        }
        Ok(())
    }
}

impl RoaCacheProjection {
    pub fn from_vcir(vcir: &ValidatedCaInstanceResult) -> StorageResult<Option<Self>> {
        Self::from_vcir_with_context(vcir, None)
    }

    pub fn from_vcir_with_context(
        vcir: &ValidatedCaInstanceResult,
        context: Option<&RoaCacheProjectionContext>,
    ) -> StorageResult<Option<Self>> {
        let Some(context) = context else {
            return Ok(None);
        };
        let mut issuer_ca_sha256_hex = None;
        let mut crl_sha256_by_uri = Vec::new();
        for artifact in &vcir.related_artifacts {
            if artifact.validation_status != VcirArtifactValidationStatus::Accepted {
                continue;
            }
            match (artifact.artifact_role, artifact.artifact_kind) {
                (
                    VcirArtifactRole::IssuerCert | VcirArtifactRole::TrustAnchorCert,
                    VcirArtifactKind::Cer,
                ) => {
                    issuer_ca_sha256_hex = Some(artifact.sha256.clone());
                }
                (_, VcirArtifactKind::Crl) => {
                    if let Some(uri) = artifact.uri.as_ref() {
                        crl_sha256_by_uri.push(RoaCacheCrlProjection {
                            uri: uri.clone(),
                            sha256: artifact.sha256.clone(),
                        });
                    }
                }
                _ => {}
            }
        }
        crl_sha256_by_uri.sort_by(|left, right| left.uri.cmp(&right.uri));

        let meta_by_uri = context
            .object_meta
            .iter()
            .map(|meta| (meta.source_object_uri.as_str(), meta))
            .collect::<HashMap<_, _>>();
        let vcir_validation_time =
            vcir.last_successful_validation_time
                .parse()
                .map_err(|detail| StorageError::InvalidData {
                    entity: "roa_cache_projection.entries[].earliest_safe_reuse_time_unix",
                    detail,
                })?;
        let mut entries: Vec<RoaCacheObjectProjection> = Vec::new();
        let mut entry_index_by_uri: HashMap<String, usize> = HashMap::new();
        for output in &vcir.local_outputs {
            let Some(projected_output) = RoaCacheLocalOutputProjection::from_local_output(output)
            else {
                continue;
            };
            let projected_output_effective_until = projected_output
                .item_effective_until
                .parse()
                .map(|time| time.unix_timestamp())
                .map_err(|detail| StorageError::InvalidData {
                    entity: "roa_cache_projection.entries[].outputs_effective_until_unix",
                    detail,
                })?;
            let Some(meta) = meta_by_uri.get(output.source_object_uri.as_str()).copied() else {
                continue;
            };
            if meta.source_object_hash != output.source_object_hash {
                return Err(StorageError::InvalidData {
                    entity: "roa_cache_projection.entries[]",
                    detail: format!(
                        "metadata source object hash mismatch for {}",
                        output.source_object_uri
                    ),
                });
            }
            let earliest_safe_reuse_time_unix = meta
                .earliest_safe_reuse_time
                .parse()
                .map(|time| time.max(vcir_validation_time).unix_timestamp())
                .map_err(|detail| StorageError::InvalidData {
                    entity: "roa_cache_projection.entries[].earliest_safe_reuse_time_unix",
                    detail,
                })?;
            if let Some(entry_index) = entry_index_by_uri.get(output.source_object_uri.as_str()) {
                let entry = &mut entries[*entry_index];
                if entry.source_object_hash != output.source_object_hash {
                    return Err(StorageError::InvalidData {
                        entity: "roa_cache_projection.entries[]",
                        detail: format!(
                            "source object hash mismatch for {}",
                            output.source_object_uri
                        ),
                    });
                }
                entry.outputs_effective_until_unix = entry
                    .outputs_effective_until_unix
                    .min(projected_output_effective_until);
                entry.outputs.push(projected_output);
            } else {
                entry_index_by_uri.insert(output.source_object_uri.clone(), entries.len());
                entries.push(RoaCacheObjectProjection {
                    source_object_uri: output.source_object_uri.clone(),
                    source_object_hash: output.source_object_hash,
                    ee_serial: Some(meta.ee_serial.clone()),
                    crl_uri: Some(meta.crl_uri.clone()),
                    earliest_safe_reuse_time_unix: Some(earliest_safe_reuse_time_unix),
                    outputs_effective_until_unix: projected_output_effective_until,
                    outputs: vec![projected_output],
                });
            }
        }
        if entries.is_empty() {
            return Ok(None);
        }
        entries.sort_by(|left, right| left.source_object_uri.cmp(&right.source_object_uri));

        let projection = Self {
            manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
            instance_effective_until: vcir.instance_gate.instance_effective_until.clone(),
            issuer_ca_sha256_hex,
            ca_validation_context_digest: Some(context.ca_validation_context_digest),
            policy_fingerprint: Some(context.policy_fingerprint),
            crl_sha256_by_uri,
            entries,
        };
        projection.validate_internal()?;
        Ok(Some(projection))
    }

    pub fn validate_internal(&self) -> StorageResult<()> {
        validate_non_empty(
            "roa_cache_projection.manifest_rsync_uri",
            &self.manifest_rsync_uri,
        )?;
        parse_time(
            "roa_cache_projection.instance_effective_until",
            &self.instance_effective_until,
        )?;
        if let Some(hash) = &self.issuer_ca_sha256_hex {
            validate_sha256_hex("roa_cache_projection.issuer_ca_sha256_hex", hash)?;
        }
        if self.ca_validation_context_digest.is_some() != self.policy_fingerprint.is_some() {
            return Err(StorageError::InvalidData {
                entity: "roa_cache_projection.context",
                detail: "ca_validation_context_digest and policy_fingerprint must be both present or both absent"
                    .to_string(),
            });
        }
        let mut seen_crls = HashSet::with_capacity(self.crl_sha256_by_uri.len());
        for crl in &self.crl_sha256_by_uri {
            crl.validate_internal()?;
            if !seen_crls.insert(crl.uri.as_str()) {
                return Err(StorageError::InvalidData {
                    entity: "roa_cache_projection.crls[]",
                    detail: format!("duplicate CRL URI: {}", crl.uri),
                });
            }
        }
        if self.entries.is_empty() {
            return Err(StorageError::InvalidData {
                entity: "roa_cache_projection.entries",
                detail: "must not be empty".to_string(),
            });
        }
        let mut seen_entries = HashSet::with_capacity(self.entries.len());
        for entry in &self.entries {
            entry.validate_internal()?;
            if !seen_entries.insert(entry.source_object_uri.as_str()) {
                return Err(StorageError::InvalidData {
                    entity: "roa_cache_projection.entries[]",
                    detail: format!("duplicate ROA URI: {}", entry.source_object_uri),
                });
            }
        }
        Ok(())
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VcirArtifactRole {
    Manifest,
    CurrentCrl,
    ChildCaCert,
    SignedObject,
    EeCert,
    IssuerCert,
    Tal,
    TrustAnchorCert,
    Other,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VcirArtifactKind {
    Cer,
    Crl,
    Mft,
    Roa,
    Aspa,
    Gbr,
    Tal,
    Other,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VcirArtifactValidationStatus {
    Accepted,
    Rejected,
    WarningOnly,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VcirRelatedArtifact {
    #[serde(rename = "r")]
    pub artifact_role: VcirArtifactRole,
    #[serde(rename = "k")]
    pub artifact_kind: VcirArtifactKind,
    #[serde(rename = "u")]
    pub uri: Option<String>,
    #[serde(rename = "h")]
    pub sha256: String,
    #[serde(rename = "t")]
    pub object_type: Option<String>,
    #[serde(rename = "s")]
    pub validation_status: VcirArtifactValidationStatus,
    /// Reject reason captured at fresh validation time for `Rejected` artifacts.
    /// `None` for accepted/warning artifacts and for cache entries written
    /// before this field existed.
    #[serde(rename = "e", default, skip_serializing_if = "Option::is_none")]
    pub reject_reason: Option<String>,
}

impl VcirRelatedArtifact {
    pub fn validate_internal(&self) -> StorageResult<()> {
        if let Some(uri) = &self.uri {
            validate_non_empty("vcir.related_artifacts[].uri", uri)?;
        }
        validate_sha256_hex("vcir.related_artifacts[].sha256", &self.sha256)?;
        Ok(())
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VcirSummary {
    #[serde(rename = "v")]
    pub local_vrp_count: u32,
    #[serde(rename = "a")]
    pub local_aspa_count: u32,
    #[serde(rename = "r")]
    pub local_router_key_count: u32,
    #[serde(rename = "c")]
    pub child_count: u32,
    #[serde(rename = "o")]
    pub accepted_object_count: u32,
    #[serde(rename = "x")]
    pub rejected_object_count: u32,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VcirAuditSummary {
    #[serde(rename = "f")]
    pub failed_fetch_eligible: bool,
    #[serde(rename = "r")]
    pub last_failed_fetch_reason: Option<String>,
    #[serde(rename = "w")]
    pub warning_count: u32,
    #[serde(rename = "a")]
    pub audit_flags: Vec<String>,
}

impl VcirAuditSummary {
    pub fn validate_internal(&self) -> StorageResult<()> {
        if let Some(reason) = &self.last_failed_fetch_reason {
            validate_non_empty("vcir.audit_summary.last_failed_fetch_reason", reason)?;
        }
        for flag in &self.audit_flags {
            validate_non_empty("vcir.audit_summary.audit_flags[]", flag)?;
        }
        Ok(())
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ValidatedCaInstanceResult {
    #[serde(rename = "m")]
    pub manifest_rsync_uri: String,
    #[serde(rename = "pm")]
    pub parent_manifest_rsync_uri: Option<String>,
    #[serde(rename = "tal")]
    pub tal_id: String,
    #[serde(rename = "subj")]
    pub ca_subject_name: String,
    #[serde(rename = "ski")]
    pub ca_ski: String,
    #[serde(rename = "aki")]
    pub issuer_ski: String,
    #[serde(rename = "vt")]
    pub last_successful_validation_time: PackTime,
    #[serde(rename = "cm")]
    pub current_manifest_rsync_uri: String,
    #[serde(rename = "crl")]
    pub current_crl_rsync_uri: String,
    #[serde(rename = "mm")]
    pub validated_manifest_meta: ValidatedManifestMeta,
    #[serde(rename = "ccr")]
    pub ccr_manifest_projection: VcirCcrManifestProjection,
    #[serde(rename = "g")]
    pub instance_gate: VcirInstanceGate,
    #[serde(rename = "ch")]
    pub child_entries: Vec<VcirChildEntry>,
    #[serde(rename = "lo")]
    pub local_outputs: Vec<VcirLocalOutput>,
    #[serde(rename = "ra")]
    pub related_artifacts: Vec<VcirRelatedArtifact>,
    #[serde(rename = "s")]
    pub summary: VcirSummary,
    #[serde(rename = "as")]
    pub audit_summary: VcirAuditSummary,
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct VcirFieldSizeBreakdown {
    pub local_output_count: u64,
    pub local_output_source_uri_bytes: u64,
    pub local_output_source_type_bytes: u64,
    pub local_output_source_hash_hex_bytes: u64,
    pub local_output_source_ee_hash_hex_bytes: u64,
    pub local_output_payload_json_bytes: u64,
    pub local_output_rule_hash_hex_bytes: u64,
    pub local_output_source_hash_binary_bytes: u64,
    pub local_output_source_ee_hash_binary_bytes: u64,
    pub local_output_payload_typed_body_bytes: u64,
    pub local_output_rule_hash_binary_bytes: u64,
    pub related_artifact_count: u64,
    pub related_artifact_uri_bytes: u64,
    pub related_artifact_hash_hex_bytes: u64,
    pub related_artifact_type_bytes: u64,
    pub child_entry_count: u64,
    pub child_entry_uri_bytes: u64,
    pub child_entry_hash_hex_bytes: u64,
}

fn serialized_cbor_len<T: Serialize>(value: &T) -> u64 {
    serde_cbor::to_vec(value)
        .map(|bytes| bytes.len() as u64)
        .unwrap_or(0)
}

impl VcirFieldSizeBreakdown {
    pub fn from_vcir(vcir: &ValidatedCaInstanceResult) -> Self {
        let mut out = Self::default();
        out.local_output_count = vcir.local_outputs.len() as u64;
        for local in &vcir.local_outputs {
            out.local_output_source_uri_bytes += local.source_object_uri.len() as u64;
            out.local_output_source_type_bytes += local.source_object_type_name().len() as u64;
            out.local_output_source_hash_hex_bytes += 64;
            out.local_output_source_ee_hash_hex_bytes += 64;
            out.local_output_payload_json_bytes += local.payload_json().len() as u64;
            out.local_output_rule_hash_hex_bytes += 64;
            out.local_output_source_hash_binary_bytes += 32;
            out.local_output_source_ee_hash_binary_bytes += 32;
            out.local_output_payload_typed_body_bytes += local.payload.typed_body_bytes();
            out.local_output_rule_hash_binary_bytes += 32;
        }
        out.related_artifact_count = vcir.related_artifacts.len() as u64;
        for artifact in &vcir.related_artifacts {
            if let Some(uri) = &artifact.uri {
                out.related_artifact_uri_bytes += uri.len() as u64;
            }
            out.related_artifact_hash_hex_bytes += artifact.sha256.len() as u64;
            if let Some(object_type) = &artifact.object_type {
                out.related_artifact_type_bytes += object_type.len() as u64;
            }
        }
        out.child_entry_count = vcir.child_entries.len() as u64;
        for child in &vcir.child_entries {
            out.child_entry_uri_bytes += child.child_manifest_rsync_uri.len() as u64
                + child.child_cert_rsync_uri.len() as u64
                + child.child_rsync_base_uri.len() as u64
                + child.child_publication_point_rsync_uri.len() as u64
                + child
                    .child_rrdp_notification_uri
                    .as_ref()
                    .map(|uri| uri.len() as u64)
                    .unwrap_or(0);
            out.child_entry_hash_hex_bytes +=
                child.child_cert_hash.len() as u64 + child.child_ski.len() as u64;
        }
        out
    }

    pub fn add_assign(&mut self, other: &Self) {
        self.local_output_count += other.local_output_count;
        self.local_output_source_uri_bytes += other.local_output_source_uri_bytes;
        self.local_output_source_type_bytes += other.local_output_source_type_bytes;
        self.local_output_source_hash_hex_bytes += other.local_output_source_hash_hex_bytes;
        self.local_output_source_ee_hash_hex_bytes += other.local_output_source_ee_hash_hex_bytes;
        self.local_output_payload_json_bytes += other.local_output_payload_json_bytes;
        self.local_output_rule_hash_hex_bytes += other.local_output_rule_hash_hex_bytes;
        self.local_output_source_hash_binary_bytes += other.local_output_source_hash_binary_bytes;
        self.local_output_source_ee_hash_binary_bytes +=
            other.local_output_source_ee_hash_binary_bytes;
        self.local_output_payload_typed_body_bytes += other.local_output_payload_typed_body_bytes;
        self.local_output_rule_hash_binary_bytes += other.local_output_rule_hash_binary_bytes;
        self.related_artifact_count += other.related_artifact_count;
        self.related_artifact_uri_bytes += other.related_artifact_uri_bytes;
        self.related_artifact_hash_hex_bytes += other.related_artifact_hash_hex_bytes;
        self.related_artifact_type_bytes += other.related_artifact_type_bytes;
        self.child_entry_count += other.child_entry_count;
        self.child_entry_uri_bytes += other.child_entry_uri_bytes;
        self.child_entry_hash_hex_bytes += other.child_entry_hash_hex_bytes;
    }

    pub fn local_output_old_projection_bytes(&self) -> u64 {
        self.local_output_source_type_bytes
            + self.local_output_source_hash_hex_bytes
            + self.local_output_source_ee_hash_hex_bytes
            + self.local_output_payload_json_bytes
            + self.local_output_rule_hash_hex_bytes
    }

    pub fn local_output_typed_projection_bytes(&self) -> u64 {
        self.local_output_count
            + self.local_output_source_hash_binary_bytes
            + self.local_output_source_ee_hash_binary_bytes
            + self.local_output_payload_typed_body_bytes
            + self.local_output_rule_hash_binary_bytes
    }

    pub fn local_output_projection_saved_bytes(&self) -> u64 {
        self.local_output_old_projection_bytes()
            .saturating_sub(self.local_output_typed_projection_bytes())
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct VcirReplaceTimingBreakdown {
    pub validate_ms: u64,
    pub vcir_encode_ms: u64,
    pub vcir_value_bytes: u64,
    pub replay_meta_encode_ms: u64,
    pub replay_meta_value_bytes: u64,
    pub roa_cache_projection_encode_ms: u64,
    pub roa_cache_projection_value_bytes: u64,
    pub publication_point_cache_projection_encode_ms: u64,
    pub publication_point_cache_projection_value_bytes: u64,
    pub batch_build_ms: u64,
    pub write_batch_ms: u64,
    pub total_encoded_bytes: u64,
    pub field_sizes: VcirFieldSizeBreakdown,
    pub rss_before_kb: Option<u64>,
    pub rss_after_validate_kb: Option<u64>,
    pub rss_after_vcir_encode_kb: Option<u64>,
    pub rss_after_replay_meta_encode_kb: Option<u64>,
    pub rss_after_roa_cache_projection_encode_kb: Option<u64>,
    pub rss_after_publication_point_cache_projection_encode_kb: Option<u64>,
    pub rss_after_write_batch_kb: Option<u64>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct VcirStorageSummary {
    pub entry_count: u64,
    pub vcir_value_bytes: u64,
    pub vcir_value_bytes_max: u64,
    pub vcir_value_bytes_max_manifest_rsync_uri: Option<String>,
    pub core_fields: VcirCoreFieldSizeBreakdown,
    pub ccr_projection: VcirCcrProjectionSizeBreakdown,
    pub child_resources: VcirChildResourceSizeBreakdown,
    pub field_sizes: VcirFieldSizeBreakdown,
    pub local_output_old_projection_bytes: u64,
    pub local_output_typed_projection_bytes: u64,
    pub local_output_projection_saved_bytes: u64,
    pub top_entries_by_vcir_value_bytes: Vec<VcirStorageEntrySummary>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct VcirStorageEntrySummary {
    pub manifest_rsync_uri: String,
    pub vcir_value_bytes: u64,
    pub local_vrp_count: u32,
    pub local_aspa_count: u32,
    pub local_router_key_count: u32,
    pub accepted_object_count: u32,
    pub rejected_object_count: u32,
    pub child_count: u32,
    pub core_fields: VcirCoreFieldSizeBreakdown,
    pub ccr_projection: VcirCcrProjectionSizeBreakdown,
    pub child_resources: VcirChildResourceSizeBreakdown,
    pub field_sizes: VcirFieldSizeBreakdown,
    pub local_output_old_projection_bytes: u64,
    pub local_output_typed_projection_bytes: u64,
    pub local_output_projection_saved_bytes: u64,
}

impl VcirStorageEntrySummary {
    fn from_vcir(vcir: &ValidatedCaInstanceResult, vcir_value_bytes: u64) -> Self {
        let field_sizes = VcirFieldSizeBreakdown::from_vcir(vcir);
        Self {
            manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
            vcir_value_bytes,
            local_vrp_count: vcir.summary.local_vrp_count,
            local_aspa_count: vcir.summary.local_aspa_count,
            local_router_key_count: vcir.summary.local_router_key_count,
            accepted_object_count: vcir.summary.accepted_object_count,
            rejected_object_count: vcir.summary.rejected_object_count,
            child_count: vcir.summary.child_count,
            core_fields: VcirCoreFieldSizeBreakdown::from_vcir(vcir),
            ccr_projection: VcirCcrProjectionSizeBreakdown::from_projection(
                &vcir.ccr_manifest_projection,
            ),
            child_resources: VcirChildResourceSizeBreakdown::from_vcir(vcir),
            local_output_old_projection_bytes: field_sizes.local_output_old_projection_bytes(),
            local_output_typed_projection_bytes: field_sizes.local_output_typed_projection_bytes(),
            local_output_projection_saved_bytes: field_sizes.local_output_projection_saved_bytes(),
            field_sizes,
        }
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct VcirCoreFieldSizeBreakdown {
    pub manifest_rsync_uri_bytes: u64,
    pub parent_manifest_rsync_uri_bytes: u64,
    pub tal_id_bytes: u64,
    pub ca_subject_name_bytes: u64,
    pub ca_ski_bytes: u64,
    pub issuer_ski_bytes: u64,
    pub current_manifest_rsync_uri_bytes: u64,
    pub current_crl_rsync_uri_bytes: u64,
    pub validated_manifest_number_bytes: u64,
}

impl VcirCoreFieldSizeBreakdown {
    fn from_vcir(vcir: &ValidatedCaInstanceResult) -> Self {
        Self {
            manifest_rsync_uri_bytes: vcir.manifest_rsync_uri.len() as u64,
            parent_manifest_rsync_uri_bytes: vcir
                .parent_manifest_rsync_uri
                .as_ref()
                .map(|uri| uri.len() as u64)
                .unwrap_or(0),
            tal_id_bytes: vcir.tal_id.len() as u64,
            ca_subject_name_bytes: vcir.ca_subject_name.len() as u64,
            ca_ski_bytes: vcir.ca_ski.len() as u64,
            issuer_ski_bytes: vcir.issuer_ski.len() as u64,
            current_manifest_rsync_uri_bytes: vcir.current_manifest_rsync_uri.len() as u64,
            current_crl_rsync_uri_bytes: vcir.current_crl_rsync_uri.len() as u64,
            validated_manifest_number_bytes: vcir
                .validated_manifest_meta
                .validated_manifest_number
                .len() as u64,
        }
    }

    fn add_assign(&mut self, other: &Self) {
        self.manifest_rsync_uri_bytes += other.manifest_rsync_uri_bytes;
        self.parent_manifest_rsync_uri_bytes += other.parent_manifest_rsync_uri_bytes;
        self.tal_id_bytes += other.tal_id_bytes;
        self.ca_subject_name_bytes += other.ca_subject_name_bytes;
        self.ca_ski_bytes += other.ca_ski_bytes;
        self.issuer_ski_bytes += other.issuer_ski_bytes;
        self.current_manifest_rsync_uri_bytes += other.current_manifest_rsync_uri_bytes;
        self.current_crl_rsync_uri_bytes += other.current_crl_rsync_uri_bytes;
        self.validated_manifest_number_bytes += other.validated_manifest_number_bytes;
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct VcirChildResourceSizeBreakdown {
    pub effective_ip_resource_cbor_bytes: u64,
    pub effective_as_resource_cbor_bytes: u64,
}

impl VcirChildResourceSizeBreakdown {
    fn from_vcir(vcir: &ValidatedCaInstanceResult) -> Self {
        let mut out = Self::default();
        for child in &vcir.child_entries {
            out.effective_ip_resource_cbor_bytes +=
                serialized_cbor_len(&child.child_effective_ip_resources);
            out.effective_as_resource_cbor_bytes +=
                serialized_cbor_len(&child.child_effective_as_resources);
        }
        out
    }

    fn add_assign(&mut self, other: &Self) {
        self.effective_ip_resource_cbor_bytes += other.effective_ip_resource_cbor_bytes;
        self.effective_as_resource_cbor_bytes += other.effective_as_resource_cbor_bytes;
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct VcirCcrProjectionSizeBreakdown {
    pub manifest_rsync_uri_bytes: u64,
    pub manifest_sha256_bytes: u64,
    pub manifest_ee_aki_bytes: u64,
    pub manifest_number_bytes: u64,
    pub manifest_sia_locations_count: u64,
    pub manifest_sia_locations_der_bytes: u64,
    pub subordinate_ski_count: u64,
    pub subordinate_ski_bytes: u64,
}

impl VcirCcrProjectionSizeBreakdown {
    fn from_projection(projection: &VcirCcrManifestProjection) -> Self {
        Self {
            manifest_rsync_uri_bytes: projection.manifest_rsync_uri.len() as u64,
            manifest_sha256_bytes: projection.manifest_sha256.len() as u64,
            manifest_ee_aki_bytes: projection.manifest_ee_aki.len() as u64,
            manifest_number_bytes: projection.manifest_number_be.len() as u64,
            manifest_sia_locations_count: projection.manifest_sia_locations_der.len() as u64,
            manifest_sia_locations_der_bytes: projection
                .manifest_sia_locations_der
                .iter()
                .map(|location| location.len() as u64)
                .sum(),
            subordinate_ski_count: projection.subordinate_skis.len() as u64,
            subordinate_ski_bytes: projection
                .subordinate_skis
                .iter()
                .map(|ski| ski.len() as u64)
                .sum(),
        }
    }

    fn add_assign(&mut self, other: &Self) {
        self.manifest_rsync_uri_bytes += other.manifest_rsync_uri_bytes;
        self.manifest_sha256_bytes += other.manifest_sha256_bytes;
        self.manifest_ee_aki_bytes += other.manifest_ee_aki_bytes;
        self.manifest_number_bytes += other.manifest_number_bytes;
        self.manifest_sia_locations_count += other.manifest_sia_locations_count;
        self.manifest_sia_locations_der_bytes += other.manifest_sia_locations_der_bytes;
        self.subordinate_ski_count += other.subordinate_ski_count;
        self.subordinate_ski_bytes += other.subordinate_ski_bytes;
    }
}

fn push_top_vcir_storage_entry(
    entries: &mut Vec<VcirStorageEntrySummary>,
    entry: VcirStorageEntrySummary,
) {
    const TOP_N: usize = 20;
    entries.push(entry);
    entries.sort_by(|left, right| {
        right
            .vcir_value_bytes
            .cmp(&left.vcir_value_bytes)
            .then_with(|| left.manifest_rsync_uri.cmp(&right.manifest_rsync_uri))
    });
    entries.truncate(TOP_N);
}

impl ValidatedCaInstanceResult {
    pub fn validate_internal(&self) -> StorageResult<()> {
        validate_non_empty("vcir.manifest_rsync_uri", &self.manifest_rsync_uri)?;
        if let Some(parent_manifest_rsync_uri) = &self.parent_manifest_rsync_uri {
            validate_non_empty("vcir.parent_manifest_rsync_uri", parent_manifest_rsync_uri)?;
        }
        validate_non_empty("vcir.tal_id", &self.tal_id)?;
        validate_non_empty("vcir.ca_subject_name", &self.ca_subject_name)?;
        validate_non_empty("vcir.ca_ski", &self.ca_ski)?;
        validate_non_empty("vcir.issuer_ski", &self.issuer_ski)?;
        parse_time(
            "vcir.last_successful_validation_time",
            &self.last_successful_validation_time,
        )?;
        validate_non_empty(
            "vcir.current_manifest_rsync_uri",
            &self.current_manifest_rsync_uri,
        )?;
        validate_non_empty("vcir.current_crl_rsync_uri", &self.current_crl_rsync_uri)?;
        self.validated_manifest_meta.validate_internal()?;
        self.ccr_manifest_projection.validate_internal()?;
        self.instance_gate.validate_internal()?;

        let expected_manifest_next = self
            .validated_manifest_meta
            .validated_manifest_next_update
            .parse()
            .map_err(|detail| StorageError::InvalidData {
                entity: "vcir",
                detail: format!(
                    "validated_manifest_meta.validated_manifest_next_update invalid: {detail}"
                ),
            })?;
        let instance_manifest_next =
            self.instance_gate
                .manifest_next_update
                .parse()
                .map_err(|detail| StorageError::InvalidData {
                    entity: "vcir",
                    detail: format!("instance_gate.manifest_next_update invalid: {detail}"),
                })?;
        if expected_manifest_next != instance_manifest_next {
            return Err(StorageError::InvalidData {
                entity: "vcir",
                detail: "instance_gate.manifest_next_update must equal validated_manifest_meta.validated_manifest_next_update".to_string(),
            });
        }

        let mut child_manifests = HashSet::with_capacity(self.child_entries.len());
        for child in &self.child_entries {
            child.validate_internal()?;
            if !child_manifests.insert(child.child_manifest_rsync_uri.as_str()) {
                return Err(StorageError::InvalidData {
                    entity: "vcir",
                    detail: format!(
                        "duplicate child_manifest_rsync_uri: {}",
                        child.child_manifest_rsync_uri
                    ),
                });
            }
        }

        let mut vrp_count = 0u32;
        let mut aspa_count = 0u32;
        let mut router_key_count = 0u32;
        for output in &self.local_outputs {
            output.validate_internal()?;
            match output.output_type {
                VcirOutputType::Vrp => vrp_count += 1,
                VcirOutputType::Aspa => aspa_count += 1,
                VcirOutputType::RouterKey => router_key_count += 1,
            }
        }
        let mut output_ids = self
            .local_outputs
            .iter()
            .map(VcirLocalOutput::output_id)
            .collect::<Vec<_>>();
        output_ids.sort_unstable();
        if let Some(duplicate) = output_ids
            .windows(2)
            .find_map(|pair| (pair[0] == pair[1]).then(|| pair[0].clone()))
        {
            return Err(StorageError::InvalidData {
                entity: "vcir",
                detail: format!("duplicate output_id: {duplicate}"),
            });
        }
        if self.summary.local_vrp_count != vrp_count {
            return Err(StorageError::InvalidData {
                entity: "vcir.summary",
                detail: format!(
                    "local_vrp_count={} does not match local_outputs count {}",
                    self.summary.local_vrp_count, vrp_count
                ),
            });
        }
        if self.summary.local_aspa_count != aspa_count {
            return Err(StorageError::InvalidData {
                entity: "vcir.summary",
                detail: format!(
                    "local_aspa_count={} does not match local_outputs count {}",
                    self.summary.local_aspa_count, aspa_count
                ),
            });
        }
        if self.summary.local_router_key_count != router_key_count {
            return Err(StorageError::InvalidData {
                entity: "vcir.summary",
                detail: format!(
                    "local_router_key_count={} does not match local_outputs count {}",
                    self.summary.local_router_key_count, router_key_count
                ),
            });
        }
        if self.summary.child_count != self.child_entries.len() as u32 {
            return Err(StorageError::InvalidData {
                entity: "vcir.summary",
                detail: format!(
                    "child_count={} does not match child_entries length {}",
                    self.summary.child_count,
                    self.child_entries.len()
                ),
            });
        }

        for artifact in &self.related_artifacts {
            artifact.validate_internal()?;
        }
        self.audit_summary.validate_internal()?;
        Ok(())
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RrdpSourceSyncState {
    Empty,
    SnapshotOnly,
    DeltaReady,
    Error,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RrdpSourceRecord {
    pub notify_uri: String,
    pub last_session_id: Option<String>,
    pub last_serial: Option<u64>,
    pub first_seen_at: PackTime,
    pub last_seen_at: PackTime,
    pub last_sync_at: Option<PackTime>,
    pub sync_state: RrdpSourceSyncState,
    pub last_snapshot_uri: Option<String>,
    pub last_snapshot_hash: Option<String>,
    pub last_error: Option<String>,
}

impl RrdpSourceRecord {
    pub fn validate_internal(&self) -> StorageResult<()> {
        validate_non_empty("rrdp_source.notify_uri", &self.notify_uri)?;
        if let Some(session_id) = &self.last_session_id {
            validate_non_empty("rrdp_source.last_session_id", session_id)?;
        }
        parse_time("rrdp_source.first_seen_at", &self.first_seen_at)?;
        parse_time("rrdp_source.last_seen_at", &self.last_seen_at)?;
        if let Some(last_sync_at) = &self.last_sync_at {
            parse_time("rrdp_source.last_sync_at", last_sync_at)?;
        }
        if let Some(last_snapshot_uri) = &self.last_snapshot_uri {
            validate_non_empty("rrdp_source.last_snapshot_uri", last_snapshot_uri)?;
        }
        if let Some(last_snapshot_hash) = &self.last_snapshot_hash {
            validate_sha256_hex("rrdp_source.last_snapshot_hash", last_snapshot_hash)?;
        }
        if let Some(last_error) = &self.last_error {
            validate_non_empty("rrdp_source.last_error", last_error)?;
        }
        Ok(())
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RrdpSourceMemberRecord {
    pub notify_uri: String,
    pub rsync_uri: String,
    pub current_hash: Option<String>,
    pub object_type: Option<String>,
    pub present: bool,
    pub last_confirmed_session_id: String,
    pub last_confirmed_serial: u64,
    pub last_changed_at: PackTime,
}

impl RrdpSourceMemberRecord {
    pub fn validate_internal(&self) -> StorageResult<()> {
        validate_non_empty("rrdp_source_member.notify_uri", &self.notify_uri)?;
        validate_non_empty("rrdp_source_member.rsync_uri", &self.rsync_uri)?;
        validate_non_empty(
            "rrdp_source_member.last_confirmed_session_id",
            &self.last_confirmed_session_id,
        )?;
        if self.present {
            let hash = self
                .current_hash
                .as_deref()
                .ok_or(StorageError::InvalidData {
                    entity: "rrdp_source_member",
                    detail: "current_hash is required when present=true".to_string(),
                })?;
            validate_sha256_hex("rrdp_source_member.current_hash", hash)?;
        } else if let Some(hash) = &self.current_hash {
            validate_sha256_hex("rrdp_source_member.current_hash", hash)?;
        }
        parse_time("rrdp_source_member.last_changed_at", &self.last_changed_at)?;
        Ok(())
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RrdpUriOwnerState {
    Active,
    Conflict,
    Withdrawn,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RrdpUriOwnerRecord {
    pub rsync_uri: String,
    pub notify_uri: String,
    pub current_hash: Option<String>,
    pub last_confirmed_session_id: String,
    pub last_confirmed_serial: u64,
    pub last_changed_at: PackTime,
    pub owner_state: RrdpUriOwnerState,
}

impl RrdpUriOwnerRecord {
    pub fn validate_internal(&self) -> StorageResult<()> {
        validate_non_empty("rrdp_uri_owner.rsync_uri", &self.rsync_uri)?;
        validate_non_empty("rrdp_uri_owner.notify_uri", &self.notify_uri)?;
        validate_non_empty(
            "rrdp_uri_owner.last_confirmed_session_id",
            &self.last_confirmed_session_id,
        )?;
        if let Some(hash) = &self.current_hash {
            validate_sha256_hex("rrdp_uri_owner.current_hash", hash)?;
        }
        parse_time("rrdp_uri_owner.last_changed_at", &self.last_changed_at)?;
        Ok(())
    }
}

fn write_roa_cache_projection_to_batch(
    projection_cf: &ColumnFamily,
    batch: &mut WriteBatch,
    vcir: &ValidatedCaInstanceResult,
    context: Option<&RoaCacheProjectionContext>,
    timing: Option<&mut VcirReplaceTimingBreakdown>,
) -> StorageResult<()> {
    let projection_key = roa_cache_projection_key(&vcir.manifest_rsync_uri);
    let projection = RoaCacheProjection::from_vcir_with_context(vcir, context)?;
    match projection {
        Some(projection) => {
            let projection_value = encode_cbor(&projection, "roa_cache_projection")?;
            if let Some(timing) = timing {
                timing.roa_cache_projection_value_bytes = projection_value.len() as u64;
            }
            batch.put_cf(projection_cf, projection_key.as_bytes(), projection_value);
        }
        None => {
            batch.delete_cf(projection_cf, projection_key.as_bytes());
        }
    }
    Ok(())
}

fn write_vcir_failed_fetch_reuse_identity_to_batch(
    failed_fetch_reuse_identity_cf: &ColumnFamily,
    batch: &mut WriteBatch,
    manifest_rsync_uri: &str,
    failed_fetch_reuse_identity: Option<&VcirFailedFetchReuseIdentity>,
) -> StorageResult<()> {
    let key = vcir_failed_fetch_reuse_identity_key(manifest_rsync_uri);
    match failed_fetch_reuse_identity {
        Some(identity) => {
            identity.validate_internal()?;
            let value = encode_cbor(identity, "vcir_failed_fetch_reuse_identity")?;
            batch.put_cf(failed_fetch_reuse_identity_cf, key.as_bytes(), value);
        }
        None => batch.delete_cf(failed_fetch_reuse_identity_cf, key.as_bytes()),
    }
    Ok(())
}

fn write_publication_point_cache_projection_to_batch(
    projection_cf: &ColumnFamily,
    batch: &mut WriteBatch,
    action: PublicationPointCacheProjectionWriteAction<'_>,
    timing: Option<&mut VcirReplaceTimingBreakdown>,
) -> StorageResult<()> {
    match action {
        PublicationPointCacheProjectionWriteAction::Keep => Ok(()),
        PublicationPointCacheProjectionWriteAction::Write(projection) => {
            projection.validate_internal()?;
            let key = publication_point_cache_projection_key(&projection.manifest_rsync_uri);
            let value = encode_cbor(projection, "publication_point_cache_projection")?;
            if let Some(timing) = timing {
                timing.publication_point_cache_projection_value_bytes = value.len() as u64;
            }
            batch.put_cf(projection_cf, key.as_bytes(), value);
            Ok(())
        }
        PublicationPointCacheProjectionWriteAction::Delete { manifest_rsync_uri } => {
            let key = publication_point_cache_projection_key(manifest_rsync_uri);
            batch.delete_cf(projection_cf, key.as_bytes());
            Ok(())
        }
    }
}

impl RocksStore {
    pub fn create_read_only_checkpoint(source: &Path, destination: &Path) -> StorageResult<()> {
        if destination.exists() {
            return Err(StorageError::InvalidData {
                entity: "work_db_checkpoint",
                detail: format!(
                    "checkpoint destination already exists: {}",
                    destination.display()
                ),
            });
        }
        if let Some(parent) = destination.parent() {
            std::fs::create_dir_all(parent)
                .map_err(|error| StorageError::RocksDb(error.to_string()))?;
        }
        let mut options = Options::default();
        let blob_mode = work_db_blob_mode_from_env();
        let memory_profile = work_db_memory_profile_from_env();
        configure_work_db_options(&mut options, blob_mode, memory_profile);
        let db = DB::open_cf_descriptors_read_only(
            &options,
            source,
            column_family_descriptors_for_blob_mode(blob_mode),
            false,
        )
        .map_err(|error| StorageError::RocksDb(error.to_string()))?;
        let checkpoint =
            Checkpoint::new(&db).map_err(|error| StorageError::RocksDb(error.to_string()))?;
        checkpoint
            .create_checkpoint(destination)
            .map_err(|error| StorageError::RocksDb(error.to_string()))
    }

    pub fn open(path: &Path) -> StorageResult<Self> {
        let mut base_opts = Options::default();
        base_opts.create_if_missing(true);
        base_opts.create_missing_column_families(true);
        let blob_mode = work_db_blob_mode_from_env();
        let memory_profile = work_db_memory_profile_from_env();
        configure_work_db_options(&mut base_opts, blob_mode, memory_profile);

        let db = DB::open_cf_descriptors(
            &base_opts,
            path,
            column_family_descriptors_for_blob_mode(blob_mode),
        )
        .map_err(|e| StorageError::RocksDb(e.to_string()))?;

        Ok(Self {
            db,
            external_raw_store: None,
            external_repo_bytes: None,
            publication_point_cache_index_dir: default_pp_cache_index_dir(path),
            child_certificate_cache_index_dir: default_child_certificate_cache_index_dir(path),
            publication_point_cache_projection_index: Mutex::new(
                PublicationPointCacheProjectionIndexState::Uninitialized,
            ),
        })
    }

    pub fn open_with_external_raw_store(path: &Path, raw_store_path: &Path) -> StorageResult<Self> {
        Self::open_with_external_stores(path, Some(raw_store_path), None)
    }

    pub fn open_with_external_repo_bytes(
        path: &Path,
        repo_bytes_path: &Path,
    ) -> StorageResult<Self> {
        Self::open_with_external_stores(path, None, Some(repo_bytes_path))
    }

    pub fn open_with_external_repo_bytes_read_only(
        path: &Path,
        repo_bytes_path: &Path,
    ) -> StorageResult<Self> {
        let mut store = Self::open(path)?;
        store.external_repo_bytes = Some(ExternalRepoBytesDb::open_read_only(repo_bytes_path)?);
        Ok(store)
    }

    pub fn open_with_external_stores(
        path: &Path,
        raw_store_path: Option<&Path>,
        repo_bytes_path: Option<&Path>,
    ) -> StorageResult<Self> {
        let mut store = Self::open(path)?;
        if let Some(raw_store_path) = raw_store_path {
            store.external_raw_store = Some(ExternalRawStoreDb::open(raw_store_path)?);
        }
        if let Some(repo_bytes_path) = repo_bytes_path {
            store.external_repo_bytes = Some(ExternalRepoBytesDb::open(repo_bytes_path)?);
        }
        Ok(store)
    }

    pub(crate) fn external_raw_store_ref(&self) -> Option<&ExternalRawStoreDb> {
        self.external_raw_store.as_ref()
    }

    pub(crate) fn external_repo_bytes_ref(&self) -> Option<&ExternalRepoBytesDb> {
        self.external_repo_bytes.as_ref()
    }

    pub fn memory_snapshot(&self) -> RocksDbMemorySnapshot {
        let mut databases = Vec::new();
        databases.push(memory_db_snapshot_for_column_families(
            "work-db",
            &self.db,
            Some(ALL_COLUMN_FAMILY_NAMES),
        ));
        if let Some(raw_store) = self.external_raw_store.as_ref() {
            databases.push(raw_store.memory_snapshot("raw-store.db"));
        }
        if let Some(repo_bytes) = self.external_repo_bytes.as_ref() {
            databases.push(repo_bytes.memory_snapshot("repo-bytes.db"));
        }

        let mut totals = RocksDbMemoryTotals::default();
        for db in &databases {
            totals.add_properties(&db.properties);
        }
        RocksDbMemorySnapshot { databases, totals }
    }

    pub fn publication_point_cache_mmap_index_load_stats(&self) -> Option<PpCacheIndexLoadStats> {
        let guard = self.publication_point_cache_projection_index.lock().ok()?;
        match &*guard {
            PublicationPointCacheProjectionIndexState::LoadedMmap { load_stats, .. } => {
                Some(load_stats.clone())
            }
            PublicationPointCacheProjectionIndexState::Uninitialized
            | PublicationPointCacheProjectionIndexState::Disabled
            | PublicationPointCacheProjectionIndexState::BuildingFromEmpty { .. }
            | PublicationPointCacheProjectionIndexState::Loaded { .. } => None,
        }
    }

    fn try_load_publication_point_cache_mmap_index_for_update(
        &self,
        reason: &'static str,
    ) -> StorageResult<()> {
        if !pp_cache_raw_index_enabled() {
            return Ok(());
        }
        let mut guard = self
            .publication_point_cache_projection_index
            .lock()
            .map_err(|e| {
                StorageError::RocksDb(format!("publication point cache index lock poisoned: {e}"))
            })?;
        if !matches!(
            *guard,
            PublicationPointCacheProjectionIndexState::Uninitialized
        ) {
            return Ok(());
        }
        match load_pp_cache_mmap_index_set(&self.publication_point_cache_index_dir) {
            Ok((mmap, stats)) => {
                crate::progress_log::emit(
                    "publication_point_cache_mmap_index_load",
                    serde_json::json!({
                        "state": "loaded",
                        "reason": reason,
                        "entries": stats.entries,
                        "bytes": stats.bytes,
                        "file_bytes": stats.file_bytes,
                        "load_ms": stats.load_ms,
                    }),
                );
                *guard = PublicationPointCacheProjectionIndexState::LoadedMmap {
                    mmap,
                    dirty: HashMap::new(),
                    dirty_bytes: 0,
                    load_stats: stats,
                };
            }
            Err(e) => {
                crate::progress_log::emit(
                    "publication_point_cache_mmap_index_load",
                    serde_json::json!({
                        "state": "deferred_fallback_scan",
                        "reason": reason,
                        "error": e.to_string(),
                    }),
                );
            }
        }
        Ok(())
    }

    fn cf(&self, name: &'static str) -> StorageResult<&ColumnFamily> {
        self.db
            .cf_handle(name)
            .ok_or(StorageError::MissingColumnFamily(name))
    }

    pub fn put_repository_view_entry(&self, entry: &RepositoryViewEntry) -> StorageResult<()> {
        entry.validate_internal()?;
        let cf = self.cf(CF_REPOSITORY_VIEW)?;
        let key = repository_view_key(&entry.rsync_uri);
        let value = encode_cbor(entry, "repository_view")?;
        self.db
            .put_cf(cf, key.as_bytes(), value)
            .map_err(|e| StorageError::RocksDb(e.to_string()))?;
        Ok(())
    }

    pub fn get_repository_view_entry(
        &self,
        rsync_uri: &str,
    ) -> StorageResult<Option<RepositoryViewEntry>> {
        let cf = self.cf(CF_REPOSITORY_VIEW)?;
        let key = repository_view_key(rsync_uri);
        let Some(bytes) = self
            .db
            .get_cf(cf, key.as_bytes())
            .map_err(|e| StorageError::RocksDb(e.to_string()))?
        else {
            return Ok(None);
        };
        let entry = decode_cbor::<RepositoryViewEntry>(&bytes, "repository_view")?;
        entry.validate_internal()?;
        Ok(Some(entry))
    }

    pub fn delete_repository_view_entry(&self, rsync_uri: &str) -> StorageResult<()> {
        let cf = self.cf(CF_REPOSITORY_VIEW)?;
        let key = repository_view_key(rsync_uri);
        self.db
            .delete_cf(cf, key.as_bytes())
            .map_err(|e| StorageError::RocksDb(e.to_string()))?;
        Ok(())
    }

    pub fn put_projection_batch(
        &self,
        repository_view_entries: &[RepositoryViewEntry],
        member_records: &[RrdpSourceMemberRecord],
        owner_records: &[RrdpUriOwnerRecord],
    ) -> StorageResult<()> {
        if repository_view_entries.is_empty()
            && member_records.is_empty()
            && owner_records.is_empty()
        {
            return Ok(());
        }

        let repo_cf = self.cf(CF_REPOSITORY_VIEW)?;
        let member_cf = self.cf(CF_RRDP_SOURCE_MEMBER)?;
        let owner_cf = self.cf(CF_RRDP_URI_OWNER)?;
        let mut batch = WriteBatch::default();

        for entry in repository_view_entries {
            entry.validate_internal()?;
            let key = repository_view_key(&entry.rsync_uri);
            let value = encode_cbor(entry, "repository_view")?;
            batch.put_cf(repo_cf, key.as_bytes(), value);
        }
        for record in member_records {
            record.validate_internal()?;
            let key = rrdp_source_member_key(&record.notify_uri, &record.rsync_uri);
            let value = encode_cbor(record, "rrdp_source_member")?;
            batch.put_cf(member_cf, key.as_bytes(), value);
        }
        for record in owner_records {
            record.validate_internal()?;
            let key = rrdp_uri_owner_key(&record.rsync_uri);
            let value = encode_cbor(record, "rrdp_uri_owner")?;
            batch.put_cf(owner_cf, key.as_bytes(), value);
        }

        self.write_batch(batch)
    }

    pub fn list_repository_view_entries_with_prefix(
        &self,
        rsync_uri_prefix: &str,
    ) -> StorageResult<Vec<RepositoryViewEntry>> {
        let cf = self.cf(CF_REPOSITORY_VIEW)?;
        let prefix = repository_view_prefix(rsync_uri_prefix);
        let mode = IteratorMode::From(prefix.as_bytes(), Direction::Forward);
        self.db
            .iterator_cf(cf, mode)
            .take_while(|res| match res {
                Ok((key, _)) => key.starts_with(prefix.as_bytes()),
                Err(_) => false,
            })
            .map(|res| {
                let (_key, value) = res.map_err(|e| StorageError::RocksDb(e.to_string()))?;
                let entry = decode_cbor::<RepositoryViewEntry>(&value, "repository_view")?;
                entry.validate_internal()?;
                Ok(entry)
            })
            .collect()
    }

    pub fn verify_current_repository_blobs(
        &self,
        batch_size: usize,
    ) -> StorageResult<RepositoryBlobVerificationSummary> {
        if batch_size == 0 {
            return Err(StorageError::InvalidData {
                entity: "repository_blob_verification",
                detail: "batch_size must be greater than zero".to_string(),
            });
        }
        let cf = self.cf(CF_REPOSITORY_VIEW)?;
        let prefix = repository_view_prefix("");
        let mode = IteratorMode::From(prefix.as_bytes(), Direction::Forward);
        let mut summary = RepositoryBlobVerificationSummary::default();
        let mut batch = Vec::with_capacity(batch_size);
        for item in self.db.iterator_cf(cf, mode) {
            let (key, value) = item.map_err(|error| StorageError::RocksDb(error.to_string()))?;
            if !key.starts_with(prefix.as_bytes()) {
                break;
            }
            let entry = decode_cbor::<RepositoryViewEntry>(&value, "repository_view")?;
            entry.validate_internal()?;
            if !matches!(
                entry.state,
                RepositoryViewState::Present | RepositoryViewState::Replaced
            ) {
                continue;
            }
            let hash = entry
                .current_hash
                .clone()
                .ok_or(StorageError::InvalidData {
                    entity: "repository_blob_verification",
                    detail: format!("current hash missing for {}", entry.rsync_uri),
                })?;
            batch.push((entry.rsync_uri, hash));
            if batch.len() >= batch_size {
                verify_repository_blob_batch(self, &batch, &mut summary)?;
                batch.clear();
            }
        }
        if !batch.is_empty() {
            verify_repository_blob_batch(self, &batch, &mut summary)?;
        }
        Ok(summary)
    }

    pub fn put_raw_by_hash_entry(&self, entry: &RawByHashEntry) -> StorageResult<()> {
        entry.validate_internal()?;
        if let Some(raw_store) = self.external_raw_store.as_ref() {
            return raw_store.put_raw_entry(entry);
        }
        let cf = self.cf(CF_RAW_BY_HASH)?;
        let key = raw_by_hash_key(&entry.sha256_hex);
        let value = encode_cbor(entry, "raw_by_hash")?;
        self.db
            .put_cf(cf, key.as_bytes(), value)
            .map_err(|e| StorageError::RocksDb(e.to_string()))?;
        Ok(())
    }

    pub fn put_raw_by_hash_entries_batch(&self, entries: &[RawByHashEntry]) -> StorageResult<()> {
        if entries.is_empty() {
            return Ok(());
        }
        if let Some(raw_store) = self.external_raw_store.as_ref() {
            return raw_store.put_raw_entries_batch(entries);
        }

        let cf = self.cf(CF_RAW_BY_HASH)?;
        let mut batch = WriteBatch::default();
        for entry in entries {
            entry.validate_internal()?;
            let key = raw_by_hash_key(&entry.sha256_hex);
            let value = encode_cbor(entry, "raw_by_hash")?;
            batch.put_cf(cf, key.as_bytes(), value);
        }
        self.write_batch(batch)
    }

    pub fn put_raw_by_hash_entries_batch_unchecked(
        &self,
        entries: &[RawByHashEntry],
    ) -> StorageResult<()> {
        if entries.is_empty() {
            return Ok(());
        }
        if let Some(raw_store) = self.external_raw_store.as_ref() {
            return raw_store.put_raw_entries_batch(entries);
        }

        let cf = self.cf(CF_RAW_BY_HASH)?;
        let mut batch = WriteBatch::default();
        for entry in entries {
            let key = raw_by_hash_key(&entry.sha256_hex);
            let value = encode_cbor(entry, "raw_by_hash")?;
            batch.put_cf(cf, key.as_bytes(), value);
        }
        self.write_batch(batch)
    }

    pub fn put_blob_bytes_batch(&self, blobs: &[(String, Vec<u8>)]) -> StorageResult<()> {
        if blobs.is_empty() {
            return Ok(());
        }
        if let Some(repo_bytes) = self.external_repo_bytes.as_ref() {
            if repo_bytes.is_read_only() {
                return repo_bytes.require_existing_blob_bytes_batch(blobs);
            }
            return repo_bytes.put_blob_bytes_batch(blobs);
        }
        if let Some(raw_store) = self.external_raw_store.as_ref() {
            return raw_store.put_blob_bytes_batch(blobs);
        }
        let cf = self.cf(CF_RAW_BLOB)?;
        let mut batch = WriteBatch::default();
        for (sha256_hex, bytes) in blobs {
            validate_sha256_hex("raw_blob.sha256_hex", sha256_hex)?;
            if bytes.is_empty() {
                return Err(StorageError::InvalidData {
                    entity: "raw_blob",
                    detail: "bytes must not be empty".to_string(),
                });
            }
            let key = raw_blob_key(sha256_hex);
            batch.put_cf(cf, key.as_bytes(), bytes.as_slice());
        }
        self.write_batch(batch)
    }

    pub fn delete_raw_by_hash_entry(&self, sha256_hex: &str) -> StorageResult<()> {
        validate_sha256_hex("raw_by_hash.sha256_hex", sha256_hex)?;
        if let Some(raw_store) = self.external_raw_store.as_ref() {
            return raw_store.delete_raw_entry(sha256_hex);
        }
        let cf = self.cf(CF_RAW_BY_HASH)?;
        let key = raw_by_hash_key(sha256_hex);
        self.db
            .delete_cf(cf, key.as_bytes())
            .map_err(|e| StorageError::RocksDb(e.to_string()))?;
        Ok(())
    }

    pub fn get_raw_by_hash_entry(&self, sha256_hex: &str) -> StorageResult<Option<RawByHashEntry>> {
        if let Some(raw_store) = self.external_raw_store.as_ref() {
            return raw_store.get_raw_entry(sha256_hex);
        }
        let cf = self.cf(CF_RAW_BY_HASH)?;
        let key = raw_by_hash_key(sha256_hex);
        let Some(bytes) = self
            .db
            .get_cf(cf, key.as_bytes())
            .map_err(|e| StorageError::RocksDb(e.to_string()))?
        else {
            return Ok(None);
        };
        let entry = decode_cbor::<RawByHashEntry>(&bytes, "raw_by_hash")?;
        entry.validate_internal()?;
        Ok(Some(entry))
    }

    pub fn get_raw_by_hash_entries_batch(
        &self,
        sha256_hexes: &[String],
    ) -> StorageResult<Vec<Option<RawByHashEntry>>> {
        if sha256_hexes.is_empty() {
            return Ok(Vec::new());
        }
        if let Some(raw_store) = self.external_raw_store.as_ref() {
            return raw_store.get_raw_entries_batch(sha256_hexes);
        }

        let cf = self.cf(CF_RAW_BY_HASH)?;
        let keys: Vec<String> = sha256_hexes
            .iter()
            .map(|hash| raw_by_hash_key(hash))
            .collect();
        self.db
            .multi_get_cf(keys.iter().map(|key| (cf, key.as_bytes())))
            .into_iter()
            .map(|res| {
                let maybe = res.map_err(|e| StorageError::RocksDb(e.to_string()))?;
                match maybe {
                    Some(bytes) => {
                        let entry = decode_cbor::<RawByHashEntry>(&bytes, "raw_by_hash")?;
                        entry.validate_internal()?;
                        Ok(Some(entry))
                    }
                    None => Ok(None),
                }
            })
            .collect()
    }

    pub fn get_blob_bytes(&self, sha256_hex: &str) -> StorageResult<Option<Vec<u8>>> {
        if let Some(repo_bytes) = self.external_repo_bytes.as_ref() {
            return repo_bytes.get_blob_bytes(sha256_hex);
        }
        if let Some(raw_store) = self.external_raw_store.as_ref() {
            return raw_store.get_blob_bytes(sha256_hex);
        }
        validate_sha256_hex("raw_blob.sha256_hex", sha256_hex)?;
        let cf = self.cf(CF_RAW_BLOB)?;
        let key = raw_blob_key(sha256_hex);
        if let Some(bytes) = self
            .db
            .get_cf(cf, key.as_bytes())
            .map_err(|e| StorageError::RocksDb(e.to_string()))?
        {
            return Ok(Some(bytes));
        }
        self.get_raw_by_hash_entry(sha256_hex)
            .map(|entry| entry.map(|entry| entry.bytes))
    }

    pub fn get_blob_bytes_batch(
        &self,
        sha256_hexes: &[String],
    ) -> StorageResult<Vec<Option<Vec<u8>>>> {
        if sha256_hexes.is_empty() {
            return Ok(Vec::new());
        }
        if let Some(repo_bytes) = self.external_repo_bytes.as_ref() {
            return repo_bytes.get_blob_bytes_batch(sha256_hexes);
        }
        if let Some(raw_store) = self.external_raw_store.as_ref() {
            return raw_store.get_blob_bytes_batch(sha256_hexes);
        }

        let cf = self.cf(CF_RAW_BLOB)?;
        let keys: Vec<String> = sha256_hexes
            .iter()
            .map(|hash| {
                validate_sha256_hex("raw_blob.sha256_hex", hash)?;
                Ok::<String, StorageError>(raw_blob_key(hash))
            })
            .collect::<Result<_, _>>()?;
        let blob_results: Vec<Option<Vec<u8>>> = self
            .db
            .multi_get_cf(keys.iter().map(|key| (cf, key.as_bytes())))
            .into_iter()
            .map(|res| res.map_err(|e| StorageError::RocksDb(e.to_string())))
            .collect::<Result<_, _>>()?;

        let mut out = Vec::with_capacity(sha256_hexes.len());
        for (sha256_hex, maybe_blob) in sha256_hexes.iter().zip(blob_results.into_iter()) {
            if maybe_blob.is_some() {
                out.push(maybe_blob);
            } else {
                out.push(
                    self.get_raw_by_hash_entry(sha256_hex)?
                        .map(|entry| entry.bytes),
                );
            }
        }
        Ok(out)
    }

    pub fn put_vcir(&self, vcir: &ValidatedCaInstanceResult) -> StorageResult<()> {
        self.put_vcir_with_publication_point_cache_projection(vcir, None)
    }

    pub fn put_vcir_with_failed_fetch_reuse_identity(
        &self,
        vcir: &ValidatedCaInstanceResult,
        failed_fetch_reuse_identity: &VcirFailedFetchReuseIdentity,
    ) -> StorageResult<()> {
        self.put_vcir_with_projection_action(
            vcir,
            None,
            PublicationPointCacheProjectionWriteAction::Keep,
            Some(failed_fetch_reuse_identity),
        )
    }

    pub fn put_vcir_with_publication_point_cache_projection(
        &self,
        vcir: &ValidatedCaInstanceResult,
        publication_point_projection: Option<&PublicationPointCacheProjection>,
    ) -> StorageResult<()> {
        self.put_vcir_with_projections(vcir, None, publication_point_projection)
    }

    pub fn put_vcir_with_projections(
        &self,
        vcir: &ValidatedCaInstanceResult,
        roa_cache_context: Option<&RoaCacheProjectionContext>,
        publication_point_projection: Option<&PublicationPointCacheProjection>,
    ) -> StorageResult<()> {
        let publication_point_projection_action = publication_point_projection
            .map(PublicationPointCacheProjectionWriteAction::Write)
            .unwrap_or(PublicationPointCacheProjectionWriteAction::Keep);
        self.put_vcir_with_projection_action(
            vcir,
            roa_cache_context,
            publication_point_projection_action,
            None,
        )
    }

    fn put_vcir_with_projection_action(
        &self,
        vcir: &ValidatedCaInstanceResult,
        roa_cache_context: Option<&RoaCacheProjectionContext>,
        publication_point_projection_action: PublicationPointCacheProjectionWriteAction<'_>,
        failed_fetch_reuse_identity: Option<&VcirFailedFetchReuseIdentity>,
    ) -> StorageResult<()> {
        vcir.validate_internal()?;
        let vcir_cf = self.cf(CF_VCIR)?;
        let failed_fetch_reuse_identity_cf = self.cf(CF_VCIR_FAILED_FETCH_REUSE_IDENTITY)?;
        let replay_cf = self.cf(CF_MANIFEST_REPLAY_META)?;
        let projection_cf = self.cf(CF_ROA_CACHE_PROJECTION)?;
        let pp_projection_cf = self.cf(CF_PUBLICATION_POINT_CACHE_PROJECTION)?;
        let replay_meta = ManifestReplayMeta::from_vcir(vcir);
        replay_meta.validate_internal()?;
        let mut batch = WriteBatch::default();
        let key = vcir_key(&vcir.manifest_rsync_uri);
        let value = encode_cbor(vcir, "vcir")?;
        batch.put_cf(vcir_cf, key.as_bytes(), value);
        write_vcir_failed_fetch_reuse_identity_to_batch(
            failed_fetch_reuse_identity_cf,
            &mut batch,
            &vcir.manifest_rsync_uri,
            failed_fetch_reuse_identity,
        )?;
        let replay_key = manifest_replay_meta_key(&replay_meta.manifest_rsync_uri);
        let replay_value = encode_cbor(&replay_meta, "manifest_replay_meta")?;
        batch.put_cf(replay_cf, replay_key.as_bytes(), replay_value);
        write_roa_cache_projection_to_batch(
            projection_cf,
            &mut batch,
            vcir,
            roa_cache_context,
            None,
        )?;
        write_publication_point_cache_projection_to_batch(
            pp_projection_cf,
            &mut batch,
            publication_point_projection_action,
            None,
        )?;
        self.write_batch(batch)?;
        self.apply_publication_point_cache_projection_index_action(
            publication_point_projection_action,
        )?;
        Ok(())
    }

    pub fn replace_vcir_and_manifest_replay_meta(
        &self,
        vcir: &ValidatedCaInstanceResult,
    ) -> StorageResult<VcirReplaceTimingBreakdown> {
        self.replace_vcir_manifest_replay_meta_and_publication_point_cache_projection(vcir, None)
    }

    pub fn replace_vcir_manifest_replay_meta_and_publication_point_cache_projection(
        &self,
        vcir: &ValidatedCaInstanceResult,
        publication_point_projection: Option<&PublicationPointCacheProjection>,
    ) -> StorageResult<VcirReplaceTimingBreakdown> {
        self.replace_vcir_manifest_replay_meta_and_projections(
            vcir,
            None,
            publication_point_projection,
        )
    }

    pub fn replace_vcir_manifest_replay_meta_and_projections(
        &self,
        vcir: &ValidatedCaInstanceResult,
        roa_cache_context: Option<&RoaCacheProjectionContext>,
        publication_point_projection: Option<&PublicationPointCacheProjection>,
    ) -> StorageResult<VcirReplaceTimingBreakdown> {
        let publication_point_projection_action = publication_point_projection
            .map(PublicationPointCacheProjectionWriteAction::Write)
            .unwrap_or(PublicationPointCacheProjectionWriteAction::Keep);
        self.replace_vcir_manifest_replay_meta_and_projection_action(
            vcir,
            roa_cache_context,
            publication_point_projection_action,
        )
    }

    pub(crate) fn replace_vcir_manifest_replay_meta_and_projection_action(
        &self,
        vcir: &ValidatedCaInstanceResult,
        roa_cache_context: Option<&RoaCacheProjectionContext>,
        publication_point_projection_action: PublicationPointCacheProjectionWriteAction<'_>,
    ) -> StorageResult<VcirReplaceTimingBreakdown> {
        self.replace_vcir_manifest_replay_meta_and_projection_action_with_failed_fetch_reuse_identity(
            vcir,
            roa_cache_context,
            publication_point_projection_action,
            None,
        )
    }

    pub(crate) fn replace_vcir_manifest_replay_meta_and_projection_action_with_failed_fetch_reuse_identity(
        &self,
        vcir: &ValidatedCaInstanceResult,
        roa_cache_context: Option<&RoaCacheProjectionContext>,
        publication_point_projection_action: PublicationPointCacheProjectionWriteAction<'_>,
        failed_fetch_reuse_identity: Option<&VcirFailedFetchReuseIdentity>,
    ) -> StorageResult<VcirReplaceTimingBreakdown> {
        let mut timing = VcirReplaceTimingBreakdown {
            rss_before_kb: process_vm_rss_kb(),
            ..VcirReplaceTimingBreakdown::default()
        };

        let validate_started = std::time::Instant::now();
        vcir.validate_internal()?;
        timing.validate_ms = validate_started.elapsed().as_millis() as u64;
        timing.rss_after_validate_kb = process_vm_rss_kb();
        timing.field_sizes = VcirFieldSizeBreakdown::from_vcir(vcir);

        let batch_build_started = std::time::Instant::now();
        let vcir_cf = self.cf(CF_VCIR)?;
        let failed_fetch_reuse_identity_cf = self.cf(CF_VCIR_FAILED_FETCH_REUSE_IDENTITY)?;
        let replay_cf = self.cf(CF_MANIFEST_REPLAY_META)?;
        let projection_cf = self.cf(CF_ROA_CACHE_PROJECTION)?;
        let pp_projection_cf = self.cf(CF_PUBLICATION_POINT_CACHE_PROJECTION)?;
        let mut batch = WriteBatch::default();

        let vcir_key = vcir_key(&vcir.manifest_rsync_uri);
        let vcir_encode_started = std::time::Instant::now();
        let vcir_value = encode_cbor(vcir, "vcir")?;
        timing.vcir_encode_ms = vcir_encode_started.elapsed().as_millis() as u64;
        timing.vcir_value_bytes = vcir_value.len() as u64;
        batch.put_cf(vcir_cf, vcir_key.as_bytes(), vcir_value);
        timing.rss_after_vcir_encode_kb = process_vm_rss_kb();
        write_vcir_failed_fetch_reuse_identity_to_batch(
            failed_fetch_reuse_identity_cf,
            &mut batch,
            &vcir.manifest_rsync_uri,
            failed_fetch_reuse_identity,
        )?;

        let replay_meta_encode_started = std::time::Instant::now();
        let replay_meta = ManifestReplayMeta::from_vcir(vcir);
        replay_meta.validate_internal()?;
        let replay_key = manifest_replay_meta_key(&replay_meta.manifest_rsync_uri);
        let replay_value = encode_cbor(&replay_meta, "manifest_replay_meta")?;
        timing.replay_meta_encode_ms = replay_meta_encode_started.elapsed().as_millis() as u64;
        timing.replay_meta_value_bytes = replay_value.len() as u64;
        batch.put_cf(replay_cf, replay_key.as_bytes(), replay_value);
        timing.rss_after_replay_meta_encode_kb = process_vm_rss_kb();

        let projection_encode_started = std::time::Instant::now();
        write_roa_cache_projection_to_batch(
            projection_cf,
            &mut batch,
            vcir,
            roa_cache_context,
            Some(&mut timing),
        )?;
        timing.roa_cache_projection_encode_ms =
            projection_encode_started.elapsed().as_millis() as u64;
        timing.rss_after_roa_cache_projection_encode_kb = process_vm_rss_kb();

        let pp_projection_encode_started = std::time::Instant::now();
        write_publication_point_cache_projection_to_batch(
            pp_projection_cf,
            &mut batch,
            publication_point_projection_action,
            Some(&mut timing),
        )?;
        timing.publication_point_cache_projection_encode_ms =
            pp_projection_encode_started.elapsed().as_millis() as u64;
        timing.rss_after_publication_point_cache_projection_encode_kb = process_vm_rss_kb();

        timing.total_encoded_bytes = timing.vcir_value_bytes
            + timing.replay_meta_value_bytes
            + timing.roa_cache_projection_value_bytes
            + timing.publication_point_cache_projection_value_bytes;
        timing.batch_build_ms = batch_build_started.elapsed().as_millis() as u64;

        let write_batch_started = std::time::Instant::now();
        self.write_batch(batch)?;
        self.apply_publication_point_cache_projection_index_action(
            publication_point_projection_action,
        )?;
        timing.write_batch_ms = write_batch_started.elapsed().as_millis() as u64;
        timing.rss_after_write_batch_kb = process_vm_rss_kb();
        Ok(timing)
    }

    pub fn get_vcir(
        &self,
        manifest_rsync_uri: &str,
    ) -> StorageResult<Option<ValidatedCaInstanceResult>> {
        let cf = self.cf(CF_VCIR)?;
        let key = vcir_key(manifest_rsync_uri);
        let Some(bytes) = self
            .db
            .get_cf(cf, key.as_bytes())
            .map_err(|e| StorageError::RocksDb(e.to_string()))?
        else {
            return Ok(None);
        };
        let vcir = decode_cbor::<ValidatedCaInstanceResult>(&bytes, "vcir")?;
        vcir.validate_internal()?;
        Ok(Some(vcir))
    }

    pub fn get_vcir_failed_fetch_reuse_identity(
        &self,
        manifest_rsync_uri: &str,
    ) -> StorageResult<Option<VcirFailedFetchReuseIdentity>> {
        let cf = self.cf(CF_VCIR_FAILED_FETCH_REUSE_IDENTITY)?;
        let key = vcir_failed_fetch_reuse_identity_key(manifest_rsync_uri);
        let Some(bytes) = self
            .db
            .get_cf(cf, key.as_bytes())
            .map_err(|e| StorageError::RocksDb(e.to_string()))?
        else {
            return Ok(None);
        };
        let identity = decode_cbor::<VcirFailedFetchReuseIdentity>(
            &bytes,
            "vcir_failed_fetch_reuse_identity",
        )?;
        identity.validate_internal()?;
        Ok(Some(identity))
    }

    pub fn get_manifest_replay_meta(
        &self,
        manifest_rsync_uri: &str,
    ) -> StorageResult<Option<ManifestReplayMeta>> {
        let cf = self.cf(CF_MANIFEST_REPLAY_META)?;
        let key = manifest_replay_meta_key(manifest_rsync_uri);
        let Some(bytes) = self
            .db
            .get_cf(cf, key.as_bytes())
            .map_err(|e| StorageError::RocksDb(e.to_string()))?
        else {
            return Ok(None);
        };
        let meta = decode_cbor::<ManifestReplayMeta>(&bytes, "manifest_replay_meta")?;
        meta.validate_internal()?;
        Ok(Some(meta))
    }

    pub fn get_roa_cache_projection(
        &self,
        manifest_rsync_uri: &str,
    ) -> StorageResult<Option<RoaCacheProjection>> {
        let cf = self.cf(CF_ROA_CACHE_PROJECTION)?;
        let key = roa_cache_projection_key(manifest_rsync_uri);
        let Some(bytes) = self
            .db
            .get_cf(cf, key.as_bytes())
            .map_err(|e| StorageError::RocksDb(e.to_string()))?
        else {
            return Ok(None);
        };
        let projection = decode_cbor::<RoaCacheProjection>(&bytes, "roa_cache_projection")?;
        projection.validate_internal()?;
        Ok(Some(projection))
    }

    pub fn put_child_certificate_cache_projection(
        &self,
        projection: &ChildCertificateCacheProjection,
    ) -> StorageResult<()> {
        projection.validate_internal()?;
        let cf = self.cf(CF_CHILD_CERTIFICATE_CACHE_PROJECTION)?;
        let key = child_certificate_cache_projection_key(&projection.cache_key_sha256_hex);
        let value = encode_cbor(projection, "child_certificate_cache_projection")?;
        self.db
            .put_cf(cf, key.as_bytes(), value)
            .map_err(|e| StorageError::RocksDb(e.to_string()))?;
        Ok(())
    }

    pub fn get_child_certificate_cache_projection(
        &self,
        cache_key_sha256_hex: &str,
    ) -> StorageResult<Option<ChildCertificateCacheProjection>> {
        validate_sha256_hex(
            "child_certificate_cache_projection.cache_key_sha256_hex",
            cache_key_sha256_hex,
        )?;
        let cf = self.cf(CF_CHILD_CERTIFICATE_CACHE_PROJECTION)?;
        let key = child_certificate_cache_projection_key(cache_key_sha256_hex);
        let Some(bytes) = self
            .db
            .get_cf(cf, key.as_bytes())
            .map_err(|e| StorageError::RocksDb(e.to_string()))?
        else {
            return Ok(None);
        };
        let projection = decode_cbor::<ChildCertificateCacheProjection>(
            &bytes,
            "child_certificate_cache_projection",
        )?;
        projection.validate_internal()?;
        Ok(Some(projection))
    }

    pub fn get_child_certificate_cache_projections_batch(
        &self,
        cache_key_sha256_hexes: &[String],
    ) -> StorageResult<Vec<Option<ChildCertificateCacheProjection>>> {
        if cache_key_sha256_hexes.is_empty() {
            return Ok(Vec::new());
        }

        for cache_key_sha256_hex in cache_key_sha256_hexes {
            validate_sha256_hex(
                "child_certificate_cache_projection.cache_key_sha256_hex",
                cache_key_sha256_hex,
            )?;
        }

        let cf = self.cf(CF_CHILD_CERTIFICATE_CACHE_PROJECTION)?;
        let keys: Vec<String> = cache_key_sha256_hexes
            .iter()
            .map(|key| child_certificate_cache_projection_key(key))
            .collect();
        self.db
            .multi_get_cf(keys.iter().map(|key| (cf, key.as_bytes())))
            .into_iter()
            .map(|res| {
                let Some(bytes) = res.map_err(|e| StorageError::RocksDb(e.to_string()))? else {
                    return Ok(None);
                };
                let projection = decode_cbor::<ChildCertificateCacheProjection>(
                    &bytes,
                    "child_certificate_cache_projection",
                )?;
                projection.validate_internal()?;
                Ok(Some(projection))
            })
            .collect()
    }

    fn child_certificate_cache_segment_path(&self, manifest_rsync_uri: &str) -> PathBuf {
        self.child_certificate_cache_index_dir
            .join(child_certificate_cache_segment_file_name(
                manifest_rsync_uri,
            ))
    }

    pub fn get_child_certificate_cache_projections_mmap_segment(
        &self,
        manifest_rsync_uri: &str,
        cache_key_sha256_hexes: &[String],
    ) -> StorageResult<Option<ChildCertificateCacheMmapLookup>> {
        if cache_key_sha256_hexes.is_empty() {
            return Ok(Some(ChildCertificateCacheMmapLookup::default()));
        }
        validate_non_empty(
            "child_certificate_cache_mmap_segment.manifest_rsync_uri",
            manifest_rsync_uri,
        )?;
        for cache_key_sha256_hex in cache_key_sha256_hexes {
            validate_sha256_hex(
                "child_certificate_cache_projection.cache_key_sha256_hex",
                cache_key_sha256_hex,
            )?;
        }

        let path = self.child_certificate_cache_segment_path(manifest_rsync_uri);
        if !path.exists() {
            return Ok(None);
        }

        let (index, _) = load_pp_cache_mmap_index(&path)?;
        let file_bytes = index.file_bytes();
        let mut hits = 0usize;
        let mut misses = 0usize;
        let mut projections = Vec::with_capacity(cache_key_sha256_hexes.len());
        for cache_key_sha256_hex in cache_key_sha256_hexes {
            match index.lookup(cache_key_sha256_hex) {
                Some(PpCacheIndexLookup::Hit(bytes)) => {
                    let projection = decode_cbor::<ChildCertificateCacheProjection>(
                        bytes,
                        "child_certificate_cache_projection_mmap_segment",
                    )?;
                    projection.validate_internal()?;
                    hits = hits.saturating_add(1);
                    projections.push(Some(projection));
                }
                Some(PpCacheIndexLookup::Deleted) | None => {
                    misses = misses.saturating_add(1);
                    projections.push(None);
                }
            }
        }

        Ok(Some(ChildCertificateCacheMmapLookup {
            projections,
            hits,
            misses,
            file_bytes,
        }))
    }

    pub fn write_child_certificate_cache_mmap_segment(
        &self,
        manifest_rsync_uri: &str,
        projections: &[ChildCertificateCacheProjection],
    ) -> StorageResult<PpCacheIndexRefreshStats> {
        validate_non_empty(
            "child_certificate_cache_mmap_segment.manifest_rsync_uri",
            manifest_rsync_uri,
        )?;
        let mut entries = Vec::with_capacity(projections.len());
        for projection in projections {
            projection.validate_internal()?;
            entries.push((
                projection.cache_key_sha256_hex.clone(),
                encode_cbor(
                    projection,
                    "child_certificate_cache_projection_mmap_segment",
                )?,
            ));
        }
        let path = self.child_certificate_cache_segment_path(manifest_rsync_uri);
        write_pp_cache_index_atomic(&path, entries)
    }

    pub fn write_child_certificate_cache_mmap_segment_overlay(
        &self,
        manifest_rsync_uri: &str,
        cache_key_sha256_hexes: &[String],
        projections: &[ChildCertificateCacheProjection],
    ) -> StorageResult<PpCacheIndexRefreshStats> {
        validate_non_empty(
            "child_certificate_cache_mmap_segment.manifest_rsync_uri",
            manifest_rsync_uri,
        )?;
        let mut dirty = HashMap::<String, Vec<u8>>::with_capacity(projections.len());
        for projection in projections {
            projection.validate_internal()?;
            dirty.insert(
                projection.cache_key_sha256_hex.clone(),
                encode_cbor(
                    projection,
                    "child_certificate_cache_projection_mmap_segment",
                )?,
            );
        }

        let path = self.child_certificate_cache_segment_path(manifest_rsync_uri);
        let existing = if path.exists() {
            Some(load_pp_cache_mmap_index(&path)?.0)
        } else {
            None
        };
        let mut entries = Vec::with_capacity(cache_key_sha256_hexes.len());
        let mut emitted = HashSet::<String>::new();
        for cache_key_sha256_hex in cache_key_sha256_hexes {
            validate_sha256_hex(
                "child_certificate_cache_projection.cache_key_sha256_hex",
                cache_key_sha256_hex,
            )?;
            if !emitted.insert(cache_key_sha256_hex.clone()) {
                continue;
            }
            if let Some(value) = dirty.remove(cache_key_sha256_hex) {
                entries.push((cache_key_sha256_hex.clone(), value));
                continue;
            }
            if let Some(existing) = existing.as_ref() {
                if let Some(PpCacheIndexLookup::Hit(bytes)) = existing.lookup(cache_key_sha256_hex)
                {
                    entries.push((cache_key_sha256_hex.clone(), bytes.to_vec()));
                }
            }
        }
        for (key, value) in dirty {
            if emitted.insert(key.clone()) {
                entries.push((key, value));
            }
        }

        write_pp_cache_index_atomic(&path, entries)
    }

    pub fn get_publication_point_cache_projection(
        &self,
        manifest_rsync_uri: &str,
    ) -> StorageResult<Option<PublicationPointCacheProjection>> {
        self.get_publication_point_cache_projection_from_db(manifest_rsync_uri)
    }

    pub fn get_publication_point_cache_projection_cached(
        &self,
        manifest_rsync_uri: &str,
    ) -> StorageResult<Option<PublicationPointCacheProjection>> {
        let mut owned_bytes: Option<Arc<[u8]>> = None;
        {
            let mut guard = self
                .publication_point_cache_projection_index
                .lock()
                .map_err(|e| {
                    StorageError::RocksDb(format!(
                        "publication point cache index lock poisoned: {e}"
                    ))
                })?;
            if matches!(
                *guard,
                PublicationPointCacheProjectionIndexState::Uninitialized
            ) {
                let load_started = std::time::Instant::now();
                let raw_index_enabled = pp_cache_raw_index_enabled();
                *guard = if !raw_index_enabled {
                    crate::progress_log::emit(
                        "publication_point_cache_raw_index",
                        serde_json::json!({
                            "state": "disabled_by_env",
                            "entries": 0,
                            "bytes": 0,
                            "load_ms": load_started.elapsed().as_millis() as u64,
                        }),
                    );
                    PublicationPointCacheProjectionIndexState::Disabled
                } else {
                    match load_pp_cache_mmap_index_set(&self.publication_point_cache_index_dir) {
                        Ok((mmap, stats)) => {
                            crate::progress_log::emit(
                                "publication_point_cache_mmap_index_load",
                                serde_json::json!({
                                    "state": "loaded",
                                    "entries": stats.entries,
                                    "bytes": stats.bytes,
                                    "file_bytes": stats.file_bytes,
                                    "load_ms": stats.load_ms,
                                }),
                            );
                            PublicationPointCacheProjectionIndexState::LoadedMmap {
                                mmap,
                                dirty: HashMap::new(),
                                dirty_bytes: 0,
                                load_stats: stats,
                            }
                        }
                        Err(e) => {
                            crate::progress_log::emit(
                                "publication_point_cache_mmap_index_load",
                                serde_json::json!({
                                    "state": "fallback_scan",
                                    "error": e.to_string(),
                                    "load_ms": load_started.elapsed().as_millis() as u64,
                                }),
                            );
                            let scan_started = std::time::Instant::now();
                            let (index, bytes) =
                                self.load_publication_point_cache_projection_index()?;
                            if index.is_empty() {
                                let limit = pp_cache_raw_index_empty_build_limit_bytes();
                                crate::progress_log::emit(
                                    "publication_point_cache_raw_index",
                                    serde_json::json!({
                                        "state": "empty_building_bounded",
                                        "entries": 0,
                                        "bytes": 0,
                                        "empty_build_limit_bytes": limit,
                                        "load_ms": scan_started.elapsed().as_millis() as u64,
                                    }),
                                );
                                PublicationPointCacheProjectionIndexState::BuildingFromEmpty {
                                    index,
                                    bytes: 0,
                                    limit,
                                }
                            } else {
                                crate::progress_log::emit(
                                    "publication_point_cache_raw_index",
                                    serde_json::json!({
                                        "state": "loaded",
                                        "entries": index.len(),
                                        "bytes": bytes,
                                        "load_ms": scan_started.elapsed().as_millis() as u64,
                                    }),
                                );
                                PublicationPointCacheProjectionIndexState::Loaded { index, bytes }
                            }
                        }
                    }
                };
            }
            match &*guard {
                PublicationPointCacheProjectionIndexState::Loaded { index, .. } => {
                    owned_bytes = index.get(manifest_rsync_uri).cloned();
                }
                PublicationPointCacheProjectionIndexState::BuildingFromEmpty { index, .. } => {
                    owned_bytes = index.get(manifest_rsync_uri).cloned();
                }
                PublicationPointCacheProjectionIndexState::LoadedMmap { mmap, dirty, .. } => {
                    if let Some(bytes) = dirty.get(manifest_rsync_uri).cloned() {
                        if bytes.is_empty() {
                            return Ok(None);
                        }
                        owned_bytes = Some(bytes);
                    } else if let Some(lookup) = mmap.lookup(manifest_rsync_uri) {
                        match lookup {
                            PpCacheIndexLookup::Hit(bytes) => {
                                let projection = decode_cbor::<PublicationPointCacheProjection>(
                                    bytes,
                                    "publication_point_cache_projection",
                                )?;
                                projection.validate_internal()?;
                                return Ok(Some(projection));
                            }
                            PpCacheIndexLookup::Deleted => return Ok(None),
                        }
                    }
                }
                PublicationPointCacheProjectionIndexState::Disabled
                | PublicationPointCacheProjectionIndexState::Uninitialized => {}
            }
        }
        let bytes = owned_bytes;
        let Some(bytes) = bytes else {
            return Ok(None);
        };
        let projection = decode_cbor::<PublicationPointCacheProjection>(
            bytes.as_ref(),
            "publication_point_cache_projection",
        )?;
        projection.validate_internal()?;
        Ok(Some(projection))
    }

    fn get_publication_point_cache_projection_from_db(
        &self,
        manifest_rsync_uri: &str,
    ) -> StorageResult<Option<PublicationPointCacheProjection>> {
        let cf = self.cf(CF_PUBLICATION_POINT_CACHE_PROJECTION)?;
        let key = publication_point_cache_projection_key(manifest_rsync_uri);
        let Some(bytes) = self
            .db
            .get_cf(cf, key.as_bytes())
            .map_err(|e| StorageError::RocksDb(e.to_string()))?
        else {
            return Ok(None);
        };
        let projection = decode_cbor::<PublicationPointCacheProjection>(
            &bytes,
            "publication_point_cache_projection",
        )?;
        projection.validate_internal()?;
        Ok(Some(projection))
    }

    fn load_publication_point_cache_projection_index(
        &self,
    ) -> StorageResult<(HashMap<String, Arc<[u8]>>, usize)> {
        let cf = self.cf(CF_PUBLICATION_POINT_CACHE_PROJECTION)?;
        let mode = IteratorMode::Start;
        let mut index = HashMap::new();
        let mut bytes_total = 0usize;
        for res in self.db.iterator_cf(cf, mode) {
            let (key, value) = res.map_err(|e| StorageError::RocksDb(e.to_string()))?;
            let Some(manifest_rsync_uri) =
                publication_point_cache_projection_key_manifest_uri(&key)
            else {
                continue;
            };
            bytes_total = bytes_total.saturating_add(value.len());
            index.insert(manifest_rsync_uri, Arc::<[u8]>::from(value.to_vec()));
        }
        Ok((index, bytes_total))
    }

    fn load_publication_point_cache_projection_entries(
        &self,
    ) -> StorageResult<Vec<(String, Vec<u8>)>> {
        let cf = self.cf(CF_PUBLICATION_POINT_CACHE_PROJECTION)?;
        let mode = IteratorMode::Start;
        let mut entries = Vec::new();
        for res in self.db.iterator_cf(cf, mode) {
            let (key, value) = res.map_err(|e| StorageError::RocksDb(e.to_string()))?;
            let Some(manifest_rsync_uri) =
                publication_point_cache_projection_key_manifest_uri(&key)
            else {
                continue;
            };
            entries.push((manifest_rsync_uri, value.to_vec()));
        }
        Ok(entries)
    }

    fn apply_publication_point_cache_projection_index_action(
        &self,
        action: PublicationPointCacheProjectionWriteAction<'_>,
    ) -> StorageResult<()> {
        match action {
            PublicationPointCacheProjectionWriteAction::Keep => Ok(()),
            PublicationPointCacheProjectionWriteAction::Write(projection) => {
                self.update_publication_point_cache_projection_index(projection)
            }
            PublicationPointCacheProjectionWriteAction::Delete { manifest_rsync_uri } => {
                self.delete_publication_point_cache_projection_index_entry(manifest_rsync_uri)
            }
        }
    }

    fn update_publication_point_cache_projection_index(
        &self,
        projection: &PublicationPointCacheProjection,
    ) -> StorageResult<()> {
        self.try_load_publication_point_cache_mmap_index_for_update("write")?;
        let mut guard = self
            .publication_point_cache_projection_index
            .lock()
            .map_err(|e| {
                StorageError::RocksDb(format!("publication point cache index lock poisoned: {e}"))
            })?;
        let bytes = encode_cbor(projection, "publication_point_cache_projection")?;
        match &mut *guard {
            PublicationPointCacheProjectionIndexState::Loaded {
                index,
                bytes: total_bytes,
            } => {
                if let Some(previous) = index.insert(
                    projection.manifest_rsync_uri.clone(),
                    Arc::<[u8]>::from(bytes.clone()),
                ) {
                    *total_bytes = total_bytes.saturating_sub(previous.len());
                }
                *total_bytes = total_bytes.saturating_add(bytes.len());
            }
            PublicationPointCacheProjectionIndexState::BuildingFromEmpty {
                index,
                bytes: total_bytes,
                limit,
            } => {
                if let Some(previous) = index.remove(&projection.manifest_rsync_uri) {
                    *total_bytes = total_bytes.saturating_sub(previous.len());
                }
                if total_bytes.saturating_add(bytes.len()) <= *limit {
                    *total_bytes += bytes.len();
                    index.insert(
                        projection.manifest_rsync_uri.clone(),
                        Arc::<[u8]>::from(bytes),
                    );
                } else {
                    *guard = PublicationPointCacheProjectionIndexState::Disabled;
                }
            }
            PublicationPointCacheProjectionIndexState::LoadedMmap {
                dirty, dirty_bytes, ..
            } => {
                if let Some(previous) = dirty.insert(
                    projection.manifest_rsync_uri.clone(),
                    Arc::<[u8]>::from(bytes.clone()),
                ) {
                    *dirty_bytes = dirty_bytes.saturating_sub(previous.len());
                }
                *dirty_bytes = dirty_bytes.saturating_add(bytes.len());
            }
            PublicationPointCacheProjectionIndexState::Uninitialized
            | PublicationPointCacheProjectionIndexState::Disabled => {}
        }
        Ok(())
    }

    fn delete_publication_point_cache_projection_index_entry(
        &self,
        manifest_rsync_uri: &str,
    ) -> StorageResult<()> {
        self.try_load_publication_point_cache_mmap_index_for_update("delete")?;
        let mut guard = self
            .publication_point_cache_projection_index
            .lock()
            .map_err(|e| {
                StorageError::RocksDb(format!("publication point cache index lock poisoned: {e}"))
            })?;
        match &mut *guard {
            PublicationPointCacheProjectionIndexState::Loaded {
                index,
                bytes: total_bytes,
            } => {
                if let Some(previous) = index.remove(manifest_rsync_uri) {
                    *total_bytes = total_bytes.saturating_sub(previous.len());
                }
            }
            PublicationPointCacheProjectionIndexState::BuildingFromEmpty {
                index,
                bytes: total_bytes,
                ..
            } => {
                if let Some(previous) = index.remove(manifest_rsync_uri) {
                    *total_bytes = total_bytes.saturating_sub(previous.len());
                }
            }
            PublicationPointCacheProjectionIndexState::LoadedMmap {
                dirty, dirty_bytes, ..
            } => {
                if let Some(previous) =
                    dirty.insert(manifest_rsync_uri.to_string(), Arc::<[u8]>::from([]))
                {
                    *dirty_bytes = dirty_bytes.saturating_sub(previous.len());
                }
            }
            PublicationPointCacheProjectionIndexState::Uninitialized
            | PublicationPointCacheProjectionIndexState::Disabled => {}
        }
        Ok(())
    }

    pub fn refresh_publication_point_cache_mmap_index(
        &self,
    ) -> StorageResult<Option<PpCacheIndexRefreshStats>> {
        if !pp_cache_raw_index_enabled() {
            return Ok(None);
        }
        self.try_load_publication_point_cache_mmap_index_for_update("refresh")?;
        enum RefreshAction {
            Entries {
                entries: Vec<(String, Vec<u8>)>,
                write_segment: bool,
                old_entries: usize,
                dirty_entries: usize,
            },
            ScanDb,
        }
        let action = {
            let guard = self
                .publication_point_cache_projection_index
                .lock()
                .map_err(|e| {
                    StorageError::RocksDb(format!(
                        "publication point cache index lock poisoned: {e}"
                    ))
                })?;
            match &*guard {
                PublicationPointCacheProjectionIndexState::LoadedMmap { mmap, dirty, .. } => {
                    RefreshAction::Entries {
                        entries: dirty
                            .iter()
                            .map(|(key, value)| (key.clone(), value.as_ref().to_vec()))
                            .collect::<Vec<_>>(),
                        write_segment: true,
                        old_entries: mmap.entries(),
                        dirty_entries: dirty.len(),
                    }
                }
                PublicationPointCacheProjectionIndexState::Loaded { index, .. }
                | PublicationPointCacheProjectionIndexState::BuildingFromEmpty { index, .. } => {
                    RefreshAction::Entries {
                        entries: index
                            .iter()
                            .map(|(key, value)| (key.clone(), value.as_ref().to_vec()))
                            .collect::<Vec<_>>(),
                        write_segment: false,
                        old_entries: 0,
                        dirty_entries: 0,
                    }
                }
                PublicationPointCacheProjectionIndexState::Disabled
                | PublicationPointCacheProjectionIndexState::Uninitialized => RefreshAction::ScanDb,
            }
        };
        let (entries, write_segment, old_entries, dirty_entries) = match action {
            RefreshAction::Entries {
                entries,
                write_segment,
                old_entries,
                dirty_entries,
            } => (entries, write_segment, old_entries, dirty_entries),
            RefreshAction::ScanDb => (
                self.load_publication_point_cache_projection_entries()?,
                false,
                0,
                0,
            ),
        };
        if entries.is_empty() {
            return Ok(None);
        }
        let current_path = self.publication_point_cache_index_dir.join("current.idx");
        let mut stats = if write_segment && current_path.exists() {
            write_pp_cache_index_segment(&self.publication_point_cache_index_dir, entries)?
        } else {
            write_pp_cache_index_atomic(&current_path, entries)?
        };
        stats.old_entries = old_entries;
        stats.dirty_entries = dirty_entries;
        crate::progress_log::emit(
            "publication_point_cache_mmap_index_refresh",
            serde_json::json!({
                "state": stats.state,
                "old_entries": stats.old_entries,
                "dirty_entries": stats.dirty_entries,
                "new_entries": stats.new_entries,
                "file_bytes": stats.file_bytes,
                "write_ms": stats.write_ms,
            }),
        );
        let directory_stats =
            pp_cache_index_directory_stats(&self.publication_point_cache_index_dir)?;
        let compaction_reason = if directory_stats.segment_count
            >= PP_CACHE_INDEX_COMPACTION_SEGMENT_THRESHOLD
        {
            Some(format!(
                "segment_count>={PP_CACHE_INDEX_COMPACTION_SEGMENT_THRESHOLD}"
            ))
        } else if directory_stats.total_file_bytes >= PP_CACHE_INDEX_COMPACTION_BYTES_THRESHOLD {
            Some(format!(
                "total_file_bytes>={PP_CACHE_INDEX_COMPACTION_BYTES_THRESHOLD}"
            ))
        } else {
            None
        };
        if let Some(reason) = compaction_reason {
            crate::progress_log::emit(
                "publication_point_cache_mmap_index_compaction",
                serde_json::json!({
                    "state": "started",
                    "reason": reason,
                    "segment_count": directory_stats.segment_count,
                    "total_file_bytes": directory_stats.total_file_bytes,
                }),
            );
            match compact_pp_cache_index(&self.publication_point_cache_index_dir) {
                Ok(mut compact_stats) => {
                    compact_stats.compaction_reason = Some(reason);
                    compact_stats.old_entries = stats.old_entries;
                    compact_stats.dirty_entries = stats.dirty_entries;
                    crate::progress_log::emit(
                        "publication_point_cache_mmap_index_compaction",
                        serde_json::json!({
                            "state": "completed",
                            "reason": compact_stats.compaction_reason,
                            "segments_before": compact_stats.compaction_segments_before,
                            "total_file_bytes_before": compact_stats.compaction_total_file_bytes_before,
                            "live_entries": compact_stats.compaction_live_entries,
                            "file_bytes": compact_stats.compaction_file_bytes,
                            "reclaimed_bytes": compact_stats.compaction_reclaimed_bytes,
                            "deleted_segments": compact_stats.compaction_deleted_segments,
                            "compaction_ms": compact_stats.compaction_ms,
                        }),
                    );
                    stats.compaction_triggered = compact_stats.compaction_triggered;
                    stats.compaction_reason = compact_stats.compaction_reason;
                    stats.compaction_segments_before = compact_stats.compaction_segments_before;
                    stats.compaction_total_file_bytes_before =
                        compact_stats.compaction_total_file_bytes_before;
                    stats.compaction_live_entries = compact_stats.compaction_live_entries;
                    stats.compaction_file_bytes = compact_stats.compaction_file_bytes;
                    stats.compaction_reclaimed_bytes = compact_stats.compaction_reclaimed_bytes;
                    stats.compaction_ms = compact_stats.compaction_ms;
                    stats.compaction_deleted_segments = compact_stats.compaction_deleted_segments;
                }
                Err(e) => {
                    let error = e.to_string();
                    crate::progress_log::emit(
                        "publication_point_cache_mmap_index_compaction",
                        serde_json::json!({
                            "state": "failed",
                            "reason": reason,
                            "segment_count": directory_stats.segment_count,
                            "total_file_bytes": directory_stats.total_file_bytes,
                            "error": error,
                        }),
                    );
                    stats.compaction_triggered = true;
                    stats.compaction_reason = Some(reason);
                    stats.compaction_segments_before = directory_stats.segment_count;
                    stats.compaction_total_file_bytes_before = directory_stats.total_file_bytes;
                    stats.compaction_error = Some(error);
                }
            }
        }
        let mut guard = self
            .publication_point_cache_projection_index
            .lock()
            .map_err(|e| {
                StorageError::RocksDb(format!("publication point cache index lock poisoned: {e}"))
            })?;
        if let PublicationPointCacheProjectionIndexState::LoadedMmap {
            dirty, dirty_bytes, ..
        } = &mut *guard
        {
            dirty.clear();
            *dirty_bytes = 0;
        }
        Ok(Some(stats))
    }

    pub fn put_transport_prefetch_snapshot(
        &self,
        snapshot: &crate::parallel::transport_prefetch::TransportPrefetchSnapshot,
    ) -> StorageResult<()> {
        let cf = self.cf(CF_TRANSPORT_PREFETCH)?;
        let value = encode_cbor(snapshot, "transport_prefetch_snapshot")?;
        self.db
            .put_cf(cf, TRANSPORT_PREFETCH_LAST_SNAPSHOT_KEY.as_bytes(), value)
            .map_err(|e| StorageError::RocksDb(e.to_string()))
    }

    pub fn get_transport_prefetch_snapshot(
        &self,
    ) -> StorageResult<Option<crate::parallel::transport_prefetch::TransportPrefetchSnapshot>> {
        let cf = self.cf(CF_TRANSPORT_PREFETCH)?;
        let Some(bytes) = self
            .db
            .get_cf(cf, TRANSPORT_PREFETCH_LAST_SNAPSHOT_KEY.as_bytes())
            .map_err(|e| StorageError::RocksDb(e.to_string()))?
        else {
            return Ok(None);
        };
        decode_cbor::<crate::parallel::transport_prefetch::TransportPrefetchSnapshot>(
            &bytes,
            "transport_prefetch_snapshot",
        )
        .map(Some)
    }

    pub fn list_vcirs(&self) -> StorageResult<Vec<ValidatedCaInstanceResult>> {
        let cf = self.cf(CF_VCIR)?;
        let mode = IteratorMode::Start;
        let mut out = Vec::new();
        for res in self.db.iterator_cf(cf, mode) {
            let (_key, bytes) = res.map_err(|e| StorageError::RocksDb(e.to_string()))?;
            let vcir = decode_cbor::<ValidatedCaInstanceResult>(&bytes, "vcir")?;
            vcir.validate_internal()?;
            out.push(vcir);
        }
        Ok(out)
    }

    /// Remove every reusable VCIR and its failed-fetch identity while retaining
    /// manifest replay metadata.  The replay metadata is a fresh-validation
    /// guard, not a reusable validation result; removing it changes the normal
    /// traversal semantics for manifests that are not selected for replay.
    ///
    /// This is intentionally narrower than [`Self::delete_vcir`].  It is used
    /// by verification-only after cloning a work-db so that it can import an
    /// explicit, contract-bound allowlist of fallback projections.
    pub fn clear_vcir_reuse_records(&self) -> StorageResult<VcirReuseRecordsCleared> {
        let vcir_cf = self.cf(CF_VCIR)?;
        let identity_cf = self.cf(CF_VCIR_FAILED_FETCH_REUSE_IDENTITY)?;
        let mut batch = WriteBatch::default();
        let mut summary = VcirReuseRecordsCleared::default();

        for entry in self.db.iterator_cf(vcir_cf, IteratorMode::Start) {
            let (key, _value) = entry.map_err(|error| StorageError::RocksDb(error.to_string()))?;
            batch.delete_cf(vcir_cf, key);
            summary.vcir_records += 1;
        }
        for entry in self.db.iterator_cf(identity_cf, IteratorMode::Start) {
            let (key, _value) = entry.map_err(|error| StorageError::RocksDb(error.to_string()))?;
            batch.delete_cf(identity_cf, key);
            summary.failed_fetch_identity_records += 1;
        }
        if summary.vcir_records > 0 || summary.failed_fetch_identity_records > 0 {
            self.write_batch(batch)?;
        }
        Ok(summary)
    }

    pub fn summarize_vcir_storage(&self) -> StorageResult<VcirStorageSummary> {
        let cf = self.cf(CF_VCIR)?;
        let mode = IteratorMode::Start;
        let mut summary = VcirStorageSummary::default();
        for res in self.db.iterator_cf(cf, mode) {
            let (_key, bytes) = res.map_err(|e| StorageError::RocksDb(e.to_string()))?;
            let vcir = decode_cbor::<ValidatedCaInstanceResult>(&bytes, "vcir")?;
            vcir.validate_internal()?;
            summary.entry_count += 1;
            let value_bytes = bytes.len() as u64;
            summary.vcir_value_bytes += value_bytes;
            if value_bytes > summary.vcir_value_bytes_max {
                summary.vcir_value_bytes_max = value_bytes;
                summary.vcir_value_bytes_max_manifest_rsync_uri =
                    Some(vcir.manifest_rsync_uri.clone());
            }
            let entry_summary = VcirStorageEntrySummary::from_vcir(&vcir, value_bytes);
            summary.core_fields.add_assign(&entry_summary.core_fields);
            summary
                .ccr_projection
                .add_assign(&entry_summary.ccr_projection);
            summary
                .child_resources
                .add_assign(&entry_summary.child_resources);
            summary.field_sizes.add_assign(&entry_summary.field_sizes);
            push_top_vcir_storage_entry(
                &mut summary.top_entries_by_vcir_value_bytes,
                entry_summary,
            );
        }
        summary.local_output_old_projection_bytes =
            summary.field_sizes.local_output_old_projection_bytes();
        summary.local_output_typed_projection_bytes =
            summary.field_sizes.local_output_typed_projection_bytes();
        summary.local_output_projection_saved_bytes =
            summary.field_sizes.local_output_projection_saved_bytes();
        Ok(summary)
    }

    pub fn delete_vcir(&self, manifest_rsync_uri: &str) -> StorageResult<()> {
        let vcir_cf = self.cf(CF_VCIR)?;
        let failed_fetch_reuse_identity_cf = self.cf(CF_VCIR_FAILED_FETCH_REUSE_IDENTITY)?;
        let replay_cf = self.cf(CF_MANIFEST_REPLAY_META)?;
        let projection_cf = self.cf(CF_ROA_CACHE_PROJECTION)?;
        let mut batch = WriteBatch::default();
        let key = vcir_key(manifest_rsync_uri);
        batch.delete_cf(vcir_cf, key.as_bytes());
        let failed_fetch_reuse_identity_key =
            vcir_failed_fetch_reuse_identity_key(manifest_rsync_uri);
        batch.delete_cf(
            failed_fetch_reuse_identity_cf,
            failed_fetch_reuse_identity_key.as_bytes(),
        );
        let replay_key = manifest_replay_meta_key(manifest_rsync_uri);
        batch.delete_cf(replay_cf, replay_key.as_bytes());
        let projection_key = roa_cache_projection_key(manifest_rsync_uri);
        batch.delete_cf(projection_cf, projection_key.as_bytes());
        self.write_batch(batch)
    }

    pub fn put_rrdp_source_record(&self, record: &RrdpSourceRecord) -> StorageResult<()> {
        record.validate_internal()?;
        let cf = self.cf(CF_RRDP_SOURCE)?;
        let key = rrdp_source_key(&record.notify_uri);
        let value = encode_cbor(record, "rrdp_source")?;
        self.db
            .put_cf(cf, key.as_bytes(), value)
            .map_err(|e| StorageError::RocksDb(e.to_string()))?;
        Ok(())
    }

    pub fn get_rrdp_source_record(
        &self,
        notify_uri: &str,
    ) -> StorageResult<Option<RrdpSourceRecord>> {
        let cf = self.cf(CF_RRDP_SOURCE)?;
        let key = rrdp_source_key(notify_uri);
        let Some(bytes) = self
            .db
            .get_cf(cf, key.as_bytes())
            .map_err(|e| StorageError::RocksDb(e.to_string()))?
        else {
            return Ok(None);
        };
        let record = decode_cbor::<RrdpSourceRecord>(&bytes, "rrdp_source")?;
        record.validate_internal()?;
        Ok(Some(record))
    }

    pub fn put_rrdp_source_member_record(
        &self,
        record: &RrdpSourceMemberRecord,
    ) -> StorageResult<()> {
        record.validate_internal()?;
        let cf = self.cf(CF_RRDP_SOURCE_MEMBER)?;
        let key = rrdp_source_member_key(&record.notify_uri, &record.rsync_uri);
        let value = encode_cbor(record, "rrdp_source_member")?;
        self.db
            .put_cf(cf, key.as_bytes(), value)
            .map_err(|e| StorageError::RocksDb(e.to_string()))?;
        Ok(())
    }

    pub fn get_rrdp_source_member_record(
        &self,
        notify_uri: &str,
        rsync_uri: &str,
    ) -> StorageResult<Option<RrdpSourceMemberRecord>> {
        let cf = self.cf(CF_RRDP_SOURCE_MEMBER)?;
        let key = rrdp_source_member_key(notify_uri, rsync_uri);
        let Some(bytes) = self
            .db
            .get_cf(cf, key.as_bytes())
            .map_err(|e| StorageError::RocksDb(e.to_string()))?
        else {
            return Ok(None);
        };
        let record = decode_cbor::<RrdpSourceMemberRecord>(&bytes, "rrdp_source_member")?;
        record.validate_internal()?;
        Ok(Some(record))
    }

    pub fn list_rrdp_source_member_records(
        &self,
        notify_uri: &str,
    ) -> StorageResult<Vec<RrdpSourceMemberRecord>> {
        let cf = self.cf(CF_RRDP_SOURCE_MEMBER)?;
        let prefix = rrdp_source_member_prefix(notify_uri);
        let mode = IteratorMode::From(prefix.as_bytes(), Direction::Forward);
        self.db
            .iterator_cf(cf, mode)
            .take_while(|res| match res {
                Ok((key, _)) => key.starts_with(prefix.as_bytes()),
                Err(_) => false,
            })
            .map(|res| {
                let (_key, value) = res.map_err(|e| StorageError::RocksDb(e.to_string()))?;
                let record = decode_cbor::<RrdpSourceMemberRecord>(&value, "rrdp_source_member")?;
                record.validate_internal()?;
                Ok(record)
            })
            .collect()
    }

    pub fn list_current_rrdp_source_members(
        &self,
        notify_uri: &str,
    ) -> StorageResult<Vec<RrdpSourceMemberRecord>> {
        let mut records = self.list_rrdp_source_member_records(notify_uri)?;
        records.retain(|record| record.present);
        records.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri));
        Ok(records)
    }

    pub fn is_current_rrdp_source_member(
        &self,
        notify_uri: &str,
        rsync_uri: &str,
    ) -> StorageResult<bool> {
        Ok(matches!(
            self.get_rrdp_source_member_record(notify_uri, rsync_uri)?,
            Some(record) if record.present
        ))
    }

    pub fn load_current_object_bytes_by_uri(
        &self,
        rsync_uri: &str,
    ) -> StorageResult<Option<Vec<u8>>> {
        Ok(self
            .load_current_object_with_hash_by_uri(rsync_uri)?
            .map(|obj| obj.bytes))
    }

    pub fn load_current_object_with_hash_by_uri(
        &self,
        rsync_uri: &str,
    ) -> StorageResult<Option<CurrentObjectWithHash>> {
        let Some(view) = self.get_repository_view_entry(rsync_uri)? else {
            return Ok(None);
        };

        match view.state {
            RepositoryViewState::Withdrawn => Ok(None),
            RepositoryViewState::Present | RepositoryViewState::Replaced => {
                let hash = view
                    .current_hash
                    .as_deref()
                    .ok_or(StorageError::InvalidData {
                        entity: "repository_view",
                        detail: format!("current_hash missing for current object URI: {rsync_uri}"),
                    })?;
                let bytes = self
                    .get_blob_bytes(hash)?
                    .ok_or(StorageError::InvalidData {
                        entity: "repository_view",
                        detail: format!(
                            "blob bytes missing for current object URI: {rsync_uri} (hash={hash})"
                        ),
                    })?;
                let current_hash = decode_sha256_hex_32("repository_view.current_hash", hash)?;
                Ok(Some(CurrentObjectWithHash {
                    current_hash_hex: hash.to_ascii_lowercase(),
                    current_hash,
                    bytes,
                }))
            }
        }
    }

    pub fn put_rrdp_uri_owner_record(&self, record: &RrdpUriOwnerRecord) -> StorageResult<()> {
        record.validate_internal()?;
        let cf = self.cf(CF_RRDP_URI_OWNER)?;
        let key = rrdp_uri_owner_key(&record.rsync_uri);
        let value = encode_cbor(record, "rrdp_uri_owner")?;
        self.db
            .put_cf(cf, key.as_bytes(), value)
            .map_err(|e| StorageError::RocksDb(e.to_string()))?;
        Ok(())
    }

    pub fn get_rrdp_uri_owner_record(
        &self,
        rsync_uri: &str,
    ) -> StorageResult<Option<RrdpUriOwnerRecord>> {
        let cf = self.cf(CF_RRDP_URI_OWNER)?;
        let key = rrdp_uri_owner_key(rsync_uri);
        let Some(bytes) = self
            .db
            .get_cf(cf, key.as_bytes())
            .map_err(|e| StorageError::RocksDb(e.to_string()))?
        else {
            return Ok(None);
        };
        let record = decode_cbor::<RrdpUriOwnerRecord>(&bytes, "rrdp_uri_owner")?;
        record.validate_internal()?;
        Ok(Some(record))
    }

    pub fn delete_rrdp_uri_owner_record(&self, rsync_uri: &str) -> StorageResult<()> {
        let cf = self.cf(CF_RRDP_URI_OWNER)?;
        let key = rrdp_uri_owner_key(rsync_uri);
        self.db
            .delete_cf(cf, key.as_bytes())
            .map_err(|e| StorageError::RocksDb(e.to_string()))?;
        Ok(())
    }

    #[allow(dead_code)]

    pub fn write_batch(&self, batch: WriteBatch) -> StorageResult<()> {
        self.db
            .write(batch)
            .map_err(|e| StorageError::RocksDb(e.to_string()))?;
        Ok(())
    }
}

fn verify_repository_blob_batch(
    store: &RocksStore,
    batch: &[(String, String)],
    summary: &mut RepositoryBlobVerificationSummary,
) -> StorageResult<()> {
    let hashes = batch
        .iter()
        .map(|(_, hash)| hash.clone())
        .collect::<Vec<_>>();
    let blobs = store.get_blob_bytes_batch(&hashes)?;
    for ((uri, expected_hash), blob) in batch.iter().zip(blobs.into_iter()) {
        let bytes = blob.as_ref().ok_or(StorageError::InvalidData {
            entity: "repository_blob_verification",
            detail: format!("blob missing for URI {uri} (hash={expected_hash})"),
        })?;
        let actual_hash = hex::encode(compute_sha256_32(bytes));
        if !actual_hash.eq_ignore_ascii_case(expected_hash) {
            return Err(StorageError::InvalidData {
                entity: "repository_blob_verification",
                detail: format!(
                    "blob hash mismatch for URI {uri}: expected={expected_hash}, actual={actual_hash}"
                ),
            });
        }
        summary.current_objects += 1;
        summary.bytes_verified += bytes.len() as u64;
    }
    summary.batches += 1;
    Ok(())
}

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