use gxhash::{HashMap, HashSet};
use ignore::{Walk, WalkBuilder};
use indicatif::{ParallelProgressIterator, ProgressIterator};
use smol_str::SmolStr;
use std::fs;
use std::fs::File;
use std::io;
use std::path;
use std::path::PathBuf;

pub struct Dirs {
    pub repo_dir: String,
    pub data_dir: Option<String>,
    pub patches_path: String,
    pub repo_base: String,
    pub ignore_path: String,
    pub lock_path: String,
    pub hash_path: String,
}
fn compare_vectors<T>(
    first: &Vec<T>,
    second: &Vec<T>,
) -> (Vec<(usize, T)>, Vec<(usize, T)>, Vec<(usize, T)>)
where
    T: std::hash::Hash + Eq + Clone,
{
    let first_set: HashSet<_> = first.iter().collect();
    let second_set: HashSet<_> = second.iter().collect();

    // Use set operations directly instead of filtering with contains
    let only_in_first: Vec<T> = first_set
        .difference(&second_set)
        .cloned()
        .cloned() // One clone for the reference, one for the value
        .collect();

    let only_in_second: Vec<T> = second_set
        .difference(&first_set)
        .cloned()
        .cloned()
        .collect();

    let in_both: Vec<T> = first_set
        .intersection(&second_set)
        .cloned()
        .cloned()
        .collect();

    // now we need to enumerate all
    // and use those same enumerations for only_in_first and only_in_second and both

    let mut hashmap = HashMap::<T, usize>::default();
    for f in second.iter().enumerate() {
        hashmap.insert(f.1.clone(), f.0);
    }

    (
        only_in_first
            .into_iter()
            .map(|f| (*hashmap.get(&f).unwrap_or(&usize::MAX), f))
            .collect(),
        only_in_second
            .into_iter()
            .map(|f| (*hashmap.get(&f).unwrap_or(&usize::MAX), f))
            .collect(),
        in_both
            .into_iter()
            .map(|f| (*hashmap.get(&f).unwrap_or(&usize::MAX), f))
            .collect(),
    )
}

pub fn get_diffs_dir(
    new_paths: &Vec<PathBuf>,
    repo_paths: &Vec<PathBuf>,
    dir1: &String,
    dir2: &String,
) -> (Vec<SmolStr>, Vec<SmolStr>, Vec<SmolStr>) {
    let diffs = compare_vectors(
        &new_paths
            .par_iter()
            .filter(|p| p.is_dir())
            .map(|p| SmolStr::from(p.strip_prefix(dir1).unwrap().to_str().unwrap()))
            .collect::<Vec<SmolStr>>(),
        &repo_paths
            .par_iter()
            .filter(|p| p.is_dir())
            .map(|p| SmolStr::from(p.strip_prefix(dir2).unwrap().to_str().unwrap()))
            .collect::<Vec<SmolStr>>(),
    );
    (
        diffs.0.into_iter().map(|h| h.1).collect(),
        diffs.1.into_iter().map(|h| h.1).collect(),
        diffs.2.into_iter().map(|h| h.1).collect(),
    )
}

pub fn get_diffs_file(
    new_paths: &Vec<PathBuf>,
    repo_paths: &Vec<PathBuf>,
    dir1: &String,
    dir2: &String,
    dont_check: bool,
) -> (
    Vec<(usize, SmolStr)>,
    Vec<(usize, SmolStr)>,
    Vec<(usize, SmolStr)>,
) {
    let mut new = new_paths
        .into_par_iter()
        .filter(|p| p.is_file())
        .map(|p| SmolStr::from(p.strip_prefix(dir1).unwrap().to_str().unwrap()))
        .collect::<Vec<SmolStr>>();
    new.sort_unstable();

    let mut old = repo_paths
        .into_par_iter()
        .filter(|p| p.is_file() || dont_check)
        .map(|p| SmolStr::from(p.strip_prefix(dir2).unwrap().to_str().unwrap()))
        .collect::<Vec<SmolStr>>();
    old.sort_unstable();

    compare_vectors(&new, &old)
}

