use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use crate::fetch::rsync::{RsyncFetchError, RsyncFetchResult, RsyncFetcher};
use crate::replay::archive::{ReplayArchiveIndex, canonical_rsync_module};

#[derive(Clone, Debug)]
pub struct PayloadReplayRsyncFetcher {
    index: Arc<ReplayArchiveIndex>,
}

impl PayloadReplayRsyncFetcher {
    pub fn new(index: Arc<ReplayArchiveIndex>) -> Self {
        Self { index }
    }

    pub fn from_paths(
        archive_root: impl AsRef<Path>,
        locks_path: impl AsRef<Path>,
    ) -> Result<Self, String> {
        let index = Arc::new(
            ReplayArchiveIndex::load(archive_root, locks_path).map_err(|e| e.to_string())?,
        );
        Ok(Self::new(index))
    }

    pub fn archive_index(&self) -> &ReplayArchiveIndex {
        self.index.as_ref()
    }
}

impl RsyncFetcher for PayloadReplayRsyncFetcher {
    fn fetch_objects(&self, rsync_base_uri: &str) -> RsyncFetchResult<Vec<(String, Vec<u8>)>> {
        let (module_uri, relative_path) = split_rsync_base_uri(rsync_base_uri)
            .map_err(|e| RsyncFetchError::Fetch(e.to_string()))?;
        let module = self
            .index
            .resolve_rsync_module_for_base_uri(rsync_base_uri)
            .map_err(|e| RsyncFetchError::Fetch(e.to_string()))?;
        let module_root = module_tree_root(&module_uri, &module.tree_dir)
            .map_err(|e| RsyncFetchError::Fetch(e.to_string()))?;
        let start = if relative_path.as_os_str().is_empty() {
            module_root
        } else {
            module_root.join(&relative_path)
        };
        if !start.is_dir() {
            return Err(RsyncFetchError::Fetch(format!(
                "replay rsync subtree not found: {}",
                start.display()
            )));
        }
        let mut out = Vec::new();
        walk_dir_collect(&start, &start, rsync_base_uri, &mut out)
            .map_err(RsyncFetchError::Fetch)?;
        Ok(out)
    }
}

fn split_rsync_base_uri(rsync_base_uri: &str) -> Result<(String, PathBuf), String> {
    let module_uri = canonical_rsync_module(rsync_base_uri).map_err(|e| e.to_string())?;
    let rest = rsync_base_uri
        .strip_prefix(&module_uri)
        .unwrap_or_default()
        .trim_matches('/');
    let relative_path = if rest.is_empty() {
        PathBuf::new()
    } else {
        let mut path = PathBuf::new();
        for segment in rest.split('/') {
            if !segment.is_empty() {
                path.push(segment);
            }
        }
        path
    };
    Ok((module_uri, relative_path))
}

fn module_tree_root(module_uri: &str, tree_dir: &Path) -> Result<PathBuf, String> {
    let rest = module_uri
        .strip_prefix("rsync://")
        .ok_or_else(|| format!("invalid rsync module URI: {module_uri}"))?;
    let mut parts = rest.trim_end_matches('/').split('/');
    let authority = parts
        .next()
        .ok_or_else(|| format!("invalid rsync module URI: {module_uri}"))?;
    let module = parts
        .next()
        .ok_or_else(|| format!("invalid rsync module URI: {module_uri}"))?;
    Ok(tree_dir.join(authority).join(module))
}

