use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;

use sha2::Digest;
use uuid::Uuid;

use crate::fetch::rsync::{
    RsyncFetchError, RsyncFetchResult, RsyncFetcher, normalize_rsync_base_uri,
};

#[derive(Clone, Debug)]
pub struct SystemRsyncConfig {
    pub rsync_bin: PathBuf,
    pub timeout: Duration,
    pub extra_args: Vec<String>,
    /// Optional root directory for persistent rsync mirrors.
    ///
    /// When set, callers may choose to sync into stable subdirectories under this
    /// root (instead of a temporary directory) to benefit from rsync's incremental
    /// behavior across runs.
    ///
    /// Note: actual mirror behavior is implemented separately from config wiring.
    pub mirror_root: Option<PathBuf>,
}

impl Default for SystemRsyncConfig {
    fn default() -> Self {
        Self {
            rsync_bin: PathBuf::from("rsync"),
            timeout: Duration::from_secs(60),
            extra_args: Vec::new(),
            mirror_root: None,
        }
    }
}

/// A `RsyncFetcher` implementation backed by the system `rsync` binary.
///
/// This is intended for live stage2 runs. For unit tests and offline fixtures,
/// prefer `LocalDirRsyncFetcher`.
#[derive(Clone, Debug)]
pub struct SystemRsyncFetcher {
    config: SystemRsyncConfig,
}

impl SystemRsyncFetcher {
    pub fn new(config: SystemRsyncConfig) -> Self {
        Self { config }
    }

    fn mirror_dst_dir(&self, normalized_rsync_base_uri: &str) -> Result<Option<PathBuf>, String> {
        let Some(root) = self.config.mirror_root.as_ref() else {
            return Ok(None);
        };

        std::fs::create_dir_all(root)
            .map_err(|e| format!("create rsync mirror root failed: {}: {e}", root.display()))?;

        let hash = hex::encode(sha2::Sha256::digest(normalized_rsync_base_uri.as_bytes()));
        let dir = root.join(hash);
        std::fs::create_dir_all(&dir).map_err(|e| {
            format!(
                "create rsync mirror directory failed: {}: {e}",
                dir.display()
            )
        })?;
        Ok(Some(dir))
    }

    fn run_rsync(&self, src: &str, dst: &Path) -> Result<(), String> {
        // `--timeout` is I/O timeout in seconds (applies to network reads/writes).
        let timeout_secs = self.config.timeout.as_secs().max(1).to_string();

        let mut cmd = Command::new(&self.config.rsync_bin);
        cmd.arg("-rt")
            .arg("--delete")
            .arg("--timeout")
            .arg(timeout_secs)
            .args(&self.config.extra_args)
            .arg(src)
            .arg(dst);

        let out = cmd
            .output()
            .map_err(|e| format!("rsync spawn failed: {e}"))?;
        if !out.status.success() {
            let stderr = String::from_utf8_lossy(&out.stderr);
            let stdout = String::from_utf8_lossy(&out.stdout);
            return Err(format!(
                "rsync failed: status={} stdout={} stderr={}",
                out.status,
                stdout.trim(),
                stderr.trim()
            ));
        }
        Ok(())
    }

    fn module_fetch_uri(&self, rsync_base_uri: &str) -> String {
        rsync_module_root_uri(rsync_base_uri)
            .unwrap_or_else(|| normalize_rsync_base_uri(rsync_base_uri))
    }
}

impl RsyncFetcher for SystemRsyncFetcher {
    fn fetch_objects(&self, rsync_base_uri: &str) -> RsyncFetchResult<Vec<(String, Vec<u8>)>> {
        let mut out = Vec::new();
        self.visit_objects(rsync_base_uri, &mut |uri, bytes| {
            out.push((uri, bytes));
            Ok(())
        })?;
        Ok(out)
    }

