lazin 1.0.4

Primarily a dotfiles manager
use std::{
    collections::BTreeSet,
    fmt::Display,
    path::{Path, PathBuf},
};

use lazin_error::{Context, LazinResult};
use lazin_pipeline::Bind;

use crate::{
    config::{self, Name},
    encryption_management::{EncryptionManager, GPG_EXTENSION, gitignore::Gitignore},
    error::LazinError,
    resolve::{self},
};

pub enum ValidationError {
    SourcePathDoesNotExist { module_name: Name, path: PathBuf },
}

impl Display for ValidationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ValidationError::SourcePathDoesNotExist { module_name, path } => {
                write!(
                    f,
                    "Module '{}' has a source path that could not be found: '{}'",
                    module_name,
                    path.display()
                )
            }
        }
    }
}

#[derive(Debug)]
pub struct Config {
    pub expanded_modules: BTreeSet<resolve::module::Module>,
    pub workspaces: BTreeSet<resolve::workspace::Workspace>,
    pub encryption_manager: EncryptionManager,
}

impl Config {
    pub fn parse(
        config: config::Config,
        encryption_manager: EncryptionManager,
        mut gitignore: Option<Gitignore>,
    ) -> LazinResult<Self> {
        let validated_modules =
            validate_module_sources(config.modules).context("Failed to validate module sources")?;

        let expanded_modules = validated_modules
            .into_iter()
            .map(resolve::module::Module::try_new)
            .collect::<LazinResult<BTreeSet<resolve::module::Module>>>()?;

        let workspaces = config
            .workspaces
            .into_iter()
            .map(|w| {
                let name: String = w.name.into();
                let modules = w
                    .modules
                    .into_iter()
                    .map(|module_name| {
                        let module = expanded_modules
                            .iter()
                            .find(|m| m.name == module_name.as_ref());
                        match module {
                            Some(m) => Ok(m.name.clone()),
                            None => Err(LazinError::ModuleNotFound(
                                name.clone().into(),
                                module_name.clone(),
                            ))
                            .context("Module not found"),
                        }
                    })
                    .collect::<LazinResult<BTreeSet<String>>>()?;
                Ok(resolve::workspace::Workspace { name, modules })
            })
            .collect::<LazinResult<BTreeSet<resolve::workspace::Workspace>>>()?;

        expanded_modules.iter().for_each(|m| {
            m.values.iter().for_each(|v| match &v.encryption {
                resolve::module::Encryption::Disabled => {}
                resolve::module::Encryption::Enabled { .. } => {
                    gitignore
                        .as_mut()
                        .map(|gitignore| gitignore.managed.insert(v.source.clone()));
                }
            })
        });

        gitignore.map(|gitignore| gitignore.save()).transpose()?;

        Ok(Self {
            expanded_modules,
            workspaces,
            encryption_manager,
        })
    }

    pub fn contains_workspace(&self, name: &str) -> bool {
        self.workspaces.iter().any(|w| w.name == name)
    }

    pub fn get_workspace_modules(&self, name: &str) -> Vec<&resolve::module::Module> {
        let workspace = match self.workspaces.iter().find(|w| w.name == name) {
            Some(w) => w,
            None => return Vec::new(),
        };

        workspace.modules.iter().fold(
            Vec::new(),
            |mut acc: Vec<&resolve::module::Module>, module_name| {
                if let Some(m) = self
                    .expanded_modules
                    .iter()
                    .find(|m| m.name == *module_name)
                {
                    acc.push(m)
                };

                acc
            },
        )
    }
}

fn validate_module_sources(
    modules: BTreeSet<config::module::Module>,
) -> LazinResult<BTreeSet<config::module::Module>> {
    modules.iter().try_for_each(|module| {
        match module
            .values
            .iter()
            .map(|(source_path, value)| match &value.config.encryption {
                config::module::Encryption::Disabled => {
                    unencrypted_source_path_validation_pipeline(module, source_path)
                }
                config::module::Encryption::Enabled { .. } => {
                    encrypted_source_path_validation_pipeline(module, source_path)
                }
            })
            .fold(true, |acc, result| match result {
                Ok(_) => acc,
                Err(e) => {
                    lazin_logger::error!(e);
                    false
                }
            }) {
            true => Ok(()),
            false => Err(LazinError::InvalidModuleSources),
        }
    })?;

    Ok(modules)
}

fn unencrypted_source_path_validation_pipeline(
    module: &config::module::Module,
    source_path: &config::module::SourcePath,
) -> Result<(), ValidationError> {
    lazin_pipeline::new(source_path)
        .bind(|path| {
            let path = Path::new(path.as_ref());
            match path.exists() {
                true => Ok(()),
                false => Err(ValidationError::SourcePathDoesNotExist {
                    module_name: module.name.clone(),
                    path: path.into(),
                }),
            }
        })
        .result()
}

fn encrypted_source_path_validation_pipeline(
    module: &config::module::Module,
    source_path: &config::module::SourcePath,
) -> Result<(), ValidationError> {
    let path = Path::new(source_path.as_ref());
    if path.is_dir() {
        lazin_pipeline::new(source_path).result()
    } else {
        lazin_pipeline::new(source_path)
            .bind(|path| {
                let path = Path::new(path.as_ref());
                let encrypted_file_path = path.with_added_extension(GPG_EXTENSION);
                match (path.exists(), encrypted_file_path.exists()) {
                    (true, true) | (true, false) | (false, true) => Ok(()),
                    (false, false) => Err(ValidationError::SourcePathDoesNotExist {
                        module_name: module.name.clone(),
                        path: path.into(),
                    }),
                }
            })
            .result()
    }
}