pub fn rm_old(base: &String, paths: Vec<SmolStr>) -> Result<(), io::Error> {
    if !paths.is_empty() {
        let style = get_style("Cleaning files", paths.len());
        paths
            .iter() // cannot be par; one thread may delete while another thinks it exists
            .progress_with(style)
            .map(|path| -> Result<(), io::Error> {
                let path = PathBuf::from(format!("{}/{}", base, path));
                if path.is_dir() && path.exists() {
                    fs::remove_dir_all(path.clone())?;
                }
                if path.is_file() && path.exists() {
                    fs::remove_file(path)?;
                }
                Ok(())
            })
            .collect::<Result<(), io::Error>>()?;
    }
    Ok(())
}

use crate::misc::{compress_writer, decompress_reader, get_spinner, get_style};
use rayon::prelude::*;
use std::io::{BufReader, BufWriter};

pub fn copy_dir_all(
    src: &String,
    paths: &Vec<&PathBuf>,
    dst: &String,
    overwrite: bool,
    only_dirs: bool,
    compress: bool,
) -> Result<(), io::Error> {
    let copy_paths: Vec<(PathBuf, PathBuf, bool)> = paths
        .par_iter()
        .filter_map(|path| {
            let file = path.strip_prefix(src).unwrap().to_str().unwrap();
            let dst_file = PathBuf::from(format!("{}/{}", dst, file));
            let src_file = PathBuf::from(format!("{}/{}", src, file));
            let n_exists = !dst_file.exists();

            if path.is_file() && (n_exists || overwrite) && !only_dirs {
                return Some((dst_file, src_file, false));
            }

            if path.is_dir() && n_exists {
                return Some((dst_file, src_file, true));
            }

            None
        })
        .collect();

    let dirs: Vec<_> = copy_paths.iter().filter(|p| p.2).collect();
    let files: Vec<_> = copy_paths.iter().filter(|p| !p.2).collect();

    if !dirs.is_empty() {
        let style = get_style("Creating directories", dirs.len());
        dirs.into_par_iter()
            .progress_with(style)
            .map(|(dst_path, _, _)| fs::create_dir_all(dst_path))
            .collect::<Result<(), io::Error>>()?;
    }

    if !files.is_empty() {
        let msg = if overwrite {
            "Updating files"
        } else {
            "Copying files"
        };
        let style = get_style(msg, files.len());
        files
            .into_par_iter()
            .progress_with(style)
            .map(|(dst_file, src_file, _)| {
                if compress {
                    io::copy(
                        &mut BufReader::new(File::open(src_file)?),
                        &mut compress_writer(dst_file)?,
                    )?;
                } else {
                    io::copy(
                        &mut decompress_reader(src_file)?,
                        &mut BufWriter::new(File::create(dst_file)?),
                    )?;
                }
                Ok(())
            })
            .collect::<Result<(), io::Error>>()?;
    }
    Ok(())
}

pub fn setup_dirs(
    repo_dir: &PathBuf,
    data_dir: Option<&PathBuf>,
    should_exist: bool,
    should_data: bool,
) -> Dirs {
    let repo_dir = path::absolute(repo_dir).unwrap();
    if PathBuf::from(repo_dir.clone()).exists() != should_exist {
        if should_exist {
            println!("Repo doesn't exist, exiting")
        } else {
            println!("Repo already exists, exiting")
        }
        exit(1);
    }

    let data_dir = match data_dir {
        Some(dir) => {
            let dir = path::absolute(dir).unwrap();
            let dir = dir.to_str().unwrap().trim_end_matches('/').to_string();

            if PathBuf::from(dir.clone()).exists() != should_data {
                if should_data {
                    println!("Data directory doesn't exist, exiting");
                } else {
                    println!("Data directory already exists, exiting");
                }
                exit(1);
            }

            Some(dir)
        }
        _ => None,
    };

    let repo_dir = repo_dir.to_str().unwrap().trim_end_matches('/').to_string();
    let patches_path = format!("{}/patches", repo_dir);
    let repo_base = format!("{}/base", repo_dir);
    let ignore_path = format!("{}/.ignore", repo_dir);
    let lock_path = format!("{}/.lock", repo_dir);
    let hash_path = format!("{}/hashes", repo_dir);

    Dirs {
        repo_dir,
        data_dir,
        patches_path,
        repo_base,
        ignore_path,
        lock_path,
        hash_path,
    }
}