    fn visit_objects(
        &self,
        rsync_base_uri: &str,
        visitor: &mut dyn FnMut(String, Vec<u8>) -> Result<(), String>,
    ) -> RsyncFetchResult<(usize, u64)> {
        let base = self.module_fetch_uri(rsync_base_uri);
        let mut count = 0usize;
        let mut bytes_total = 0u64;
        let mut wrapped = |uri: String, bytes: Vec<u8>| -> Result<(), String> {
            bytes_total += bytes.len() as u64;
            count += 1;
            visitor(uri, bytes)
        };

        if let Some(dst) = self
            .mirror_dst_dir(&base)
            .map_err(|e| RsyncFetchError::Fetch(e.to_string()))?
        {
            self.run_rsync(&base, &dst)
                .map_err(RsyncFetchError::Fetch)?;
            walk_dir_visit(&dst, &dst, &base, &mut wrapped).map_err(RsyncFetchError::Fetch)?;
            return Ok((count, bytes_total));
        }

        let tmp = TempDir::new().map_err(|e| RsyncFetchError::Fetch(e.to_string()))?;
        self.run_rsync(&base, tmp.path())
            .map_err(RsyncFetchError::Fetch)?;
        walk_dir_visit(tmp.path(), tmp.path(), &base, &mut wrapped).map_err(RsyncFetchError::Fetch)?;
        Ok((count, bytes_total))
    }

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

struct TempDir {
    path: PathBuf,
}

impl TempDir {
    fn new() -> Result<Self, String> {
        let mut p = std::env::temp_dir();
        p.push(format!("rpki-system-rsync-{}", Uuid::new_v4()));
        std::fs::create_dir_all(&p).map_err(|e| e.to_string())?;
        Ok(Self { path: p })
    }

    fn path(&self) -> &Path {
        &self.path
    }
}

impl Drop for TempDir {
    fn drop(&mut self) {
        let _ = std::fs::remove_dir_all(&self.path);
    }
}

fn rsync_module_root_uri(s: &str) -> Option<String> {
    let normalized = normalize_rsync_base_uri(s);
    let rest = normalized.strip_prefix("rsync://")?;
    let mut host_and_path = rest.splitn(2, '/');
    let authority = host_and_path.next()?;
    let path = host_and_path.next()?;
    let mut segments: Vec<&str> = path.split('/').filter(|segment| !segment.is_empty()).collect();
    if segments.is_empty() {
        return None;
    }
    let module = segments.remove(0);
    Some(format!("rsync://{authority}/{module}/"))
}

fn walk_dir_collect(
    root: &Path,
    current: &Path,
    rsync_base_uri: &str,
    out: &mut Vec<(String, Vec<u8>)>,
) -> Result<(), String> {
    let rd = std::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 uri = format!("{rsync_base_uri}{rel}");
        let bytes = std::fs::read(&path).map_err(|e| e.to_string())?;
        out.push((uri, bytes));
    }
    Ok(())
}

