use log::{error, info, warn};
use std::path::{Path, PathBuf};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum RunDirError {
    #[error("Unable to initialize rundir.")]
    Initialize { path: PathBuf },
    #[error("The path provided is not a directory.")]
    PathIsNotDir { path: PathBuf },
    #[error("The directory is not empty.")]
    DirIsNotEmpty { path: PathBuf, child_path: PathBuf },
    #[error("Unable to create directory: {}", path.display())]
    CreateDir {
        path: PathBuf,
        #[source]
        source: std::io::Error,
    },
    #[error("Unable to scan directory: {}", path.display())]
    ScanDir {
        path: PathBuf,
        #[source]
        source: std::io::Error,
    },
    #[error("Unable to recreate directory: {}", path.display())]
    RecreateDir {
        path: PathBuf,
        #[source]
        source: std::io::Error,
    },
    #[error("Unable to remove subdirectory: {}", path.display())]
    RemoveSubDir {
        path: PathBuf,
        #[source]
        source: std::io::Error,
    },
    #[error("Invalid subdirectory name: {}", name)]
    InvalidSubDirName {
        name: String,
        inner_error: Option<std::io::Error>,
    },
}

type Result<T, E = RunDirError> = std::result::Result<T, E>;

pub struct RunDir {
    path: PathBuf,
    allow_cleaning: bool,
}

impl RunDir {
    pub fn new<T: Into<PathBuf>>(path: T) -> RunDir {
        RunDir {
            path: path.into(),
            allow_cleaning: false,
        }
    }

    /// Set whether the directory can perform cleanup operations.
    ///
    /// Use with caution. This will clear existing directories on initialization
    /// and cleanup.
    pub fn allow_cleaning(mut self, allow_cleaning: bool) -> RunDir {
        self.allow_cleaning = allow_cleaning;

        self
    }

    /// Creates the initial RunDir.
    ///
    /// # Examples
    ///
    /// ```
    /// use collective::rundir::RunDir;
    ///
    /// let rundir = RunDir::new("tests/rundir").allow_cleaning(true);
    ///
    /// rundir.initialize().unwrap();
    ///
    /// rundir.cleanup().unwrap();
    /// ```
    pub fn initialize(&self) -> Result<()> {
        if Path::exists(&self.path) {
            info!("RunDir already exists: {}", self.path.display());

            if !Path::is_dir(&self.path) {
                error!("RunDir is not a directory: {}", self.path.display());

                return Err(RunDirError::PathIsNotDir {
                    path: self.path.clone(),
                });
            } else {
                // Scan dir
                let mut dir_iterator =
                    std::fs::read_dir(&self.path).map_err(|source| RunDirError::ScanDir {
                        path: self.path.clone(),
                        source,
                    })?;

                let existing_child_path = dir_iterator
                    .next()
                    .map(|entry_result| entry_result.map(|entry| entry.path()));

                if let Some(child_path) = existing_child_path {
                    let child_path = child_path.map_err(|source| RunDirError::ScanDir {
                        path: self.path.clone(),
                        source,
                    })?;

                    if self.allow_cleaning {
                        info!("Recreating RunDir.");

                        std::fs::remove_dir_all(&self.path).map_err(|source| {
                            RunDirError::RecreateDir {
                                path: self.path.clone(),
                                source,
                            }
                        })?;

                        std::fs::create_dir_all(&self.path).map_err(|source| {
                            RunDirError::RecreateDir {
                                path: self.path.clone(),
                                source,
                            }
                        })?;
                    } else {
                        return Err(RunDirError::DirIsNotEmpty {
                            path: self.path.clone(),
                            child_path,
                        });
                    }
                }
            }
        } else {
            info!("Creating new RunDir: {}", self.path.display());

            std::fs::create_dir_all(&self.path).map_err(|source| RunDirError::CreateDir {
                path: self.path.clone(),
                source,
            })?
        }

        Ok(())
    }

    pub fn cleanup(&self) -> Result<()> {
        if self.allow_cleaning {
            std::fs::remove_dir_all(&self.path).map_err(|source| RunDirError::RecreateDir {
                path: self.path.clone(),
                source,
            })?;

            info!("Cleaned up RunDir: {}", self.path.display());
        } else {
            warn!("Leaving RunDir unmodified. Manual cleanup may be needed.");
        }

        Ok(())
    }

