use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use std::time::Duration;

use super::config::ParallelPhase1Config;
use super::types::{
    RepoSyncResultEnvelope, RepoSyncTask, RepoTransportMode, RepoTransportResultEnvelope,
    RepoTransportResultKind, RepoTransportTask,
};
use crate::analysis::timing::TimingHandle;
use crate::audit_downloads::DownloadLogHandle;
use crate::current_repo_index::CurrentRepoIndexHandle;
use crate::fetch::rsync::RsyncFetcher;
use crate::fetch::rsync_system::{
    RsyncFailFastProfile, with_scoped_rsync_fail_fast_profile, with_scoped_rsync_timeout_override,
};
use crate::policy::Policy;
use crate::storage::RocksStore;
use crate::sync::repo::{
    RepoSyncSource, run_rrdp_transport, run_rsync_transport, sync_publication_point,
};
use crate::sync::rrdp::Fetcher;

const RETRY_SHORT_TIMEOUT: Duration = Duration::from_secs(1);

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RepoWorkerPoolConfig {
    pub max_workers: usize,
}

impl From<&ParallelPhase1Config> for RepoWorkerPoolConfig {
    fn from(value: &ParallelPhase1Config) -> Self {
        Self {
            max_workers: value.max_repo_sync_workers_global,
        }
    }
}

pub trait RepoSyncExecutor: Send + Sync + 'static {
    fn execute(&self, task: RepoSyncTask) -> RepoSyncResultEnvelope;
}

pub trait RepoTransportExecutor: Send + Sync + 'static {
    fn execute_transport(&self, task: RepoTransportTask) -> RepoTransportResultEnvelope;
}

pub struct LiveRrdpTransportExecutor<H: Fetcher> {
    store: Arc<RocksStore>,
    current_repo_index: CurrentRepoIndexHandle,
    http_fetcher: Arc<H>,
    timing: Option<TimingHandle>,
    download_log: Option<DownloadLogHandle>,
}

impl<H: Fetcher> LiveRrdpTransportExecutor<H> {
    pub fn new(
        store: Arc<RocksStore>,
        current_repo_index: CurrentRepoIndexHandle,
        http_fetcher: Arc<H>,
        timing: Option<TimingHandle>,
        download_log: Option<DownloadLogHandle>,
    ) -> Self {
        Self {
            store,
            current_repo_index,
            http_fetcher,
            timing,
            download_log,
        }
    }
}

impl<H: Fetcher + 'static> RepoTransportExecutor for LiveRrdpTransportExecutor<H> {
    fn execute_transport(&self, task: RepoTransportTask) -> RepoTransportResultEnvelope {
        let started = std::time::Instant::now();
        debug_assert_eq!(task.mode, RepoTransportMode::Rrdp);
        let notification_uri = task
            .repo_identity
            .notification_uri
            .as_deref()
            .expect("rrdp transport requires notification uri");
        let sync_result = if task.retry_short_timeout {
            crate::fetch::http::with_scoped_http_timeout_override(RETRY_SHORT_TIMEOUT, || {
                run_rrdp_transport(
                    self.store.as_ref(),
                    notification_uri,
                    Some(&self.current_repo_index),
                    self.http_fetcher.as_ref(),
                    self.timing.as_ref(),
                    self.download_log.as_ref(),
                )
            })
        } else {
            run_rrdp_transport(
                self.store.as_ref(),
                notification_uri,
                Some(&self.current_repo_index),
                self.http_fetcher.as_ref(),
                self.timing.as_ref(),
                self.download_log.as_ref(),
            )
        };
        match sync_result {
            Ok(_) => RepoTransportResultEnvelope {
                dedup_key: task.dedup_key,
                rsync_failure_scope_uri: task.rsync_failure_scope_uri,
                repo_identity: task.repo_identity,
                mode: RepoTransportMode::Rrdp,
                tal_id: task.tal_id,
                rir_id: task.rir_id,
                timing_ms: started.elapsed().as_millis() as u64,
                result: RepoTransportResultKind::Success {
                    source: "rrdp".to_string(),
                    warnings: Vec::new(),
                },
            },
            Err(err) => {
                let error_class =
                    crate::parallel::dead_repo_blacklist::classify_rrdp_sync_error(&err);
                RepoTransportResultEnvelope {
                    dedup_key: task.dedup_key,
                    rsync_failure_scope_uri: task.rsync_failure_scope_uri,
                    repo_identity: task.repo_identity,
                    mode: RepoTransportMode::Rrdp,
                    tal_id: task.tal_id,
                    rir_id: task.rir_id,
                    timing_ms: started.elapsed().as_millis() as u64,
                    result: RepoTransportResultKind::Failed {
                        detail: err.to_string(),
                        warnings: Vec::new(),
                        error_class,
                    },
                }
            }
        }
    }
}

pub struct LiveRsyncTransportExecutor<R: RsyncFetcher> {
    store: Arc<RocksStore>,
    current_repo_index: CurrentRepoIndexHandle,
    rsync_fetcher: Arc<R>,
    timing: Option<TimingHandle>,
    download_log: Option<DownloadLogHandle>,
}

impl<R: RsyncFetcher> LiveRsyncTransportExecutor<R> {
    pub fn new(
        store: Arc<RocksStore>,
        current_repo_index: CurrentRepoIndexHandle,
        rsync_fetcher: Arc<R>,
        timing: Option<TimingHandle>,
        download_log: Option<DownloadLogHandle>,
    ) -> Self {
        Self {
            store,
            current_repo_index,
            rsync_fetcher,
            timing,
            download_log,
        }
    }
}

