/// NCBI FTP file listing and download.
use regex::Regex;
use std::path::{Path, PathBuf};

const BASE: &str = "https://ftp.ncbi.nlm.nih.gov/pubmed";

#[derive(Debug, Clone)]
pub struct FtpFile {
    pub name: String,
    pub url: String,
    pub md5_url: String,
}

/// List .xml.gz files from NCBI HTTPS directory listing.
pub fn list_files(baseline: bool, updates: bool) -> anyhow::Result<Vec<FtpFile>> {
    let agent = ureq::AgentBuilder::new()
        .timeout_connect(std::time::Duration::from_secs(30))
        .timeout_read(std::time::Duration::from_secs(30))
        .build();
    let re = Regex::new(r#"href="(pubmed\d+n\d+\.xml\.gz)""#)?;

    let mut files = Vec::new();
    let mut urls = Vec::new();
    if baseline { urls.push(format!("{BASE}/baseline/")); }
    if updates  { urls.push(format!("{BASE}/updatefiles/")); }

    for url in urls {
        let html = agent.get(&url).call()?.into_string()?;
        for cap in re.captures_iter(&html) {
            let name = cap[1].to_owned();
            files.push(FtpFile {
                url: format!("{url}{name}"),
                md5_url: format!("{url}{name}.md5"),
                name,
            });
        }
    }
    files.sort_by(|a, b| a.name.cmp(&b.name));
    Ok(files)
}

/// Download a single file with MD5 verification.
pub fn download_file(
    agent: &ureq::Agent,
    f: &FtpFile,
    cache_dir: &Path,
) -> anyhow::Result<PathBuf> {
    let dest = cache_dir.join(&f.name);
    let expected_md5 = fetch_md5(agent, &f.md5_url);

    // Skip if cached and valid
    if dest.exists() {
        match &expected_md5 {
            Some(expected) if verify_md5(&dest, expected)? => return Ok(dest),
            None => return Ok(dest),
            _ => {}
        }
    }

    let tmp = dest.with_extension("tmp");
    {
        let resp = agent.get(&f.url).call()?;
        let mut reader = resp.into_reader();
        let mut file = std::fs::File::create(&tmp)?;
        std::io::copy(&mut reader, &mut file)?;
    }

    if let Some(ref expected) = expected_md5 {
        if !verify_md5(&tmp, expected)? {
            std::fs::remove_file(&tmp).ok();
            anyhow::bail!("MD5 mismatch: {}", f.name);
        }
    }
    std::fs::rename(&tmp, &dest)?;
    Ok(dest)
}

fn fetch_md5(agent: &ureq::Agent, url: &str) -> Option<String> {
    let text = agent.get(url).call().ok()?.into_string().ok()?;
    text.split_whitespace()
        .find(|w| w.len() == 32 && w.chars().all(|c| c.is_ascii_hexdigit()))
        .map(|s| s.to_lowercase())
}

fn verify_md5(path: &Path, expected: &str) -> anyhow::Result<bool> {
    use md5::{Md5, Digest};
    let mut hasher = Md5::new();
    let mut file = std::fs::File::open(path)?;
    std::io::copy(&mut file, &mut hasher)?;
    Ok(format!("{:x}", hasher.finalize()) == expected)
}