    /// Creates a subdir within the RunDir.
    pub fn create_subdir(&self, name: &str) -> Result<PathBuf> {
        let pathbuf = self.validate_subdir_name(name)?;

        std::fs::create_dir(&pathbuf).map_err(|source| RunDirError::CreateDir {
            path: pathbuf.clone(),
            source,
        })?;

        Ok(pathbuf)
    }

    /// Removes a subdir and all its contents from the RunDir.
    pub fn remove_subdir_all(&self, name: &str) -> Result<()> {
        let pathbuf = self.validate_subdir_name(name)?;

        std::fs::remove_dir_all(&pathbuf).map_err(|source| RunDirError::RemoveSubDir {
            path: pathbuf,
            source,
        })?;

        Ok(())
    }

    /// Checks if a subdir exists within the RunDir.
    pub fn subdir_exists(&self, name: &str) -> Result<bool> {
        let pathbuf = self.validate_subdir_name(name)?;

        Ok(pathbuf.exists())
    }

    fn validate_subdir_name(&self, name: &str) -> Result<PathBuf> {
        // Check that the name results in a dir exactly one level below the current one.
        let mut pathbuf = self.path.clone();

        pathbuf.push(name);

        if let Some(parent) = pathbuf.parent() {
            if parent != self.path.as_path() {
                return Err(RunDirError::InvalidSubDirName {
                    name: String::from(name),
                    inner_error: None,
                });
            }
        } else {
            return Err(RunDirError::InvalidSubDirName {
                name: String::from(name),
                inner_error: None,
            });
        }

        Ok(pathbuf)
    }
}

impl AsRef<Path> for RunDir {
    fn as_ref(&self) -> &Path {
        &self.path
    }
}

#[cfg(test)]
mod tests {
    use super::{RunDir, RunDirError};
    use std::path::PathBuf;

    #[test]
    fn test_initialize() {
        // Create dir for the first time.
        let result = RunDir::new("tests/rundir").initialize();

        assert!(result.is_ok());

        // Use existing empty dir.
        let result = RunDir::new("tests/rundir").initialize();

        assert!(result.is_ok());

        // Fail when dir is not empty and allow_cleaning is not set.
        std::fs::write("tests/rundir/hello.world", "test").unwrap();
        let result = RunDir::new("tests/rundir").initialize();

        match result {
            Err(RunDirError::DirIsNotEmpty { path, child_path }) => {
                assert_eq!(path, PathBuf::from("tests/rundir"));
                assert_eq!(child_path, PathBuf::from("tests/rundir/hello.world"));
            }
            _ => panic!("Expected an error."),
        }

        // Clean existing dir.
        let result = RunDir::new("tests/rundir")
            .allow_cleaning(true)
            .initialize();

        assert!(result.is_ok());

        std::fs::remove_dir("tests/rundir").unwrap();

        // Fail when dir is not a directory.
        std::fs::write("tests/rundir", "hello").unwrap();
        let result = RunDir::new("tests/rundir").initialize();

        match result {
            Err(RunDirError::PathIsNotDir { path }) => {
                assert_eq!(path, PathBuf::from("tests/rundir"));
            }
            _ => panic!("Expected an error."),
        }

        std::fs::remove_file("tests/rundir").unwrap();
    }

    #[test]
    fn test_subdirs() {
        let rundir = RunDir::new("tests/rundir2").allow_cleaning(true);

        rundir.initialize().unwrap();

        assert!(!PathBuf::from("tests/rundir2/subtest").exists());
        assert!(!rundir.subdir_exists("subtest").unwrap());

        rundir.create_subdir("subtest").unwrap();
        assert!(PathBuf::from("tests/rundir2/subtest").exists());
        assert!(rundir.subdir_exists("subtest").unwrap());

        rundir.remove_subdir_all("subtest").unwrap();
        assert!(!PathBuf::from("tests/rundir2/subtest").exists());
        assert!(!rundir.subdir_exists("subtest").unwrap());

        rundir.cleanup().unwrap();
    }
}