impl<R: RsyncFetcher + 'static> RepoTransportExecutor for LiveRsyncTransportExecutor<R> {
    fn execute_transport(&self, task: RepoTransportTask) -> RepoTransportResultEnvelope {
        let started = std::time::Instant::now();
        debug_assert_eq!(task.mode, RepoTransportMode::Rsync);
        let sync_result = if task.retry_short_timeout {
            with_scoped_rsync_timeout_override(RETRY_SHORT_TIMEOUT, || {
                with_scoped_rsync_fail_fast_profile(
                    RsyncFailFastProfile {
                        initial_wall_clock_timeout: RETRY_SHORT_TIMEOUT,
                        max_wall_clock_timeout: RETRY_SHORT_TIMEOUT,
                        max_attempts: 1,
                    },
                    || {
                        run_rsync_transport(
                            self.store.as_ref(),
                            &task.repo_identity.rsync_base_uri,
                            Some(&self.current_repo_index),
                            self.rsync_fetcher.as_ref(),
                            self.timing.as_ref(),
                            self.download_log.as_ref(),
                        )
                    },
                )
            })
        } else {
            run_rsync_transport(
                self.store.as_ref(),
                &task.repo_identity.rsync_base_uri,
                Some(&self.current_repo_index),
                self.rsync_fetcher.as_ref(),
                self.timing.as_ref(),
                self.download_log.as_ref(),
            )
        };
        match sync_result {
            Ok(_) => RepoTransportResultEnvelope {
                dedup_key: task.dedup_key,
                rsync_failure_scope_uri: task.rsync_failure_scope_uri,
                repo_identity: task.repo_identity,
                mode: RepoTransportMode::Rsync,
                tal_id: task.tal_id,
                rir_id: task.rir_id,
                timing_ms: started.elapsed().as_millis() as u64,
                result: RepoTransportResultKind::Success {
                    source: "rsync".to_string(),
                    warnings: Vec::new(),
                },
            },
            Err(err) => {
                let error_class =
                    crate::parallel::dead_repo_blacklist::classify_rsync_repo_sync_error(&err);
                RepoTransportResultEnvelope {
                    dedup_key: task.dedup_key,
                    rsync_failure_scope_uri: task.rsync_failure_scope_uri,
                    repo_identity: task.repo_identity,
                    mode: RepoTransportMode::Rsync,
                    tal_id: task.tal_id,
                    rir_id: task.rir_id,
                    timing_ms: started.elapsed().as_millis() as u64,
                    result: RepoTransportResultKind::Failed {
                        detail: err.to_string(),
                        warnings: Vec::new(),
                        error_class,
                    },
                }
            }
        }
    }
}

pub struct LiveRepoTransportExecutor<H: Fetcher, R: RsyncFetcher> {
    rrdp: LiveRrdpTransportExecutor<H>,
    rsync: LiveRsyncTransportExecutor<R>,
}

impl<H: Fetcher, R: RsyncFetcher> LiveRepoTransportExecutor<H, R> {
    pub fn new(
        store: Arc<RocksStore>,
        current_repo_index: CurrentRepoIndexHandle,
        http_fetcher: Arc<H>,
        rsync_fetcher: Arc<R>,
        timing: Option<TimingHandle>,
        download_log: Option<DownloadLogHandle>,
    ) -> Self {
        Self {
            rrdp: LiveRrdpTransportExecutor::new(
                Arc::clone(&store),
                current_repo_index.clone(),
                http_fetcher,
                timing.clone(),
                download_log.clone(),
            ),
            rsync: LiveRsyncTransportExecutor::new(
                store,
                current_repo_index,
                rsync_fetcher,
                timing,
                download_log,
            ),
        }
    }
}

impl<H: Fetcher + 'static, R: RsyncFetcher + 'static> RepoTransportExecutor
    for LiveRepoTransportExecutor<H, R>
{
    fn execute_transport(&self, task: RepoTransportTask) -> RepoTransportResultEnvelope {
        match task.mode {
            RepoTransportMode::Rrdp => self.rrdp.execute_transport(task),
            RepoTransportMode::Rsync => self.rsync.execute_transport(task),
        }
    }
}

pub struct LiveRepoSyncExecutor<H: Fetcher, R: RsyncFetcher> {
    store: Arc<RocksStore>,
    policy: Policy,
    http_fetcher: Arc<H>,
    rsync_fetcher: Arc<R>,
    timing: Option<TimingHandle>,
    download_log: Option<DownloadLogHandle>,
}

impl<H: Fetcher, R: RsyncFetcher> LiveRepoSyncExecutor<H, R> {
    pub fn new(
        store: Arc<RocksStore>,
        policy: Policy,
        http_fetcher: Arc<H>,
        rsync_fetcher: Arc<R>,
        timing: Option<TimingHandle>,
        download_log: Option<DownloadLogHandle>,
    ) -> Self {
        Self {
            store,
            policy,
            http_fetcher,
            rsync_fetcher,
            timing,
            download_log,
        }
    }
}

