use std::sync::Arc;

use crate::fetch::rsync::{RsyncFetchError, RsyncFetchResult, RsyncFetcher};
use crate::fetch::rsync_system::{
    RsyncScopePolicy, scoped_rsync_failure_dedup_key, scoped_rsync_fetch_uri,
};
use crate::storage::{RepositoryViewState, RocksStore};

#[derive(Clone)]
pub struct CurrentRepositoryViewRsyncFetcher {
    store: Arc<RocksStore>,
    scope_policy: RsyncScopePolicy,
}

impl CurrentRepositoryViewRsyncFetcher {
    pub fn new(store: Arc<RocksStore>, scope_policy: RsyncScopePolicy) -> Self {
        Self {
            store,
            scope_policy,
        }
    }
}

impl RsyncFetcher for CurrentRepositoryViewRsyncFetcher {
    fn fetch_objects(&self, rsync_base_uri: &str) -> RsyncFetchResult<Vec<(String, Vec<u8>)>> {
        // The source run's live fetcher may have fetched a module root rather
        // than the publication-point URI that triggered it. Read that same
        // frozen-view prefix here so transport prefetch request identities and
        // their materialized object sets stay equivalent.
        let base = scoped_rsync_fetch_uri(self.scope_policy, rsync_base_uri);
        let entries = self
            .store
            .list_repository_view_entries_with_prefix(&base)
            .map_err(|error| {
                RsyncFetchError::Fetch(format!(
                    "list frozen repository view failed for {base}: {error}"
                ))
            })?;
        let mut objects = Vec::with_capacity(entries.len());
        for entry in entries {
            if !matches!(
                entry.state,
                RepositoryViewState::Present | RepositoryViewState::Replaced
            ) {
                continue;
            }
            let bytes = self
                .store
                .load_current_object_bytes_by_uri(&entry.rsync_uri)
                .map_err(|error| {
                    RsyncFetchError::Fetch(format!(
                        "load frozen repository object failed for {}: {error}",
                        entry.rsync_uri
                    ))
                })?
                .ok_or_else(|| {
                    RsyncFetchError::Fetch(format!(
                        "frozen repository object missing for {}",
                        entry.rsync_uri
                    ))
                })?;
            objects.push((entry.rsync_uri, bytes));
        }
        objects.sort_by(|left, right| left.0.cmp(&right.0));
        if objects.is_empty() {
            return Err(RsyncFetchError::Fetch(format!(
                "frozen repository view contains no current objects under {base}"
            )));
        }
        Ok(objects)
    }

    fn fetch_object(&self, rsync_uri: &str) -> RsyncFetchResult<Vec<u8>> {
        self.store
            .load_current_object_bytes_by_uri(rsync_uri)
            .map_err(|error| {
                RsyncFetchError::Fetch(format!(
                    "load frozen repository object failed for {rsync_uri}: {error}"
                ))
            })?
            .ok_or_else(|| {
                RsyncFetchError::Fetch(format!("frozen repository object not found: {rsync_uri}"))
            })
    }

    fn dedup_key(&self, rsync_base_uri: &str) -> String {
        scoped_rsync_fetch_uri(self.scope_policy, rsync_base_uri)
    }

