use std::path::{Path, PathBuf};
use std::{fs, io};

use fs_extra::error::ErrorKind;

#[napi]
pub fn remove(src: String) -> anyhow::Result<()> {
    fs_extra::remove_items(&[src]).map_err(|err| match err.kind {
        ErrorKind::Io(err_kind) => anyhow::Error::new(err_kind),
        _ => anyhow::Error::new(err),
    })
}

#[napi]
pub fn copy(src: String, dest: String) -> anyhow::Result<()> {
    let dest: PathBuf = dest.into();
    let dest_parent = dest.parent().unwrap_or(&dest);

    let src: PathBuf = src.into();

    if !dest_parent.exists() {
        fs::create_dir_all(dest_parent)?;
    }

    if src.is_dir() {
        copy_dir_all(&src, dest).map_err(anyhow::Error::new)?;
    } else if src.is_symlink() {
        symlink(fs::read_link(src)?, dest)?;
    } else {
        fs::copy(src, dest)?;
    }

    Ok(())
}

#[cfg(windows)]
fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> {
    std::os::windows::fs::symlink_file(original, link)
}

#[cfg(unix)]
fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> {
    std::os::unix::fs::symlink(original, link)
}

fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
    fs::create_dir_all(&dst)?;
    for entry in fs::read_dir(src)? {
        let entry = entry?;
        let ty = entry.file_type()?;
        if ty.is_dir() {
            copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
        } else if ty.is_symlink() {
            symlink(
                fs::read_link(entry.path())?,
                dst.as_ref().join(entry.file_name()),
            )?;
        } else {
            fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
        }
    }
    Ok(())
}

#[cfg(test)]
mod test {
    use super::*;
    use assert_fs::prelude::*;
    use assert_fs::TempDir;

    #[test]
    fn should_copy_directories() {
        let temp = TempDir::new().unwrap();
        temp.child("parent")
            .child("child")
            .child("grand-child")
            .child(".config")
            .child("file.txt")
            .touch()
            .unwrap();
        let src = temp.join("parent/child/grand-child/.config");
        let dest = temp.join("new-parent/child/grand-child/.config");
        copy(src.to_string_lossy().into(), dest.to_string_lossy().into()).unwrap();

        assert!(temp
            .child("new-parent/child/grand-child/.config/file.txt")
            .exists());
    }

    #[test]
    fn should_copy_single_files() {
        let temp = TempDir::new().unwrap();
        temp.child("parent")
            .child("file.txt")
            .write_str("content")
            .unwrap();

        let src = temp.join("parent/file.txt");
        let dest = temp.join("new-parent/file.txt");
        copy(src.to_string_lossy().into(), dest.to_string_lossy().into()).unwrap();

        assert!(temp.child("new-parent/file.txt").exists());
    }

    #[test]
    fn should_copy_symlinks() {
        let temp = TempDir::new().unwrap();
        let target = temp.child("parent").child("target.txt");
        target.touch().unwrap();
        let link = temp.child("parent").child("file.txt");

        link.symlink_to_file(&target).unwrap();

        let src = temp.join("parent/file.txt");
        let dest = temp.join("new-parent/file.txt");
        copy(src.to_string_lossy().into(), dest.to_string_lossy().into()).unwrap();

        assert!(temp.child("new-parent/file.txt").exists());
        assert_eq!(
            temp.child("new-parent/file.txt").read_link().unwrap(),
            target.path()
        );
    }

    #[test]
    fn should_copy_directories_with_symlinks() {
        let temp = TempDir::new().unwrap();
        let target = temp.child("parent").child("target.txt");
        target.touch().unwrap();
        let link = temp.child("parent").child("file.txt");

        link.symlink_to_file(&target).unwrap();

        let src = temp.join("parent");
        let dest = temp.join("new-parent");
        copy(src.to_string_lossy().into(), dest.to_string_lossy().into()).unwrap();

        assert!(temp.child("new-parent/file.txt").exists());
        assert_eq!(
            temp.child("new-parent/file.txt").read_link().unwrap(),
            target.path()
        );
    }
}