impl<H: Fetcher + 'static, R: RsyncFetcher + 'static> RepoSyncExecutor
    for LiveRepoSyncExecutor<H, R>
{
    fn execute(&self, task: RepoSyncTask) -> RepoSyncResultEnvelope {
        let started = std::time::Instant::now();
        crate::progress_log::emit(
            "phase1_repo_worker_execute_start",
            serde_json::json!({
                "repo_key_rsync_base_uri": task.repo_key.rsync_base_uri,
                "repo_key_notification_uri": task.repo_key.notification_uri,
                "tal_id": task.tal_id,
                "rir_id": task.rir_id,
            }),
        );
        match sync_publication_point(
            self.store.as_ref(),
            &self.policy,
            task.repo_key.notification_uri.as_deref(),
            &task.repo_key.rsync_base_uri,
            self.http_fetcher.as_ref(),
            self.rsync_fetcher.as_ref(),
            self.timing.as_ref(),
            self.download_log.as_ref(),
        ) {
            Ok(res) => {
                let timing_ms = started.elapsed().as_millis() as u64;
                crate::progress_log::emit(
                    "phase1_repo_worker_execute_finish",
                    serde_json::json!({
                        "repo_key_rsync_base_uri": task.repo_key.rsync_base_uri,
                        "repo_key_notification_uri": task.repo_key.notification_uri,
                        "result": "success",
                        "phase": repo_sync_phase_label(res.phase),
                        "timing_ms": timing_ms,
                    }),
                );
                RepoSyncResultEnvelope {
                    repo_key: task.repo_key.clone(),
                    tal_id: task.tal_id,
                    rir_id: task.rir_id,
                    result: super::types::RepoSyncResultKind::Success(
                        super::types::RepoSyncResultRef {
                            repo_key: task.repo_key,
                            source: repo_sync_source_label(res.source).to_string(),
                        },
                    ),
                    phase: Some(repo_sync_phase_label(res.phase).to_string()),
                    timing_ms,
                    warnings: res.warnings,
                }
            }
            Err(err) => {
                let timing_ms = started.elapsed().as_millis() as u64;
                crate::progress_log::emit(
                    "phase1_repo_worker_execute_finish",
                    serde_json::json!({
                        "repo_key_rsync_base_uri": task.repo_key.rsync_base_uri,
                        "repo_key_notification_uri": task.repo_key.notification_uri,
                        "result": "failed",
                        "phase": "repo_sync_failed",
                        "timing_ms": timing_ms,
                        "error": err.to_string(),
                    }),
                );
                RepoSyncResultEnvelope {
                    repo_key: task.repo_key,
                    tal_id: task.tal_id,
                    rir_id: task.rir_id,
                    result: super::types::RepoSyncResultKind::Failed {
                        detail: err.to_string(),
                    },
                    phase: Some("repo_sync_failed".to_string()),
                    timing_ms,
                    warnings: Vec::new(),
                }
            }
        }
    }
}

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

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

enum RepoWorkerMessage {
    Task(RepoSyncTask),
    Shutdown,
}

enum TransportWorkerMessage {
    Task(RepoTransportTask),
    Shutdown,
}

pub struct RepoWorkerPool<E: RepoSyncExecutor> {
    config: RepoWorkerPoolConfig,
    task_tx: Sender<RepoWorkerMessage>,
    result_rx: Receiver<RepoSyncResultEnvelope>,
    workers: Vec<JoinHandle<()>>,
    _executor: Arc<E>,
}

pub struct RepoTransportWorkerPool<E: RepoTransportExecutor> {
    config: RepoWorkerPoolConfig,
    task_tx: Sender<TransportWorkerMessage>,
    result_rx: Receiver<RepoTransportResultEnvelope>,
    workers: Vec<JoinHandle<()>>,
    _executor: Arc<E>,
}

impl<E: RepoSyncExecutor> RepoWorkerPool<E> {
    pub fn new(config: RepoWorkerPoolConfig, executor: E) -> Result<Self, String> {
        if config.max_workers == 0 {
            return Err("RepoWorkerPool requires at least one worker".to_string());
        }

        let executor = Arc::new(executor);
        let (task_tx, task_rx) = mpsc::channel::<RepoWorkerMessage>();
        let (result_tx, result_rx) = mpsc::channel::<RepoSyncResultEnvelope>();
        let shared_task_rx = Arc::new(Mutex::new(task_rx));

        let mut workers = Vec::with_capacity(config.max_workers);
        for idx in 0..config.max_workers {
            let task_rx = Arc::clone(&shared_task_rx);
            let result_tx = result_tx.clone();
            let executor = Arc::clone(&executor);
            workers.push(
                thread::Builder::new()
                    .name(format!("repo-sync-worker-{idx}"))
                    .spawn(move || worker_loop(task_rx, result_tx, executor))
                    .map_err(|e| format!("spawn repo worker failed: {e}"))?,
            );
        }

        Ok(Self {
            config,
            task_tx,
            result_rx,
            workers,
            _executor: executor,
        })
    }

    pub fn worker_count(&self) -> usize {
        self.config.max_workers
    }

    pub fn submit(&self, task: RepoSyncTask) -> Result<(), String> {
        self.task_tx
            .send(RepoWorkerMessage::Task(task))
            .map_err(|e| format!("submit repo task failed: {e}"))
    }

    pub fn recv_result_timeout(
        &self,
        timeout: Duration,
    ) -> Result<Option<RepoSyncResultEnvelope>, String> {
        match self.result_rx.recv_timeout(timeout) {
            Ok(msg) => Ok(Some(msg)),
            Err(RecvTimeoutError::Timeout) => Ok(None),
            Err(RecvTimeoutError::Disconnected) => {
                Err("repo worker result channel disconnected".to_string())
            }
        }
    }