fn walk_dir_collect(
    root: &Path,
    current: &Path,
    rsync_base_uri: &str,
    out: &mut Vec<(String, Vec<u8>)>,
) -> Result<(), String> {
    let rd = fs::read_dir(current).map_err(|e| e.to_string())?;
    for entry in rd {
        let entry = entry.map_err(|e| e.to_string())?;
        let path = entry.path();
        let meta = entry.metadata().map_err(|e| e.to_string())?;
        if meta.is_dir() {
            walk_dir_collect(root, &path, rsync_base_uri, out)?;
            continue;
        }
        if !meta.is_file() {
            continue;
        }
        let rel = path
            .strip_prefix(root)
            .map_err(|e| e.to_string())?
            .to_string_lossy()
            .replace('\\', "/");
        let base = if rsync_base_uri.ends_with('/') {
            rsync_base_uri.to_string()
        } else {
            format!("{rsync_base_uri}/")
        };
        let uri = format!("{base}{rel}");
        let bytes = fs::read(&path).map_err(|e| e.to_string())?;
        out.push((uri, bytes));
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    fn build_rsync_archive() -> (tempfile::TempDir, PathBuf, PathBuf) {
        let temp = tempfile::tempdir().expect("tempdir");
        let archive_root = temp.path().join("payload-archive");
        let capture = "capture-rsync";
        let capture_root = archive_root.join("v1").join("captures").join(capture);
        std::fs::create_dir_all(&capture_root).expect("mkdir capture root");
        std::fs::write(
            capture_root.join("capture.json"),
            format!(
                r#"{{"version":1,"captureId":"{capture}","createdAt":"2026-03-13T00:00:00Z","notes":""}}"#
            ),
        )
        .expect("write capture meta");

        let module_uri = "rsync://rsync.example.test/repo/";
        let mod_hash = crate::replay::archive::sha256_hex(module_uri.as_bytes());
        let module_root = capture_root
            .join("rsync/modules")
            .join(&mod_hash)
            .join("tree")
            .join("rsync.example.test")
            .join("repo");
        std::fs::create_dir_all(module_root.join("child")).expect("mkdir module tree");
        let module_bucket_dir = capture_root.join("rsync/modules").join(&mod_hash);
        std::fs::write(
            module_bucket_dir.join("meta.json"),
            format!(
                r#"{{"version":1,"module":"{module_uri}","createdAt":"2026-03-13T00:00:00Z","lastSeenAt":"2026-03-13T00:00:01Z"}}"#
            ),
        )
        .expect("write module meta");
        std::fs::write(module_root.join("a.roa"), b"a").expect("write a.roa");
        std::fs::write(module_root.join("child").join("b.cer"), b"b").expect("write b.cer");

        let locks_path = temp.path().join("locks.json");
        std::fs::write(
            &locks_path,
            format!(
                r#"{{"version":1,"capture":"{capture}","rrdp":{{}},"rsync":{{"{module_uri}":{{"transport":"rsync"}}}}}}"#
            ),
        )
        .expect("write locks");

        (temp, archive_root, locks_path)
    }

    #[test]
    fn payload_replay_rsync_fetcher_reads_module_root() {
        let (_temp, archive_root, locks_path) = build_rsync_archive();
        let fetcher = PayloadReplayRsyncFetcher::from_paths(&archive_root, &locks_path)
            .expect("build replay rsync fetcher");
        let mut objects = fetcher
            .fetch_objects("rsync://rsync.example.test/repo/")
            .expect("fetch root objects");
        objects.sort_by(|a, b| a.0.cmp(&b.0));

        assert_eq!(objects.len(), 2);
        assert_eq!(objects[0].0, "rsync://rsync.example.test/repo/a.roa");
        assert_eq!(objects[0].1, b"a");
        assert_eq!(objects[1].0, "rsync://rsync.example.test/repo/child/b.cer");
        assert_eq!(objects[1].1, b"b");
    }

    #[test]
    fn payload_replay_rsync_fetcher_reads_subtree_only() {
        let (_temp, archive_root, locks_path) = build_rsync_archive();
        let fetcher = PayloadReplayRsyncFetcher::from_paths(&archive_root, &locks_path)
            .expect("build replay rsync fetcher");
        let objects = fetcher
            .fetch_objects("rsync://rsync.example.test/repo/child/")
            .expect("fetch child subtree");

        assert_eq!(objects.len(), 1);
        assert_eq!(objects[0].0, "rsync://rsync.example.test/repo/child/b.cer");
        assert_eq!(objects[0].1, b"b");
    }

    #[test]
    fn payload_replay_rsync_fetcher_rejects_missing_module_and_subtree() {
        let (_temp, archive_root, locks_path) = build_rsync_archive();
        let fetcher = PayloadReplayRsyncFetcher::from_paths(&archive_root, &locks_path)
            .expect("build replay rsync fetcher");

        let err = fetcher
            .fetch_objects("rsync://missing.example/repo/")
            .unwrap_err();
        match err {
            RsyncFetchError::Fetch(msg) => assert!(
                msg.contains("no replay lock found for rsync module"),
                "{msg}"
            ),
        }

        let err = fetcher
            .fetch_objects("rsync://rsync.example.test/repo/missing/")
            .unwrap_err();
        match err {
            RsyncFetchError::Fetch(msg) => {
                assert!(msg.contains("replay rsync subtree not found"), "{msg}")
            }
        }
    }

    #[test]
    fn payload_replay_rsync_fetcher_rejects_invalid_uri_and_exposes_index() {
        let (_temp, archive_root, locks_path) = build_rsync_archive();
        let fetcher = PayloadReplayRsyncFetcher::from_paths(&archive_root, &locks_path)
            .expect("build replay rsync fetcher");
        assert_eq!(fetcher.archive_index().rsync_modules.len(), 1);

        let err = fetcher
            .fetch_objects("https://not-rsync.example/repo/")
            .unwrap_err();
        match err {
            RsyncFetchError::Fetch(msg) => {
                assert!(msg.contains("URI must start with rsync://"), "{msg}")
            }
        }
    }
}
