use std::fs::*;
use rayon::prelude::*;
use std::io::BufReader;
use std::io::prelude::*;
use sha2::{Digest, Sha256};


struct ProgramFlags{
	verbose: bool,
	write_file_list: bool,
}

impl ProgramFlags {
	fn new(args: &Vec<String>) -> Self{
		let mut verbose = false;
		let mut write_file_list = false;

		for string in args.iter(){
			match string.as_str(){
				"--verbose" | "-v" => {
					verbose = true;
				},

				"--write-file-list" | "-w" => {
					write_file_list = true;
				},

				"--help" | "-h" => {
					println!("Usage: {:?} <file_or_directory_path> [options]", args[0]);
					println!("Options:");
					println!("  --verbose, -v               Enable verbose output");
					println!("  --write-file-list, -w       Write the list of processed files to file_list.txt");
					println!("  --help, -h                  Show this help message");
					std::process::exit(0);
				},

				_ => {}
			}
		}

		ProgramFlags {
			verbose,
			write_file_list,
		}
	}
}

struct FileHasher {
    file_path: String,
}


impl FileHasher{
	fn new(file_path: String) -> Self{
		FileHasher { file_path }
	}

	fn hash_file(&mut self) -> String{
		let mut file = File::open(&self.file_path).expect("Unable to open file");
		let mut reader = BufReader::new(&mut file);
		let mut hasher = Sha256::new();
		let mut buffer = [0; 999999];

		loop{
			let bytes_read = reader.read(&mut buffer).expect("Unable to read from file");
			if bytes_read == 0 {
				break;
			}
			hasher.update(&buffer[..bytes_read]);
		}

		let result = hasher.finalize();

		// convert into a hex string
		let hash_string = format!("{:x}", result);
		hash_string
	}
}


/// Recursively gets all files in a directory and its subdirectories.
/// Returns a vector of file paths as strings.
fn get_all_files_in_directory(dir_path: &str) -> Vec<String> {
	let metadata = metadata(dir_path);
	match metadata {
		Ok(meta) => {
			if !meta.is_dir() {
				panic!("The provided path is not a directory: {}", dir_path);
			}
		},
		Err(e) => {
			panic!("Unable to read metadata for {}: {}", dir_path, e);
		}
	}
	let mut file_paths: Vec<String> = Vec::new();

	let entries = read_dir(dir_path).expect("Unable to read directory");
	for entry in entries {
		let entry = entry.expect("Unable to get directory entry");
		let path = entry.path();
		if path.is_file() {
			if let Some(path_str) = path.to_str() {
				file_paths.push(path_str.to_string());
			}
		}else if path.is_dir() {
			if let Some(path_str) = path.to_str() {
				let mut nested_files = get_all_files_in_directory(path_str);
				file_paths.append(&mut nested_files);
			}
		}
	}

	file_paths
}

/// Resolves a given path, expanding '~' and returns the absolute path of items.
/// For example, if '~/docs' contains two files 'file1.txt' and 'file2.txt',
/// the function will return a vector containing the absolute paths of these files.
/// If the path is a file, it will return a vector containing only that file's absolute path.
fn resolve_path(path: &str) -> Vec<String> {
	let mut resolved_path = path.to_string();
	if resolved_path.starts_with("~") {
		let home_str = dirs::home_dir()
			.expect("Unable to find home directory")
			.to_str()
			.expect("Unable to convert home directory to string")
			.to_string();
		resolved_path = resolved_path.replacen("~", &*home_str, 1)
	}

	// check if it's a file or directory
	let metadata = metadata(&resolved_path).expect("Unable to get metadata");
	if metadata.is_file(){
		return vec![resolved_path];
	} else if metadata.is_dir(){
		let items = get_all_files_in_directory(&resolved_path);
		return items;
	}

	Vec::new()
}


fn main() {
	let args = std::env::args().collect::<Vec<String>>();
	let flags = ProgramFlags::new(&args);
	if args.len() < 2 {
		println!("Usage: {} <file_or_directory_path>", args[0]);
		return;
	}
	let path = &args[1];
	let items = resolve_path(path);

	// use rayon to parallelize the hashing
	let mut hashes = items.par_iter().map(|item| {
		let mut file_hasher = FileHasher::new(item.to_string());
		let hash = file_hasher.hash_file();
		hash
	}).collect::<Vec<String>>();

	// sort the hashes to ensure a consistent order
	hashes.sort();

	// hash the combined hashes
	let mut final_hasher = Sha256::new();
	for hash in hashes {
		final_hasher.update(hash.as_bytes());
	}

	let final_hash = final_hasher.finalize();
	let final_hash_string = format!("{:x}", final_hash);
	println!("{}", final_hash_string);

	if flags.write_file_list {
		let mut file = File::create("file_list.txt").expect("Unable to create file_list.txt");
		for item in items.iter() {
			writeln!(file, "{}", item).expect("Unable to write to file_list.txt");
		}
	}

	if flags.verbose {
		println!("Processed {} items.", items.len());
	}

}