    pub fn shutdown(mut self) -> Result<(), String> {
        self.shutdown_inner()
    }

    fn shutdown_inner(&mut self) -> Result<(), String> {
        if self.workers.is_empty() {
            return Ok(());
        }

        for _ in 0..self.workers.len() {
            self.task_tx
                .send(RepoWorkerMessage::Shutdown)
                .map_err(|e| format!("send shutdown to repo worker failed: {e}"))?;
        }

        let mut first_err: Option<String> = None;
        for handle in self.workers.drain(..) {
            if let Err(e) = handle.join() {
                if first_err.is_none() {
                    first_err = Some(format!("join repo worker failed: {e:?}"));
                }
            }
        }

        if let Some(err) = first_err {
            return Err(err);
        }
        Ok(())
    }
}

impl<E: RepoSyncExecutor> Drop for RepoWorkerPool<E> {
    fn drop(&mut self) {
        let _ = self.shutdown_inner();
    }
}

impl<E: RepoTransportExecutor> RepoTransportWorkerPool<E> {
    pub fn new(config: RepoWorkerPoolConfig, executor: E) -> Result<Self, String> {
        if config.max_workers == 0 {
            return Err("RepoTransportWorkerPool requires at least one worker".to_string());
        }
        let executor = Arc::new(executor);
        let (task_tx, task_rx) = mpsc::channel::<TransportWorkerMessage>();
        let (result_tx, result_rx) = mpsc::channel::<RepoTransportResultEnvelope>();
        let shared_task_rx = Arc::new(Mutex::new(task_rx));
        let mut workers = Vec::with_capacity(config.max_workers);
        for idx in 0..config.max_workers {
            let task_rx = Arc::clone(&shared_task_rx);
            let result_tx = result_tx.clone();
            let executor = Arc::clone(&executor);
            workers.push(
                thread::Builder::new()
                    .name(format!("repo-transport-worker-{idx}"))
                    .spawn(move || transport_worker_loop(task_rx, result_tx, executor))
                    .map_err(|e| format!("spawn repo transport worker failed: {e}"))?,
            );
        }
        Ok(Self {
            config,
            task_tx,
            result_rx,
            workers,
            _executor: executor,
        })
    }

    pub fn submit(&self, task: RepoTransportTask) -> Result<(), String> {
        self.task_tx
            .send(TransportWorkerMessage::Task(task))
            .map_err(|e| format!("submit repo transport task failed: {e}"))
    }

    pub fn recv_result_timeout(
        &self,
        timeout: Duration,
    ) -> Result<Option<RepoTransportResultEnvelope>, String> {
        match self.result_rx.recv_timeout(timeout) {
            Ok(msg) => Ok(Some(msg)),
            Err(RecvTimeoutError::Timeout) => Ok(None),
            Err(RecvTimeoutError::Disconnected) => {
                Err("repo transport worker result channel disconnected".to_string())
            }
        }
    }

    pub fn worker_count(&self) -> usize {
        self.config.max_workers
    }

    pub fn shutdown(mut self) -> Result<(), String> {
        self.shutdown_inner()
    }

    fn shutdown_inner(&mut self) -> Result<(), String> {
        if self.workers.is_empty() {
            return Ok(());
        }
        for _ in 0..self.workers.len() {
            self.task_tx
                .send(TransportWorkerMessage::Shutdown)
                .map_err(|e| format!("send shutdown to repo transport worker failed: {e}"))?;
        }
        let mut first_err: Option<String> = None;
        for handle in self.workers.drain(..) {
            if let Err(e) = handle.join() {
                if first_err.is_none() {
                    first_err = Some(format!("join repo transport worker failed: {e:?}"));
                }
            }
        }
        if let Some(err) = first_err {
            return Err(err);
        }
        Ok(())
    }
}

impl<E: RepoTransportExecutor> Drop for RepoTransportWorkerPool<E> {
    fn drop(&mut self) {
        let _ = self.shutdown_inner();
    }
}

fn worker_loop<E: RepoSyncExecutor>(
    task_rx: Arc<Mutex<Receiver<RepoWorkerMessage>>>,
    result_tx: Sender<RepoSyncResultEnvelope>,
    executor: Arc<E>,
) {
    loop {
        let message = {
            let rx = task_rx.lock().expect("repo worker receiver lock poisoned");
            rx.recv()
        };

        match message {
            Ok(RepoWorkerMessage::Task(task)) => {
                let result = executor.execute(task);
                if result_tx.send(result).is_err() {
                    break;
                }
            }
            Ok(RepoWorkerMessage::Shutdown) | Err(_) => break,
        }
    }
}

fn transport_worker_loop<E: RepoTransportExecutor>(
    task_rx: Arc<Mutex<Receiver<TransportWorkerMessage>>>,
    result_tx: Sender<RepoTransportResultEnvelope>,
    executor: Arc<E>,
) {
    loop {
        let message = {
            let rx = task_rx
                .lock()
                .expect("repo transport worker receiver lock poisoned");
            rx.recv()
        };

        match message {
            Ok(TransportWorkerMessage::Task(task)) => {
                let result = executor.execute_transport(task);
                if result_tx.send(result).is_err() {
                    break;
                }
            }
            Ok(TransportWorkerMessage::Shutdown) | Err(_) => break,
        }
    }
}