    fn failure_dedup_key(&self, rsync_base_uri: &str) -> Option<String> {
        scoped_rsync_failure_dedup_key(self.scope_policy, rsync_base_uri)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::storage::{RepositoryViewEntry, RocksStore};

    #[test]
    fn current_repository_fetcher_returns_sorted_present_objects() {
        let dir = tempfile::tempdir().expect("tempdir");
        let store = Arc::new(RocksStore::open(dir.path()).expect("open store"));
        let entries = [
            ("rsync://example.test/repo/b.roa", b"b".as_slice()),
            ("rsync://example.test/repo/a.mft", b"a".as_slice()),
        ];
        for (uri, bytes) in entries {
            let hash = hex::encode(crate::cir::sha256(bytes));
            store
                .put_blob_bytes_batch(&[(hash.clone(), bytes.to_vec())])
                .expect("put blob");
            store
                .put_repository_view_entry(&RepositoryViewEntry {
                    rsync_uri: uri.to_string(),
                    repository_source: Some("fixture".to_string()),
                    object_type: Some("roa".to_string()),
                    state: RepositoryViewState::Present,
                    current_hash: Some(hash),
                })
                .expect("put view");
        }
        let fetcher = CurrentRepositoryViewRsyncFetcher::new(store, RsyncScopePolicy::default());
        let objects = fetcher
            .fetch_objects("rsync://example.test/repo/")
            .expect("fetch objects");
        assert_eq!(
            objects
                .iter()
                .map(|(uri, _)| uri.as_str())
                .collect::<Vec<_>>(),
            vec![
                "rsync://example.test/repo/a.mft",
                "rsync://example.test/repo/b.roa"
            ]
        );
        assert_eq!(
            fetcher
                .fetch_object("rsync://example.test/repo/a.mft")
                .expect("fetch one"),
            b"a"
        );
        assert!(
            fetcher
                .fetch_object("rsync://example.test/repo/missing.roa")
                .is_err()
        );
        assert_eq!(
            fetcher.dedup_key("rsync://example.test/repo"),
            "rsync://example.test/repo/"
        );
        assert!(
            fetcher
                .fetch_objects("rsync://empty.example/repo/")
                .unwrap_err()
                .to_string()
                .contains("no current objects")
        );
    }

    #[test]
    fn current_repository_fetcher_ignores_withdrawn_and_reports_missing_blob() {
        let dir = tempfile::tempdir().expect("tempdir");
        let store = Arc::new(RocksStore::open(dir.path()).expect("open store"));
        store
            .put_repository_view_entry(&RepositoryViewEntry {
                rsync_uri: "rsync://example.test/repo/withdrawn.roa".to_string(),
                repository_source: Some("fixture".to_string()),
                object_type: Some("roa".to_string()),
                state: RepositoryViewState::Withdrawn,
                current_hash: None,
            })
            .expect("put withdrawn view");
        let missing_hash = "ab".repeat(32);
        store
            .put_repository_view_entry(&RepositoryViewEntry {
                rsync_uri: "rsync://example.test/repo/missing.roa".to_string(),
                repository_source: Some("fixture".to_string()),
                object_type: Some("roa".to_string()),
                state: RepositoryViewState::Present,
                current_hash: Some(missing_hash),
            })
            .expect("put missing view");
        let fetcher = CurrentRepositoryViewRsyncFetcher::new(store, RsyncScopePolicy::default());
        let error = fetcher
            .fetch_objects("rsync://example.test/repo/")
            .unwrap_err()
            .to_string();
        assert!(error.contains("blob bytes missing"));
    }

    #[test]
    fn current_repository_fetcher_replays_module_scope_and_failure_dedup() {
        let dir = tempfile::tempdir().expect("tempdir");
        let store = Arc::new(RocksStore::open(dir.path()).expect("open store"));
        for (uri, bytes) in [
            ("rsync://example.test/repo/ca/a.mft", b"a".as_slice()),
            ("rsync://example.test/repo/other/b.roa", b"b".as_slice()),
        ] {
            let hash = hex::encode(crate::cir::sha256(bytes));
            store
                .put_blob_bytes_batch(&[(hash.clone(), bytes.to_vec())])
                .expect("put blob");
            store
                .put_repository_view_entry(&RepositoryViewEntry {
                    rsync_uri: uri.to_string(),
                    repository_source: Some("fixture".to_string()),
                    object_type: Some("fixture".to_string()),
                    state: RepositoryViewState::Present,
                    current_hash: Some(hash),
                })
                .expect("put view");
        }

        let module_fetcher = CurrentRepositoryViewRsyncFetcher::new(
            Arc::clone(&store),
            RsyncScopePolicy::ModuleRoot,
        );
        let objects = module_fetcher
            .fetch_objects("rsync://example.test/repo/ca/")
            .expect("fetch module scope");
        assert_eq!(objects.len(), 2);
        assert_eq!(
            module_fetcher.dedup_key("rsync://example.test/repo/ca/"),
            "rsync://example.test/repo/"
        );

        let host_fetcher = CurrentRepositoryViewRsyncFetcher::new(store, RsyncScopePolicy::Host);
        assert_eq!(
            host_fetcher.failure_dedup_key("rsync://example.test/repo/ca/"),
            Some("rsync://example.test/".to_string())
        );
    }
}
