use sha2::Digest;
use std::path::{Path, PathBuf};

const BUNDLE_URL_BASE: &str = "https://github.com/matthewjberger/nightshade/releases/download";
const BUNDLE_SIZE_LIMIT: u64 = 256 * 1024 * 1024;

fn main() {
    println!("cargo:rerun-if-changed=../dist");
    println!("cargo:rerun-if-changed=bundle.sha256");
    let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR is set"));
    let manifest_dir =
        PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set"));
    let staging = out_dir.join("dist");
    if staging.exists() {
        std::fs::remove_dir_all(&staging).expect("failed to clear the staged web bundle");
    }
    std::fs::create_dir_all(&staging).expect("failed to create the staged web bundle directory");
    let local_dist = manifest_dir.join("../dist");
    if local_dist.join("index.html").exists() {
        copy_directory(&local_dist, &staging);
        return;
    }
    let expected_hash = std::fs::read_to_string(manifest_dir.join("bundle.sha256"))
        .ok()
        .map(|contents| contents.trim().to_lowercase());
    let cache_path = out_dir.join("nightshade-editor-web.tar.gz");
    let archive = match cached_bundle(&cache_path, expected_hash.as_deref()) {
        Some(archive) => archive,
        None => {
            let version = std::env::var("CARGO_PKG_VERSION").expect("CARGO_PKG_VERSION is set");
            let url = format!("{BUNDLE_URL_BASE}/v{version}/nightshade-editor-web.tar.gz");
            match download(&url) {
                Ok(archive) => {
                    verify(expected_hash.as_deref(), &archive, &url);
                    std::fs::write(&cache_path, &archive)
                        .expect("failed to cache the editor web bundle");
                    archive
                }
                Err(error) => {
                    report_missing_bundle(&url, &error);
                    return;
                }
            }
        }
    };
    let decoder = flate2::read::GzDecoder::new(archive.as_slice());
    tar::Archive::new(decoder)
        .unpack(&staging)
        .expect("failed to unpack the editor web bundle");
}

fn cached_bundle(cache_path: &Path, expected_hash: Option<&str>) -> Option<Vec<u8>> {
    let expected_hash = expected_hash?;
    let archive = std::fs::read(cache_path).ok()?;
    let actual_hash = format!("{:x}", sha2::Sha256::digest(&archive));
    (actual_hash == expected_hash).then_some(archive)
}

fn download(url: &str) -> Result<Vec<u8>, ureq::Error> {
    let agent: ureq::Agent = ureq::Agent::config_builder()
        .timeout_connect(Some(std::time::Duration::from_secs(10)))
        .timeout_global(Some(std::time::Duration::from_secs(300)))
        .build()
        .into();
    let mut response = agent.get(url).call()?;
    response
        .body_mut()
        .with_config()
        .limit(BUNDLE_SIZE_LIMIT)
        .read_to_vec()
}

fn verify(expected_hash: Option<&str>, archive: &[u8], url: &str) {
    let Some(expected_hash) = expected_hash else {
        println!(
            "cargo:warning=bundle.sha256 is missing, the downloaded web bundle was not verified"
        );
        return;
    };
    let actual_hash = format!("{:x}", sha2::Sha256::digest(archive));
    assert_eq!(
        expected_hash, actual_hash,
        "the editor web bundle downloaded from {url} does not match bundle.sha256"
    );
}

fn report_missing_bundle(url: &str, error: &ureq::Error) {
    if std::env::var("PROFILE").as_deref() == Ok("release") {
        panic!(
            "failed to download the editor web bundle from {url}: {error}\nretry with network access, or build the bundle from the repo with `just dist` and install with `just install`"
        );
    }
    println!("cargo:warning=failed to download the editor web bundle from {url}: {error}");
    println!("cargo:warning=the editor will report a missing web bundle at launch");
}

fn copy_directory(source: &Path, destination: &Path) {
    std::fs::create_dir_all(destination).expect("failed to create a web bundle directory");
    for entry in std::fs::read_dir(source).expect("failed to read the local web bundle") {
        let entry = entry.expect("failed to read a local web bundle entry");
        let target = destination.join(entry.file_name());
        if entry
            .file_type()
            .expect("failed to inspect a local web bundle entry")
            .is_dir()
        {
            copy_directory(&entry.path(), &target);
        } else {
            std::fs::copy(entry.path(), &target).expect("failed to copy a web bundle file");
        }
    }
}