pub fn get_walk(dir: &String, ignore: &String) -> Result<Vec<PathBuf>, io::Error> {
    let mut base_walker = WalkBuilder::new(dir);
    if fs::exists(ignore.clone())? {
        base_walker.add_ignore(ignore);
    }
    let style_walk = get_spinner("Getting paths");
    let base_raw: Vec<_> = base_walker
        .standard_filters(false)
        .build()
        .progress_with(style_walk)
        .collect::<Vec<_>>();
    let style_filter = get_spinner("Filtering paths");
    let base_paths = base_raw
        .into_par_iter()
        .progress_with(style_filter)
        .filter_map(|r| r.ok())
        .map(|d| d.path().to_path_buf())
        .filter(|d| *d != PathBuf::from(dir))
        .map(|p| is_empty_dir(p))
        .collect::<Result<Vec<_>, io::Error>>()?
        .into_par_iter()
        .filter(|p| !p.0)
        .map(|p| p.1)
        .collect();
    Ok(base_paths)
}

pub fn get_patches(dir: String) -> Vec<PathBuf> {
    Walk::new(dir)
        .filter_map(|r| r.ok())
        .map(|d| d.path().to_path_buf())
        .filter(|p| p.is_dir())
        .collect::<Vec<PathBuf>>()[1..]
        .to_vec()
}

use std::process::exit;
pub fn init_lock(repo_dir: &PathBuf) -> Result<(), io::Error> {
    let lock_path = PathBuf::from(setup_dirs(repo_dir, None, true, false).lock_path);
    if lock_path.exists() {
        println!("Lockfile exists, exiting");
        exit(1)
    }
    File::create_new(lock_path)?;
    Ok(())
}

pub fn del_lock(repo_dir: &PathBuf) -> Result<(), io::Error> {
    let lock_path = setup_dirs(repo_dir, None, true, false).lock_path;
    fs::remove_file(lock_path)
}

fn is_empty_dir(p: PathBuf) -> Result<(bool, PathBuf), io::Error> {
    Ok((
        if p.is_dir() {
            p.read_dir()?.count() == 0
        } else {
            false
        },
        p,
    ))
}

use std::sync::Mutex;
pub fn get_full_dirs(paths: &Vec<PathBuf>, repo_base: &String) -> Vec<PathBuf> {
    let files: Vec<&PathBuf> = paths.par_iter().filter(|p| p.is_file()).collect();
    let gxhashes = Mutex::new(HashSet::<PathBuf>::default());
    files.par_iter().for_each(|f| {
        let mut parent = PathBuf::from(f.parent().unwrap());
        while parent != PathBuf::from(repo_base.clone()) {
            gxhashes.lock().unwrap().insert(parent.clone());
            parent = PathBuf::from(parent.parent().unwrap());
        }
    });
    gxhashes.into_inner().unwrap().drain().collect()
}

pub fn get_empty_dirs(
    paths: &Vec<PathBuf>,
    full_dirs: &Vec<PathBuf>,
    repo_base: &String,
) -> Vec<SmolStr> {
    get_diffs_dir(&full_dirs, &paths, repo_base, repo_base).1
}

use std::time::SystemTime;
use std::time::UNIX_EPOCH;
pub fn get_ts() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs()
}