#[cfg(test)]
mod tests {
    use base64::Engine;
    use sha2::Digest;
    use std::collections::HashMap;
    use std::fs;
    use std::sync::atomic::{AtomicUsize, Ordering};
    use std::sync::{Arc, Barrier, Mutex};
    use std::time::Duration;

    use super::{
        LiveRepoSyncExecutor, LiveRrdpTransportExecutor, LiveRsyncTransportExecutor,
        RepoSyncExecutor, RepoTransportExecutor, RepoTransportWorkerPool, RepoWorkerPool,
        RepoWorkerPoolConfig,
    };
    use crate::current_repo_index::CurrentRepoIndex;
    use crate::fetch::rsync::{
        LocalDirRsyncFetcher, RsyncFetchError, RsyncFetchResult, RsyncFetcher,
    };
    use crate::parallel::config::ParallelPhase1Config;
    use crate::parallel::types::{
        RepoDedupKey, RepoIdentity, RepoKey, RepoRequester, RepoSyncResultEnvelope,
        RepoSyncResultKind, RepoSyncResultRef, RepoSyncTask, RepoTransportMode,
        RepoTransportResultEnvelope, RepoTransportResultKind, RepoTransportTask,
    };
    use crate::policy::SyncPreference;
    use crate::report::Warning;
    use crate::storage::RocksStore;
    use crate::sync::rrdp::Fetcher;

    fn sample_task(name: &str) -> RepoSyncTask {
        RepoSyncTask {
            repo_key: RepoKey::new(format!("rsync://example.test/{name}/"), None),
            validation_time: time::OffsetDateTime::UNIX_EPOCH,
            sync_preference: SyncPreference::RrdpThenRsync,
            tal_id: "arin".to_string(),
            rir_id: "arin".to_string(),
            priority: 0,
            requesters: vec![RepoRequester {
                tal_id: "arin".to_string(),
                rir_id: "arin".to_string(),
                parent_node_id: None,
                ca_instance_handle_id: format!("node:{name}"),
                publication_point_rsync_uri: format!("rsync://example.test/{name}/"),
                manifest_rsync_uri: format!("rsync://example.test/{name}/root.mft"),
            }],
        }
    }

    fn sample_rrdp_transport_task(
        notification_uri: &str,
        rsync_base_uri: &str,
    ) -> RepoTransportTask {
        RepoTransportTask {
            dedup_key: RepoDedupKey::RrdpNotify {
                notification_uri: notification_uri.to_string(),
            },
            rsync_failure_scope_uri: None,
            repo_identity: RepoIdentity::new(Some(notification_uri.to_string()), rsync_base_uri),
            mode: RepoTransportMode::Rrdp,
            retry_short_timeout: false,
            tal_id: "arin".to_string(),
            rir_id: "arin".to_string(),
            validation_time: time::OffsetDateTime::UNIX_EPOCH,
            priority: 0,
            requesters: vec![RepoRequester::with_tal_rir(
                "arin",
                "arin",
                format!("{rsync_base_uri}root.mft"),
                rsync_base_uri.to_string(),
                "node:rrdp",
            )],
        }
    }

    fn sample_rsync_transport_task(
        rsync_scope_uri: &str,
        rsync_base_uri: &str,
    ) -> RepoTransportTask {
        RepoTransportTask {
            dedup_key: RepoDedupKey::RsyncScope {
                rsync_scope_uri: rsync_scope_uri.to_string(),
            },
            rsync_failure_scope_uri: None,
            repo_identity: RepoIdentity::new(None, rsync_base_uri),
            mode: RepoTransportMode::Rsync,
            retry_short_timeout: false,
            tal_id: "arin".to_string(),
            rir_id: "arin".to_string(),
            validation_time: time::OffsetDateTime::UNIX_EPOCH,
            priority: 0,
            requesters: vec![RepoRequester::with_tal_rir(
                "arin",
                "arin",
                format!("{rsync_base_uri}root.mft"),
                rsync_base_uri.to_string(),
                "node:rsync",
            )],
        }
    }

    struct SuccessExecutor;

    impl RepoSyncExecutor for SuccessExecutor {
        fn execute(&self, task: RepoSyncTask) -> RepoSyncResultEnvelope {
            RepoSyncResultEnvelope {
                repo_key: task.repo_key.clone(),
                tal_id: task.tal_id,
                rir_id: task.rir_id,
                result: RepoSyncResultKind::Success(RepoSyncResultRef {
                    repo_key: task.repo_key,
                    source: "rrdp".to_string(),
                }),
                phase: Some("rrdp_ok".to_string()),
                timing_ms: 12,
                warnings: Vec::new(),
            }
        }
    }

    struct FailureExecutor;

    impl RepoSyncExecutor for FailureExecutor {
        fn execute(&self, task: RepoSyncTask) -> RepoSyncResultEnvelope {
            RepoSyncResultEnvelope {
                repo_key: task.repo_key,
                tal_id: task.tal_id,
                rir_id: task.rir_id,
                result: RepoSyncResultKind::Failed {
                    detail: "timeout".to_string(),
                },
                phase: Some("repo_sync_failed".to_string()),
                timing_ms: 33,
                warnings: vec![Warning::new("timeout")],
            }
        }
    }

    struct BarrierExecutor {
        barrier: Arc<Barrier>,
        active: Arc<AtomicUsize>,
        peak: Arc<AtomicUsize>,
    }

