use crate::analysis::timing::TimingHandle;
use crate::audit::AuditDownloadKind;
use crate::audit_downloads::DownloadLogHandle;
use crate::current_repo_index::CurrentRepoIndexHandle;
use crate::fetch::rsync::{RsyncFetchError, RsyncFetcher};
use crate::policy::{Policy, SyncPreference};
use crate::replay::archive::{ReplayArchiveIndex, ReplayTransport};
use crate::replay::delta_archive::{ReplayDeltaArchiveIndex, ReplayDeltaRrdpKind};
use crate::report::{RfcRef, Warning};
use crate::storage::RocksStore;
use crate::sync::rrdp::sync_from_notification_with_timing_and_download_log;
use crate::sync::rrdp::{Fetcher as HttpFetcher, RrdpSyncError, load_rrdp_local_state};
use crate::sync::store_projection::{
    build_repository_view_present_entry, build_repository_view_withdrawn_entry,
    prepare_repo_bytes_batch_owned,
};
use std::collections::HashSet;

#[cfg(test)]
use crate::storage::RrdpSourceSyncState;
#[cfg(test)]
use crate::sync::rrdp::persist_rrdp_local_state;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RepoSyncSource {
    Rrdp,
    Rsync,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RepoSyncPhase {
    RrdpOk,
    RrdpFailedRsyncOk,
    RsyncOnlyOk,
    ReplayRrdpOk,
    ReplayRsyncOk,
    ReplayNoopRrdp,
    ReplayNoopRsync,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RepoSyncResult {
    pub source: RepoSyncSource,
    pub phase: RepoSyncPhase,
    pub objects_written: usize,
    pub warnings: Vec<Warning>,
}

#[derive(Debug, thiserror::Error)]
pub enum RepoSyncError {
    #[error("RRDP sync failed: {0}")]
    Rrdp(#[from] RrdpSyncError),

    #[error("rsync fallback failed: {0}")]
    Rsync(#[from] RsyncFetchError),

    #[error("replay sync error: {0}")]
    Replay(String),

    #[error("storage error: {0}")]
    Storage(String),
}

/// Sync a publication point into the current repository view.
///
/// v1 behavior:
/// - If `rrdp_notification_uri` is present and `policy.sync_preference` is `rrdp_then_rsync`,
///   try RRDP snapshot sync first (RFC 8182 §3.4.1-§3.4.3).
/// - On RRDP failure, fall back to rsync (RFC 8182 §3.4.5).
/// - If `sync_preference` is `rsync_only` or there is no RRDP URI, use rsync.
pub fn sync_publication_point(
    store: &RocksStore,
    policy: &Policy,
    rrdp_notification_uri: Option<&str>,
    rsync_base_uri: &str,
    http_fetcher: &dyn HttpFetcher,
    rsync_fetcher: &dyn RsyncFetcher,
    timing: Option<&TimingHandle>,
    download_log: Option<&DownloadLogHandle>,
) -> Result<RepoSyncResult, RepoSyncError> {
    match (policy.sync_preference, rrdp_notification_uri) {
        (SyncPreference::RrdpThenRsync, Some(notification_uri)) => {
            match try_rrdp_sync_with_retry(
                store,
                notification_uri,
                None,
                http_fetcher,
                timing,
                download_log,
            ) {
                Ok(written) => {
                    if let Some(t) = timing.as_ref() {
                        t.record_count("repo_sync_rrdp_ok_total", 1);
                        t.record_count("repo_sync_rrdp_objects_written_total", written as u64);
                    }
                    crate::progress_log::emit(
                        "repo_sync_rrdp_ok",
                        serde_json::json!({
                            "notify_uri": notification_uri,
                            "objects_written": written,
                        }),
                    );
                    Ok(RepoSyncResult {
                        source: RepoSyncSource::Rrdp,
                        phase: RepoSyncPhase::RrdpOk,
                        objects_written: written,
                        warnings: Vec::new(),
                    })
                }
                Err(err) => {
                    if let Some(t) = timing.as_ref() {
                        t.record_count("repo_sync_rrdp_failed_total", 1);
                    }
                    crate::progress_log::emit(
                        "rrdp_fallback_rsync",
                        serde_json::json!({
                            "notify_uri": notification_uri,
                            "rsync_base_uri": rsync_base_uri,
                            "rrdp_error": err.to_string(),
                        }),
                    );
                    crate::progress_log::emit(
                        "rrdp_failed_fallback_rsync",
                        serde_json::json!({
                            "notify_uri": notification_uri,
                            "rsync_base_uri": rsync_base_uri,
                            "rrdp_error": err.to_string(),
                        }),
                    );
                    let warnings = vec![
                        Warning::new(format!("RRDP failed; falling back to rsync: {err}"))
                            .with_rfc_refs(&[RfcRef("RFC 8182 §3.4.5")])
                            .with_context(notification_uri),
                    ];
                    let written = rsync_sync_into_current_store(
                        store,
                        rsync_base_uri,
                        None,
                        rsync_fetcher,
                        timing,
                        download_log,
                    )?;
                    if let Some(t) = timing.as_ref() {
                        t.record_count("repo_sync_rsync_peer_aligned_profile_total", 1);
                    }
                    if let Some(t) = timing.as_ref() {
                        t.record_count("repo_sync_rsync_fallback_ok_total", 1);
                        t.record_count("repo_sync_rsync_objects_written_total", written as u64);
                    }
                    Ok(RepoSyncResult {
                        source: RepoSyncSource::Rsync,
                        phase: RepoSyncPhase::RrdpFailedRsyncOk,
                        objects_written: written,
                        warnings,
                    })
                }
            }
        }
        _ => {
            let written = rsync_sync_into_current_store(
                store,
                rsync_base_uri,
                None,
                rsync_fetcher,
                timing,
                download_log,
            )?;
            if let Some(t) = timing.as_ref() {
                t.record_count("repo_sync_rsync_peer_aligned_profile_total", 1);
            }
            crate::progress_log::emit(
                "repo_sync_rsync_direct",
                serde_json::json!({
                    "rsync_base_uri": rsync_base_uri,
                    "objects_written": written,
                }),
            );
            if let Some(t) = timing.as_ref() {
                t.record_count("repo_sync_rsync_direct_total", 1);
                t.record_count("repo_sync_rsync_objects_written_total", written as u64);
            }
            Ok(RepoSyncResult {
                source: RepoSyncSource::Rsync,
                phase: RepoSyncPhase::RsyncOnlyOk,
                objects_written: written,
                warnings: Vec::new(),
            })
        }
    }
}

pub fn sync_publication_point_replay(
    store: &RocksStore,
    replay_index: &ReplayArchiveIndex,
    rrdp_notification_uri: Option<&str>,
    rsync_base_uri: &str,
    http_fetcher: &dyn HttpFetcher,
    rsync_fetcher: &dyn RsyncFetcher,
    timing: Option<&TimingHandle>,
    download_log: Option<&DownloadLogHandle>,
) -> Result<RepoSyncResult, RepoSyncError> {
    match resolve_replay_transport(replay_index, rrdp_notification_uri, rsync_base_uri)? {
        ReplayResolvedTransport::Rrdp(notification_uri) => {
            let written = try_rrdp_sync_with_retry(
                store,
                notification_uri,
                None,
                http_fetcher,
                timing,
                download_log,
            )?;
            if let Some(t) = timing.as_ref() {
                t.record_count("repo_sync_rrdp_ok_total", 1);
                t.record_count("repo_sync_rrdp_objects_written_total", written as u64);
            }
            Ok(RepoSyncResult {
                source: RepoSyncSource::Rrdp,
                phase: RepoSyncPhase::ReplayRrdpOk,
                objects_written: written,
                warnings: Vec::new(),
            })
        }
        ReplayResolvedTransport::Rsync => {
            let written = rsync_sync_into_current_store(
                store,
                rsync_base_uri,
                None,
                rsync_fetcher,
                timing,
                download_log,
            )?;
            if let Some(t) = timing.as_ref() {
                t.record_count("repo_sync_rsync_direct_total", 1);
                t.record_count("repo_sync_rsync_objects_written_total", written as u64);
            }
            Ok(RepoSyncResult {
                source: RepoSyncSource::Rsync,
                phase: RepoSyncPhase::ReplayRsyncOk,
                objects_written: written,
                warnings: Vec::new(),
            })
        }
    }
}

pub fn sync_publication_point_replay_delta(
    store: &RocksStore,
    delta_index: &ReplayDeltaArchiveIndex,
    rrdp_notification_uri: Option<&str>,
    rsync_base_uri: &str,
    http_fetcher: &dyn HttpFetcher,
    rsync_fetcher: &dyn RsyncFetcher,
    timing: Option<&TimingHandle>,
    download_log: Option<&DownloadLogHandle>,
) -> Result<RepoSyncResult, RepoSyncError> {
    match resolve_replay_delta_transport(store, delta_index, rrdp_notification_uri, rsync_base_uri)?
    {
        ReplayDeltaResolvedTransport::Rrdp(notification_uri) => {
            let written = try_rrdp_sync_with_retry(
                store,
                notification_uri,
                None,
                http_fetcher,
                timing,
                download_log,
            )?;
            if let Some(t) = timing.as_ref() {
                t.record_count("repo_sync_rrdp_ok_total", 1);
                t.record_count("repo_sync_rrdp_objects_written_total", written as u64);
            }
            Ok(RepoSyncResult {
                source: RepoSyncSource::Rrdp,
                phase: RepoSyncPhase::ReplayRrdpOk,
                objects_written: written,
                warnings: Vec::new(),
            })
        }
        ReplayDeltaResolvedTransport::Rsync => {
            let written = rsync_sync_into_current_store(
                store,
                rsync_base_uri,
                None,
                rsync_fetcher,
                timing,
                download_log,
            )?;
            if let Some(t) = timing.as_ref() {
                t.record_count("repo_sync_rsync_direct_total", 1);
                t.record_count("repo_sync_rsync_objects_written_total", written as u64);
            }
            Ok(RepoSyncResult {
                source: RepoSyncSource::Rsync,
                phase: RepoSyncPhase::ReplayRsyncOk,
                objects_written: written,
                warnings: Vec::new(),
            })
        }
        ReplayDeltaResolvedTransport::Noop(source) => Ok(RepoSyncResult {
            source,
            phase: match source {
                RepoSyncSource::Rrdp => RepoSyncPhase::ReplayNoopRrdp,
                RepoSyncSource::Rsync => RepoSyncPhase::ReplayNoopRsync,
            },
            objects_written: 0,
            warnings: Vec::new(),
        }),
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ReplayResolvedTransport<'a> {
    Rrdp(&'a str),
    Rsync,
}

enum ReplayDeltaResolvedTransport<'a> {
    Rrdp(&'a str),
    Rsync,
    Noop(RepoSyncSource),
}

fn resolve_replay_transport<'a>(
    replay_index: &'a ReplayArchiveIndex,
    rrdp_notification_uri: Option<&'a str>,
    rsync_base_uri: &str,
) -> Result<ReplayResolvedTransport<'a>, RepoSyncError> {
    if let Some(notification_uri) = rrdp_notification_uri {
        let lock = replay_index.rrdp_lock(notification_uri).ok_or_else(|| {
            RepoSyncError::Replay(format!(
                "replay RRDP lock missing for notification URI: {notification_uri}"
            ))
        })?;
        return Ok(match lock.transport {
            ReplayTransport::Rrdp => ReplayResolvedTransport::Rrdp(notification_uri),
            ReplayTransport::Rsync => ReplayResolvedTransport::Rsync,
        });
    }

    replay_index
        .resolve_rsync_module_for_base_uri(rsync_base_uri)
        .map_err(|e| RepoSyncError::Replay(e.to_string()))?;
    Ok(ReplayResolvedTransport::Rsync)
}

fn resolve_replay_delta_transport<'a>(
    store: &RocksStore,
    delta_index: &'a ReplayDeltaArchiveIndex,
    rrdp_notification_uri: Option<&'a str>,
    rsync_base_uri: &str,
) -> Result<ReplayDeltaResolvedTransport<'a>, RepoSyncError> {
    if let Some(notification_uri) = rrdp_notification_uri {
        let repo = delta_index.rrdp_repo(notification_uri).ok_or_else(|| {
            RepoSyncError::Replay(format!(
                "delta replay RRDP entry missing for notification URI: {notification_uri}"
            ))
        })?;
        validate_delta_replay_base_state_for_repo(store, notification_uri, &repo.transition.base)?;
        return match repo.transition.kind {
            ReplayDeltaRrdpKind::Delta => Ok(ReplayDeltaResolvedTransport::Rrdp(notification_uri)),
            ReplayDeltaRrdpKind::Unchanged => Ok(ReplayDeltaResolvedTransport::Noop(
                match repo.transition.target.transport {
                    ReplayTransport::Rrdp => RepoSyncSource::Rrdp,
                    ReplayTransport::Rsync => RepoSyncSource::Rsync,
                },
            )),
            ReplayDeltaRrdpKind::FallbackRsync => Ok(ReplayDeltaResolvedTransport::Rsync),
            ReplayDeltaRrdpKind::SessionReset => Err(RepoSyncError::Replay(format!(
                "delta replay kind session-reset requires fresh full replay for {notification_uri}"
            ))),
            ReplayDeltaRrdpKind::Gap => Err(RepoSyncError::Replay(format!(
                "delta replay kind gap requires fresh full replay for {notification_uri}"
            ))),
        };
    }

    delta_index
        .resolve_rsync_module_for_base_uri(rsync_base_uri)
        .map_err(|e| RepoSyncError::Replay(e.to_string()))?;
    Ok(ReplayDeltaResolvedTransport::Rsync)
}

fn validate_delta_replay_base_state_for_repo(
    store: &RocksStore,
    notification_uri: &str,
    base: &crate::replay::delta_archive::ReplayDeltaRrdpState,
) -> Result<(), RepoSyncError> {
    match base.transport {
        ReplayTransport::Rrdp => {
            let state = load_rrdp_local_state(store, notification_uri)
                .map_err(RepoSyncError::Storage)?
                .ok_or_else(|| {
                    RepoSyncError::Replay(format!(
                        "delta replay base state missing for {notification_uri}: expected RRDP session={} serial={}",
                        base.session.as_deref().unwrap_or("<none>"),
                        base.serial
                            .map(|v| v.to_string())
                            .unwrap_or_else(|| "<none>".to_string())
                    ))
                })?;
            let expected_session = base.session.as_deref().unwrap_or("");
            let expected_serial = base.serial.unwrap_or_default();
            if state.session_id != expected_session || state.serial != expected_serial {
                return Err(RepoSyncError::Replay(format!(
                    "delta replay base state mismatch for {notification_uri}: expected session={} serial={}, actual session={} serial={}",
                    expected_session, expected_serial, state.session_id, state.serial
                )));
            }
        }
        ReplayTransport::Rsync => {}
    }
    Ok(())
}

fn try_rrdp_sync(
    store: &RocksStore,
    notification_uri: &str,
    current_repo_index: Option<&CurrentRepoIndexHandle>,
    http_fetcher: &dyn HttpFetcher,
    timing: Option<&TimingHandle>,
    download_log: Option<&DownloadLogHandle>,
) -> Result<usize, RrdpSyncError> {
    let notification_xml = {
        let _step = timing
            .as_ref()
            .map(|t| t.span_rrdp_repo_step(notification_uri, "fetch_notification"));
        let _total = timing
            .as_ref()
            .map(|t| t.span_phase("rrdp_fetch_notification_total"));
        let mut dl_span = download_log
            .map(|dl| dl.span_download(AuditDownloadKind::RrdpNotification, notification_uri));
        match http_fetcher.fetch(notification_uri) {
            Ok(v) => {
                if let Some(t) = timing.as_ref() {
                    t.record_count("rrdp_notification_fetch_ok_total", 1);
                }
                if let Some(s) = dl_span.as_mut() {
                    s.set_bytes(v.len() as u64);
                    s.set_ok();
                }
                v
            }
            Err(e) => {
                if let Some(t) = timing.as_ref() {
                    t.record_count("rrdp_notification_fetch_fail_total", 1);
                }
                if let Some(s) = dl_span.as_mut() {
                    s.set_err(e.clone());
                }
                return Err(RrdpSyncError::Fetch(e));
            }
        }
    };
    if let Some(t) = timing.as_ref() {
        t.record_count(
            "rrdp_notification_bytes_total",
            notification_xml.len() as u64,
        );
    }

    sync_from_notification_with_timing_and_download_log(
        store,
        notification_uri,
        current_repo_index,
        &notification_xml,
        http_fetcher,
        timing,
        download_log,
    )
}

fn is_retryable_http_fetch_error(msg: &str) -> bool {
    if msg.contains("http request failed:") || msg.contains("http read body failed:") {
        return true;
    }
    let Some(rest) = msg.strip_prefix("http status ") else {
        return false;
    };
    let code = rest
        .trim()
        .split_whitespace()
        .next()
        .and_then(|s| s.parse::<u16>().ok())
        .unwrap_or(0);
    code == 408 || code == 429 || (500..600).contains(&code)
}

fn try_rrdp_sync_with_retry(
    store: &RocksStore,
    notification_uri: &str,
    current_repo_index: Option<&CurrentRepoIndexHandle>,
    http_fetcher: &dyn HttpFetcher,
    timing: Option<&TimingHandle>,
    download_log: Option<&DownloadLogHandle>,
) -> Result<usize, RrdpSyncError> {
    let attempt = 1usize;
    crate::progress_log::emit(
        "rrdp_sync_attempt",
        serde_json::json!({
            "notify_uri": notification_uri,
            "attempt": attempt,
        }),
    );
    if let Some(t) = timing.as_ref() {
        t.record_count("rrdp_retry_attempt_total", 1);
    }

    match try_rrdp_sync(
        store,
        notification_uri,
        current_repo_index,
        http_fetcher,
        timing,
        download_log,
    ) {
        Ok(written) => {
            crate::progress_log::emit(
                "rrdp_sync_success",
                serde_json::json!({
                    "notify_uri": notification_uri,
                    "attempt": attempt,
                    "objects_written": written,
                }),
            );
            Ok(written)
        }
        Err(err) => {
            let retryable = match &err {
                RrdpSyncError::Fetch(msg) => is_retryable_http_fetch_error(msg),
                _ => false,
            };
            crate::progress_log::emit(
                "rrdp_sync_failed",
                serde_json::json!({
                    "notify_uri": notification_uri,
                    "attempt": attempt,
                    "retryable": retryable,
                    "error": err.to_string(),
                }),
            );
            if let Some(t) = timing.as_ref() {
                match &err {
                    RrdpSyncError::Fetch(_) => t.record_count("rrdp_failed_fetch_total", 1),
                    RrdpSyncError::Rrdp(_) => t.record_count("rrdp_failed_protocol_total", 1),
                    RrdpSyncError::Storage(_) => t.record_count("rrdp_failed_storage_total", 1),
                }
            }
            Err(err)
        }
    }
}

pub(crate) fn run_rrdp_transport(
    store: &RocksStore,
    notification_uri: &str,
    current_repo_index: Option<&CurrentRepoIndexHandle>,
    http_fetcher: &dyn HttpFetcher,
    timing: Option<&TimingHandle>,
    download_log: Option<&DownloadLogHandle>,
) -> Result<usize, RrdpSyncError> {
    try_rrdp_sync_with_retry(
        store,
        notification_uri,
        current_repo_index,
        http_fetcher,
        timing,
        download_log,
    )
}

fn rsync_sync_into_current_store(
    store: &RocksStore,
    rsync_base_uri: &str,
    current_repo_index: Option<&CurrentRepoIndexHandle>,
    rsync_fetcher: &dyn RsyncFetcher,
    timing: Option<&TimingHandle>,
    download_log: Option<&DownloadLogHandle>,
) -> Result<usize, RepoSyncError> {
    let started = std::time::Instant::now();
    let sync_scope_uri = rsync_fetcher.dedup_key(rsync_base_uri);
    crate::progress_log::emit(
        "rsync_sync_start",
        serde_json::json!({
            "rsync_base_uri": rsync_base_uri,
            "sync_scope_uri": &sync_scope_uri,
        }),
    );
    let _s = timing
        .as_ref()
        .map(|t| t.span_rrdp_repo_step(rsync_base_uri, "rsync_fetch_objects"));
    let _p = timing.as_ref().map(|t| t.span_phase("rsync_fetch_total"));
    let mut dl_span =
        download_log.map(|dl| dl.span_download(AuditDownloadKind::Rsync, rsync_base_uri));
    let mut new_set: HashSet<String> = HashSet::new();
    let mut fetched_objects: Vec<(String, Vec<u8>)> = Vec::new();
    let (object_count, bytes_total) =
        match rsync_fetcher.visit_objects(rsync_base_uri, &mut |uri, bytes| {
            new_set.insert(uri.clone());
            fetched_objects.push((uri, bytes));
            Ok(())
        }) {
            Ok(v) => {
                if let Some(s) = dl_span.as_mut() {
                    s.set_objects(v.0 as u64, v.1);
                    s.set_bytes(v.1);
                    s.set_ok();
                }
                v
            }
            Err(e) => {
                if let Some(s) = dl_span.as_mut() {
                    s.set_err(e.to_string());
                }
                return Err(e.into());
            }
        };
    crate::progress_log::emit(
        "rsync_sync_fetch_done",
        serde_json::json!({
            "rsync_base_uri": rsync_base_uri,
            "sync_scope_uri": &sync_scope_uri,
            "object_count": object_count,
            "bytes_total": bytes_total,
            "duration_ms": started.elapsed().as_millis() as u64,
        }),
    );
    if let Some(t) = timing.as_ref() {
        t.record_count("rsync_objects_fetched_total", object_count as u64);
        t.record_count("rsync_objects_bytes_total", bytes_total);
    }
    drop(_p);

    let existing_view = store
        .list_repository_view_entries_with_prefix(&sync_scope_uri)
        .map_err(|e| RepoSyncError::Storage(e.to_string()))?;

    let _proj = timing
        .as_ref()
        .map(|t| t.span_phase("rsync_write_current_store_total"));
    let prepared_bytes =
        prepare_repo_bytes_batch_owned(fetched_objects).map_err(RepoSyncError::Storage)?;
    let mut repository_view_entries = Vec::new();
    for entry in existing_view {
        if !new_set.contains(&entry.rsync_uri) {
            repository_view_entries.push(build_repository_view_withdrawn_entry(
                &sync_scope_uri,
                &entry.rsync_uri,
                entry.current_hash,
            ));
        }
    }

    for uri in &new_set {
        let current_hash = prepared_bytes
            .uri_to_hash
            .get(uri)
            .cloned()
            .ok_or_else(|| {
                RepoSyncError::Storage(format!("missing raw_by_hash mapping for {uri}"))
            })?;
        repository_view_entries.push(build_repository_view_present_entry(
            &sync_scope_uri,
            uri,
            &current_hash,
        ));
    }

    store
        .put_blob_bytes_batch(&prepared_bytes.blobs_to_write)
        .map_err(|e| RepoSyncError::Storage(e.to_string()))?;
    store
        .put_projection_batch(&repository_view_entries, &[], &[])
        .map_err(|e| RepoSyncError::Storage(e.to_string()))?;
    if let Some(index) = current_repo_index {
        index
            .write()
            .map_err(|_| RepoSyncError::Storage("current repo index lock poisoned".to_string()))?
            .apply_repository_view_entries(&repository_view_entries)
            .map_err(RepoSyncError::Storage)?;
    }

    let total_duration_ms = started.elapsed().as_millis() as u64;
    crate::progress_log::emit(
        "rsync_sync_done",
        serde_json::json!({
            "rsync_base_uri": rsync_base_uri,
            "sync_scope_uri": &sync_scope_uri,
            "object_count": object_count,
            "bytes_total": bytes_total,
            "duration_ms": total_duration_ms,
        }),
    );
    if (total_duration_ms as f64) / 1000.0 >= crate::progress_log::slow_threshold_secs() {
        crate::progress_log::emit(
            "rsync_sync_slow",
            serde_json::json!({
                "rsync_base_uri": rsync_base_uri,
                "sync_scope_uri": &sync_scope_uri,
                "object_count": object_count,
                "bytes_total": bytes_total,
                "duration_ms": total_duration_ms,
            }),
        );
    }

    Ok(object_count)
}

pub(crate) fn run_rsync_transport(
    store: &RocksStore,
    rsync_base_uri: &str,
    current_repo_index: Option<&CurrentRepoIndexHandle>,
    rsync_fetcher: &dyn RsyncFetcher,
    timing: Option<&TimingHandle>,
    download_log: Option<&DownloadLogHandle>,
) -> Result<usize, RepoSyncError> {
    rsync_sync_into_current_store(
        store,
        rsync_base_uri,
        current_repo_index,
        rsync_fetcher,
        timing,
        download_log,
    )
}

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