use std::{
    collections::HashSet,
    ffi::OsStr,
    fs,
    io::{Error, ErrorKind},
    path::Path,
};

use anyhow::{Context, Result};
use clap::{App, Arg};

fn main() -> Result<()> {
    let matches = App::new("Clean Workspace")
        .version("1.0.1")
        .author("Phillip Couto <phillip@couto.in>")
        .about("Cleans packages and build files from projects in workspace folder.")
        .arg(
            Arg::with_name("WORKSPACE PATH")
                .help("Path to workspace folder")
                .required(true)
                .index(1),
        )
        .arg(Arg::with_name("dry-run")
            .long("dry-run")
            .help("Only display the folders to be removed without removing them.")
    )
        .get_matches();

    let workspace_path = matches.value_of("WORKSPACE PATH").unwrap();
    let dry_run = matches.is_present("dry-run");

    let folders_to_remove = find_folders_to_remove(&workspace_path)?;

    let mut total: u32 = 0;
    for folder in folders_to_remove {
        if dry_run{
            println!("would remove: {:}", folder);
        } else {
        println!("removing: {:}", folder);
        total += remove_folder(&folder)?;
        }
    }
    if !dry_run{
    println!("removed a total of {} files and folders", total);
    }
    Ok(())
}

fn find_folders_to_remove(workspace_path: &str) -> std::io::Result<HashSet<String>> {
    let mut folders_to_remove: HashSet<String> = HashSet::with_capacity(100);

    let mut queue: Vec<String> = Vec::with_capacity(100);

    queue.push(workspace_path.to_owned());

    while let Some(folder) = queue.pop() {
        if folders_to_remove.contains(&folder) {
            continue;
        }

        for entry in fs::read_dir(folder)? {
            let entry = entry?;
            let path = entry.path();
            let path_string = path.to_string_lossy().to_string();
            if path.is_dir() {
                queue.push(path_string);
                continue;
            }
            if path.file_name() == Some(OsStr::new("package.json")) {
                let modules_folder = path
                    .parent()
                    .ok_or_else(|| Error::new(ErrorKind::Other, "missing parent folder"))?;
                let modules_folder = Path::join(modules_folder, "node_modules");
                if fs::metadata(&modules_folder).is_ok() {
                    folders_to_remove.insert(modules_folder.to_string_lossy().to_string());
                }
            } else if path.file_name() == Some(OsStr::new("Cargo.toml")) {
                let targets_folder = path
                    .parent()
                    .ok_or_else(|| Error::new(ErrorKind::Other, "missing parent folder"))?;
                let targets_folder = Path::join(targets_folder, "target");
                if fs::metadata(&targets_folder).is_ok() {
                    folders_to_remove.insert(targets_folder.to_string_lossy().to_string());
                }
            }
        }
    }

    Ok(folders_to_remove)
}

fn remove_folder(folder: &str) -> Result<u32> {
    let mut count: u32 = 0;
    for entry in fs::read_dir(folder)? {
        let entry = entry?;
        let path = entry.path();
        let path_string = path.to_string_lossy().to_string();
        let lstat =
            fs::symlink_metadata(&path).with_context(|| format!("lstate path: {}", path_string))?;
        if path.is_dir() && lstat.is_dir() {
            count += remove_folder(&path_string)?;
        } else {
            fs::remove_file(&path).with_context(|| format!("remove file: {}", path_string))?;
            count += 1;
        }
    }
    fs::remove_dir(folder).with_context(|| format!("remove folder: {}", folder))?;
    Ok(count + 1)
}