    #[derive(Clone)]
    struct PanicHttpFetcher;

    impl Fetcher for PanicHttpFetcher {
        fn fetch(&self, _uri: &str) -> Result<Vec<u8>, String> {
            panic!("http fetch should not be used in this test")
        }
    }

    #[derive(Clone)]
    struct ErrorHttpFetcher;

    impl Fetcher for ErrorHttpFetcher {
        fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
            Err(format!("fetch blocked for {uri}"))
        }
    }

    #[derive(Clone)]
    struct FailingRsyncFetcher;

    #[derive(Clone, Default)]
    struct MockHttpFetcher {
        map: Arc<Mutex<HashMap<String, Vec<u8>>>>,
    }

    impl MockHttpFetcher {
        fn new() -> Self {
            Self::default()
        }

        fn insert(&self, uri: &str, bytes: Vec<u8>) {
            self.map
                .lock()
                .expect("http fixture lock")
                .insert(uri.to_string(), bytes);
        }
    }

    impl RsyncFetcher for FailingRsyncFetcher {
        fn fetch_objects(&self, _rsync_base_uri: &str) -> RsyncFetchResult<Vec<(String, Vec<u8>)>> {
            Err(RsyncFetchError::Fetch("boom".to_string()))
        }
    }

    impl Fetcher for MockHttpFetcher {
        fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
            self.map
                .lock()
                .expect("http fixture lock")
                .get(uri)
                .cloned()
                .ok_or_else(|| format!("missing fixture for {uri}"))
        }
    }

    fn sha256_hex(bytes: &[u8]) -> String {
        hex::encode(sha2::Sha256::digest(bytes))
    }

    fn rrdp_notification_xml(session_id: &str, serial: u64, snapshot_uri: &str) -> String {
        let snapshot_body = rrdp_snapshot_xml(
            session_id,
            &[("rsync://example.test/repo/a.roa", b"a".as_ref())],
        );
        let snapshot_hash = sha256_hex(snapshot_body.as_bytes());
        format!(
            r#"<?xml version="1.0" encoding="UTF-8"?>
<notification xmlns="http://www.ripe.net/rpki/rrdp" version="1" session_id="{session_id}" serial="{serial}">
  <snapshot uri="{snapshot_uri}" hash="{snapshot_hash}"/>
</notification>"#
        )
    }

    fn rrdp_snapshot_xml(session_id: &str, objects: &[(&str, &[u8])]) -> String {
        let mut body = String::from(&format!(
            r#"<?xml version="1.0" encoding="UTF-8"?>
<snapshot xmlns="http://www.ripe.net/rpki/rrdp" version="1" session_id="{session_id}" serial="1">
"#
        ));
        for (uri, bytes) in objects {
            body.push_str(&format!(
                "  <publish uri=\"{uri}\">{}</publish>\n",
                base64::engine::general_purpose::STANDARD.encode(bytes)
            ));
        }
        body.push_str("</snapshot>");
        body
    }

    impl RepoSyncExecutor for BarrierExecutor {
        fn execute(&self, task: RepoSyncTask) -> RepoSyncResultEnvelope {
            let now = self.active.fetch_add(1, Ordering::SeqCst) + 1;
            loop {
                let peak = self.peak.load(Ordering::SeqCst);
                if now > peak {
                    if self
                        .peak
                        .compare_exchange(peak, now, Ordering::SeqCst, Ordering::SeqCst)
                        .is_ok()
                    {
                        break;
                    }
                } else {
                    break;
                }
            }
            self.barrier.wait();
            self.active.fetch_sub(1, Ordering::SeqCst);
            RepoSyncResultEnvelope {
                repo_key: task.repo_key.clone(),
                tal_id: task.tal_id,
                rir_id: task.rir_id,
                result: RepoSyncResultKind::Success(RepoSyncResultRef {
                    repo_key: task.repo_key,
                    source: "rsync".to_string(),
                }),
                phase: Some("rsync_only_ok".to_string()),
                timing_ms: 1,
                warnings: Vec::new(),
            }
        }
    }

    struct SuccessTransportExecutor;

    impl RepoTransportExecutor for SuccessTransportExecutor {
        fn execute_transport(&self, task: RepoTransportTask) -> RepoTransportResultEnvelope {
            RepoTransportResultEnvelope {
                dedup_key: task.dedup_key,
                rsync_failure_scope_uri: task.rsync_failure_scope_uri,
                repo_identity: task.repo_identity,
                mode: task.mode,
                tal_id: task.tal_id,
                rir_id: task.rir_id,
                timing_ms: 5,
                result: RepoTransportResultKind::Success {
                    source: match task.mode {
                        RepoTransportMode::Rrdp => "rrdp".to_string(),
                        RepoTransportMode::Rsync => "rsync".to_string(),
                    },
                    warnings: Vec::new(),
                },
            }
        }
    }

    #[test]
    fn repo_worker_pool_config_uses_parallel_phase1_budget() {
        let cfg = ParallelPhase1Config {
            max_repo_sync_workers_global: 9,
            ..ParallelPhase1Config::default()
        };
        let pool_cfg = RepoWorkerPoolConfig::from(&cfg);
        assert_eq!(pool_cfg.max_workers, 9);
    }

    #[test]
    fn repo_worker_pool_rejects_zero_workers() {
        let err = RepoWorkerPool::new(RepoWorkerPoolConfig { max_workers: 0 }, SuccessExecutor)
            .err()
            .expect("zero workers should fail");
        assert!(err.contains("at least one worker"));
    }

    #[test]
    fn repo_worker_pool_processes_tasks_and_returns_results() {
        let pool = RepoWorkerPool::new(RepoWorkerPoolConfig { max_workers: 2 }, SuccessExecutor)
            .expect("pool");
        pool.submit(sample_task("a")).expect("submit task a");
        pool.submit(sample_task("b")).expect("submit task b");

        let mut results = Vec::new();
        results.push(
            pool.recv_result_timeout(Duration::from_secs(1))
                .expect("recv result 1")
                .expect("result 1"),
        );
        results.push(
            pool.recv_result_timeout(Duration::from_secs(1))
                .expect("recv result 2")
                .expect("result 2"),
        );

        assert_eq!(results.len(), 2);
        assert!(
            results
                .iter()
                .all(|res| matches!(res.result, RepoSyncResultKind::Success(_)))
        );
    }

    #[test]
    fn repo_worker_pool_returns_failure_envelopes() {
        let pool = RepoWorkerPool::new(RepoWorkerPoolConfig { max_workers: 1 }, FailureExecutor)
            .expect("pool");
        pool.submit(sample_task("fail")).expect("submit");
        let result = pool
            .recv_result_timeout(Duration::from_secs(1))
            .expect("recv")
            .expect("result");
        assert!(matches!(
            result.result,
            RepoSyncResultKind::Failed { ref detail } if detail == "timeout"
        ));
        assert_eq!(result.warnings, vec![Warning::new("timeout")]);
    }

    #[test]
    fn repo_worker_pool_processes_work_in_parallel() {
        let barrier = Arc::new(Barrier::new(2));
        let active = Arc::new(AtomicUsize::new(0));
        let peak = Arc::new(AtomicUsize::new(0));
        let executor = BarrierExecutor {
            barrier: Arc::clone(&barrier),
            active: Arc::clone(&active),
            peak: Arc::clone(&peak),
        };
        let pool =
            RepoWorkerPool::new(RepoWorkerPoolConfig { max_workers: 2 }, executor).expect("pool");
        pool.submit(sample_task("a")).expect("submit a");
        pool.submit(sample_task("b")).expect("submit b");

        let _ = pool
            .recv_result_timeout(Duration::from_secs(1))
            .expect("recv 1")
            .expect("result 1");
        let _ = pool
            .recv_result_timeout(Duration::from_secs(1))
            .expect("recv 2")
            .expect("result 2");

        assert!(peak.load(Ordering::SeqCst) >= 2);
    }

    #[test]
    fn repo_worker_pool_exposes_configured_worker_count() {
        let pool = RepoWorkerPool::new(RepoWorkerPoolConfig { max_workers: 3 }, SuccessExecutor)
            .expect("pool");
        assert_eq!(pool.worker_count(), 3);
    }

    #[test]
    fn repo_transport_worker_pool_processes_transport_tasks() {
        let pool = RepoTransportWorkerPool::new(
            RepoWorkerPoolConfig { max_workers: 2 },
            SuccessTransportExecutor,
        )
        .expect("pool");
        pool.submit(sample_rrdp_transport_task(
            "https://example.test/notify.xml",
            "rsync://example.test/repo/",
        ))
        .expect("submit rrdp");
        pool.submit(sample_rsync_transport_task(
            "rsync://example.test/module/",
            "rsync://example.test/repo/",
        ))
        .expect("submit rsync");

        let first = pool
            .recv_result_timeout(Duration::from_secs(1))
            .expect("recv first")
            .expect("first result");
        let second = pool
            .recv_result_timeout(Duration::from_secs(1))
            .expect("recv second")
            .expect("second result");
        assert!(matches!(
            first.result,
            RepoTransportResultKind::Success { .. }
        ));
        assert!(matches!(
            second.result,
            RepoTransportResultKind::Success { .. }
        ));
    }

    #[test]
    fn live_repo_sync_executor_runs_rsync_sync_and_updates_store() {
        let td = tempfile::tempdir().expect("tempdir");
        fs::create_dir_all(td.path().join("nested")).expect("mkdir");
        fs::write(td.path().join("a.roa"), b"a").expect("write a");
        fs::write(td.path().join("nested").join("b.cer"), b"b").expect("write b");

        let store_dir = tempfile::tempdir().expect("store tempdir");
        let store = Arc::new(RocksStore::open(store_dir.path()).expect("open store"));
        let executor = LiveRepoSyncExecutor::new(
            Arc::clone(&store),
            crate::policy::Policy::default(),
            Arc::new(PanicHttpFetcher),
            Arc::new(LocalDirRsyncFetcher::new(td.path())),
            None,
            None,
        );

        let result = executor.execute(sample_task("repo"));
        assert!(matches!(result.result, RepoSyncResultKind::Success(_)));
        assert_eq!(result.tal_id, "arin");
        assert_eq!(result.rir_id, "arin");
        assert_eq!(result.warnings.len(), 0);

        let view = store
            .get_repository_view_entry("rsync://example.test/repo/a.roa")
            .expect("read view")
            .expect("entry exists");
        assert_eq!(view.rsync_uri, "rsync://example.test/repo/a.roa");
        assert!(view.current_hash.is_some());
    }

    #[test]
    fn live_repo_sync_executor_returns_failure_when_rsync_fails() {
        let store_dir = tempfile::tempdir().expect("store tempdir");
        let store = Arc::new(RocksStore::open(store_dir.path()).expect("open store"));
        let executor = LiveRepoSyncExecutor::new(
            Arc::clone(&store),
            crate::policy::Policy::default(),
            Arc::new(PanicHttpFetcher),
            Arc::new(FailingRsyncFetcher),
            None,
            None,
        );

        let result = executor.execute(sample_task("repo"));
        assert!(matches!(
            result.result,
            RepoSyncResultKind::Failed { ref detail } if detail.contains("rsync fallback failed")
        ));
        assert!(
            store
                .get_repository_view_entry("rsync://example.test/repo/a.roa")
                .expect("read view")
                .is_none()
        );
    }

    #[test]
    fn live_rrdp_transport_executor_succeeds_on_valid_notification() {
        let td = tempfile::tempdir().expect("tempdir");
        let store = Arc::new(RocksStore::open(td.path()).expect("open store"));
        let http = MockHttpFetcher::new();
        let notify = "https://example.test/notification.xml";
        let snapshot = "https://example.test/snapshot.xml";
        let snapshot_bytes = rrdp_snapshot_xml(
            "123e4567-e89b-12d3-a456-426614174000",
            &[("rsync://example.test/repo/a.roa", b"a".as_ref())],
        );
        let notification_bytes =
            rrdp_notification_xml("123e4567-e89b-12d3-a456-426614174000", 1, snapshot);
        http.insert(notify, notification_bytes.into_bytes());
        http.insert(snapshot, snapshot_bytes.into_bytes());
        let current_repo_index = CurrentRepoIndex::shared();
        let executor = LiveRrdpTransportExecutor::new(
            Arc::clone(&store),
            current_repo_index.clone(),
            Arc::new(http),
            None,
            None,
        );
        let result = executor.execute_transport(sample_rrdp_transport_task(
            notify,
            "rsync://example.test/repo/",
        ));
        assert!(
            matches!(result.result, RepoTransportResultKind::Success { .. }),
            "{result:?}"
        );
        let index = current_repo_index.read().expect("index read lock");
        assert!(
            index
                .get_by_uri("rsync://example.test/repo/a.roa")
                .is_some()
        );
        assert_eq!(
            index.list_scope_uris(notify),
            vec!["rsync://example.test/repo/a.roa".to_string()]
        );
    }

    #[test]
    fn live_rrdp_transport_executor_reports_failure_without_rsync_fallback() {
        let td = tempfile::tempdir().expect("tempdir");
        let store = Arc::new(RocksStore::open(td.path()).expect("open store"));
        let executor = LiveRrdpTransportExecutor::new(
            Arc::clone(&store),
            CurrentRepoIndex::shared(),
            Arc::new(ErrorHttpFetcher),
            None,
            None,
        );
        let result = executor.execute_transport(sample_rrdp_transport_task(
            "https://example.test/notification.xml",
            "rsync://example.test/repo/",
        ));
        assert!(matches!(
            result.result,
            RepoTransportResultKind::Failed { .. }
        ));
    }

    #[test]
    fn live_rsync_transport_executor_succeeds_on_local_repo() {
        let td = tempfile::tempdir().expect("tempdir");
        fs::create_dir_all(td.path().join("nested")).expect("mkdir");
        fs::write(td.path().join("a.roa"), b"a").expect("write");
        fs::write(td.path().join("nested").join("b.cer"), b"b").expect("write");
        let store_dir = tempfile::tempdir().expect("store tempdir");
        let store = Arc::new(RocksStore::open(store_dir.path()).expect("open store"));
        let current_repo_index = CurrentRepoIndex::shared();
        let executor = LiveRsyncTransportExecutor::new(
            Arc::clone(&store),
            current_repo_index.clone(),
            Arc::new(LocalDirRsyncFetcher::new(td.path())),
            None,
            None,
        );
        let result = executor.execute_transport(sample_rsync_transport_task(
            "rsync://example.test/repo/",
            "rsync://example.test/repo/",
        ));
        assert!(matches!(
            result.result,
            RepoTransportResultKind::Success { .. }
        ));
        let index = current_repo_index.read().expect("index read lock");
        assert!(
            index
                .get_by_uri("rsync://example.test/repo/a.roa")
                .is_some()
        );
        assert!(
            index
                .get_by_uri("rsync://example.test/repo/nested/b.cer")
                .is_some()
        );
        assert_eq!(
            index.list_scope_uris("rsync://example.test/repo/"),
            vec![
                "rsync://example.test/repo/a.roa".to_string(),
                "rsync://example.test/repo/nested/b.cer".to_string()
            ]
        );
    }

    #[test]
    fn live_rsync_transport_executor_reports_failure() {
        let store_dir = tempfile::tempdir().expect("store tempdir");
        let store = Arc::new(RocksStore::open(store_dir.path()).expect("open store"));
        let executor = LiveRsyncTransportExecutor::new(
            Arc::clone(&store),
            CurrentRepoIndex::shared(),
            Arc::new(FailingRsyncFetcher),
            None,
            None,
        );
        let result = executor.execute_transport(sample_rsync_transport_task(
            "rsync://example.test/module/",
            "rsync://example.test/repo/",
        ));
        assert!(matches!(
            result.result,
            RepoTransportResultKind::Failed { .. }
        ));
    }
}