fn walk_dir_visit(
    root: &Path,
    current: &Path,
    rsync_base_uri: &str,
    visitor: &mut dyn FnMut(String, Vec<u8>) -> Result<(), String>,
) -> Result<(), String> {
    let rd = std::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_visit(root, &path, rsync_base_uri, visitor)?;
            continue;
        }
        if !meta.is_file() {
            continue;
        }
        let rel = path
            .strip_prefix(root)
            .map_err(|e| e.to_string())?
            .to_string_lossy()
            .replace('\\', "/");
        let uri = format!("{rsync_base_uri}{rel}");
        let bytes = std::fs::read(&path).map_err(|e| e.to_string())?;
        visitor(uri, bytes)?;
    }
    Ok(())
}

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

    #[test]
    fn normalize_rsync_base_uri_appends_slash_when_missing() {
        assert_eq!(
            normalize_rsync_base_uri("rsync://example.net/repo"),
            "rsync://example.net/repo/".to_string()
        );
        assert_eq!(
            normalize_rsync_base_uri("rsync://example.net/repo/"),
            "rsync://example.net/repo/".to_string()
        );
    }

    #[test]
    fn walk_dir_collect_collects_files_and_normalizes_backslashes_in_uri() {
        let temp = tempfile::tempdir().expect("tempdir");
        let root = temp.path();
        std::fs::create_dir_all(root.join("sub")).expect("mkdir");
        std::fs::write(root.join("sub").join("a.cer"), b"x").expect("write");
        std::fs::write(root.join("b\\c.mft"), b"y").expect("write backslash file");

        let mut out: Vec<(String, Vec<u8>)> = Vec::new();
        walk_dir_collect(root, root, "rsync://example.net/repo/", &mut out).expect("walk");
        out.sort_by(|a, b| a.0.cmp(&b.0));

        assert_eq!(out.len(), 2);
        assert_eq!(out[0].0, "rsync://example.net/repo/b/c.mft");
        assert_eq!(out[0].1, b"y");
        assert_eq!(out[1].0, "rsync://example.net/repo/sub/a.cer");
        assert_eq!(out[1].1, b"x");
    }

    #[test]
    fn rsync_module_root_uri_returns_host_and_module_only() {
        assert_eq!(
            rsync_module_root_uri("rsync://example.net/repo/ta/ca/publication-point/"),
            Some("rsync://example.net/repo/".to_string())
        );
        assert_eq!(
            rsync_module_root_uri("rsync://example.net/repo/ta/"),
            Some("rsync://example.net/repo/".to_string())
        );
        assert_eq!(
            rsync_module_root_uri("rsync://example.net/repo/"),
            Some("rsync://example.net/repo/".to_string())
        );
        assert_eq!(rsync_module_root_uri("https://example.net/repo"), None);
    }

    #[test]
    fn system_rsync_dedup_key_uses_module_root() {
        let fetcher = SystemRsyncFetcher::new(SystemRsyncConfig::default());
        assert_eq!(
            fetcher.dedup_key("rsync://example.net/repo/ta/ca/publication-point/"),
            "rsync://example.net/repo/"
        );
    }

    #[test]
    fn system_rsync_fetcher_reports_spawn_and_exit_errors() {
        let dst = tempfile::tempdir().expect("tempdir");

        // 1) Spawn error.
        let f = SystemRsyncFetcher::new(SystemRsyncConfig {
            rsync_bin: PathBuf::from("/this/does/not/exist/rsync"),
            timeout: Duration::from_secs(1),
            extra_args: Vec::new(),
            mirror_root: None,
        });
        let e = f
            .run_rsync("rsync://example.net/repo/", dst.path())
            .expect_err("spawn must fail");
        assert!(e.contains("rsync spawn failed:"), "{e}");

        // 2) Non-zero exit status.
        let f = SystemRsyncFetcher::new(SystemRsyncConfig {
            rsync_bin: PathBuf::from("false"),
            timeout: Duration::from_secs(1),
            extra_args: Vec::new(),
            mirror_root: None,
        });
        let e = f
            .run_rsync("rsync://example.net/repo/", dst.path())
            .expect_err("false must fail");
        assert!(e.contains("rsync failed:"), "{e}");
        assert!(e.contains("status="), "{e}");
    }

    #[test]
    fn mirror_dst_dir_reports_root_creation_error() {
        let temp = tempfile::tempdir().expect("tempdir");
        let root_file = temp.path().join("mirror-root-file");
        std::fs::write(&root_file, b"not a directory").expect("write root file");

        let fetcher = SystemRsyncFetcher::new(SystemRsyncConfig {
            rsync_bin: PathBuf::from("rsync"),
            timeout: Duration::from_secs(1),
            extra_args: Vec::new(),
            mirror_root: Some(root_file.clone()),
        });

        let err = fetcher
            .mirror_dst_dir("rsync://example.net/repo/")
            .expect_err("file mirror root must fail");
        assert!(err.contains("create rsync mirror root failed"), "{err}");
        assert!(err.contains(&root_file.display().to_string()), "{err}");
    }

    #[cfg(unix)]
    #[test]
    fn mirror_dst_dir_reports_directory_creation_error_inside_root() {
        use std::os::unix::fs::PermissionsExt;

        let temp = tempfile::tempdir().expect("tempdir");
        let root = temp.path().join("mirror");
        std::fs::create_dir_all(&root).expect("mkdir root");
        let mut perms = std::fs::metadata(&root).expect("metadata").permissions();
        perms.set_mode(0o555);
        std::fs::set_permissions(&root, perms).expect("chmod root readonly");

        let fetcher = SystemRsyncFetcher::new(SystemRsyncConfig {
            rsync_bin: PathBuf::from("rsync"),
            timeout: Duration::from_secs(1),
            extra_args: Vec::new(),
            mirror_root: Some(root.clone()),
        });

        let err = fetcher
            .mirror_dst_dir("rsync://example.net/repo/")
            .expect_err("readonly mirror root must fail");
        assert!(
            err.contains("create rsync mirror directory failed"),
            "{err}"
        );

        let mut perms = std::fs::metadata(&root).expect("metadata").permissions();
        perms.set_mode(0o755);
        std::fs::set_permissions(&root, perms).expect("restore perms");
    }

    #[cfg(unix)]
    #[test]
    fn walk_dir_collect_ignores_non_file_entries() {
        use std::os::unix::net::UnixListener;

        let temp = tempfile::tempdir().expect("tempdir");
        let root = temp.path();
        std::fs::write(root.join("a.cer"), b"x").expect("write file");
        let socket_path = root.join("skip.sock");
        let _listener = UnixListener::bind(&socket_path).expect("bind socket");

        let mut out: Vec<(String, Vec<u8>)> = Vec::new();
        walk_dir_collect(root, root, "rsync://example.net/repo/", &mut out).expect("walk");

        assert_eq!(out.len(), 1);
        assert_eq!(out[0].0, "rsync://example.net/repo/a.cer");
    }
}
