//! The unified configuration object representation.

use std::{collections::BTreeMap, fmt::Display};

use libc::geteuid;
use log::{debug, error, trace};
use serde::{Deserialize, Serialize};

use crate::{
    TechnologySettings,
    config::technology::TechnologySettingsDefaults,
    core::{Context, Os, Purpose},
    file::{ConfigLoader, LoadMode},
};

/// An amalgamation of [`Os`], [`Purpose`] and [`Context`].
#[derive(Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct OsPurposeContext {
    os: Os,

    /// The target purpose for this override.
    purpose: Purpose,

    /// The context for this override.
    context: Context,
}

impl OsPurposeContext {
    /// Creates a new [`OsPurposeContext`]
    pub fn new(os: Os, purpose: Purpose, context: Context) -> Self {
        Self {
            os,
            purpose,
            context,
        }
    }
}

impl Display for OsPurposeContext {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}/{}/{}", self.os, self.purpose, self.context)
    }
}

/// The unified configuration view for a system.
///
/// Contains the fully resolved OS-level and context-level technology settings.
#[derive(Debug)]
pub struct VoaConfig {
    /// The built-in default settings for all supported backend technologies.
    defaults: TechnologySettings,

    /// Per [`Os`] settings for all supported backend technologies.
    oses: BTreeMap<Os, TechnologySettings>,

    /// Per [`Os`]/[`Purpose`]/[`Context`] settings for all supported backend technologies.
    contexts: BTreeMap<OsPurposeContext, TechnologySettings>,
}

impl VoaConfig {
    /// Creates a new [`VoaConfig`] by loading all available configuration files from system-wide
    /// and possible user-specific locations.
    ///
    /// # Note
    ///
    /// Depending on the effective uid of the calling user, the configurations are loaded only from
    /// system mode locations (i.e. uid < 1000), or from system mode and user mode locations (i.e.
    /// uid >= 1000).
    pub fn load() -> Self {
        let euid = unsafe { geteuid() };
        trace!("Config::load called with process user id {euid}");

        let loader = if euid < 1000 {
            debug!("⤷ Using system mode configuration file locations");
            ConfigLoader::load(LoadMode::System)
        } else {
            debug!("⤷ Using user mode configuration file locations");
            ConfigLoader::load(LoadMode::User)
        };

        Self::from(loader)
    }

    /// Returns the technology settings for an OS, or the built-in default.
    pub fn settings_for_os_or_default(&self, os: &Os) -> &TechnologySettings {
        self.oses.get(os).unwrap_or(&self.defaults)
    }

    /// Returns the technology settings for a specific context, an OS or the built-in default.
    pub fn settings_for_context_or_default(
        &self,
        os: &Os,
        purpose: &Purpose,
        context: &Context,
    ) -> &TechnologySettings {
        self.contexts
            .get(&OsPurposeContext::new(
                os.clone(),
                purpose.clone(),
                context.clone(),
            ))
            .unwrap_or(self.oses.get(os).unwrap_or(&self.defaults))
    }

    /// Returns a reference to the map of [`Os`] and [`TechnologySettings`].
    pub fn oses(&self) -> &BTreeMap<Os, TechnologySettings> {
        &self.oses
    }

    /// Returns a reference to the map of [`OsPurposeContext`] and [`TechnologySettings`].
    pub fn contexts(&self) -> &BTreeMap<OsPurposeContext, TechnologySettings> {
        &self.contexts
    }
}

impl From<ConfigLoader> for VoaConfig {
    fn from(value: ConfigLoader) -> Self {
        let defaults = TechnologySettings::default();
        let mut oses: BTreeMap<Os, TechnologySettings> = BTreeMap::new();
        let mut contexts: BTreeMap<OsPurposeContext, TechnologySettings> = BTreeMap::new();

        // Extract all OS default technology settings from default configuration files.
        for (os, config_file) in value.defaults() {
            if let Some(config_technology_settings) = config_file.default_technology_settings() {
                match TechnologySettings::from_config_with_defaults(
                    config_technology_settings,
                    TechnologySettingsDefaults {
                        built_in_defaults: &defaults,
                        os_defaults: None,
                    },
                ) {
                    Ok(technology_settings) => {
                        oses.insert(os.clone(), technology_settings);
                    }
                    Err(error) => {
                        error!(
                            "Unable to create default technology settings for OS {os} due to error: {error}"
                        );
                    }
                }
            }
        }
        // Extract all OS default technology settings from drop-in configuration files.
        //
        // Here, any OS default settings found in drop-ins will have a higher priority and will
        // override previously found ones.
        // However, for populating unset fields in these drop-ins, previously found OS default
        // technology settings will be considered.
        for (os, config_files) in value.drop_ins() {
            for config_file in config_files.iter() {
                if let Some(config_technology_settings) = config_file.default_technology_settings()
                {
                    match TechnologySettings::from_config_with_defaults(
                        config_technology_settings,
                        TechnologySettingsDefaults {
                            built_in_defaults: &defaults,
                            os_defaults: oses.get(os),
                        },
                    ) {
                        Ok(technology_settings) => {
                            oses.insert(os.clone(), technology_settings);
                        }
                        Err(error) => {
                            error!(
                                "Unable to create default technology settings for OS {os} due to error: {error}"
                            );
                        }
                    }
                }
            }
        }

        // Extract all context-level technology settings from default configuration files.
        for (os, config_file) in value.defaults() {
            // NOTE: Each ConfigOsSettings can only contain a list of unique context overrides.
            for context_settings in config_file.contexts().iter() {
                let os_purpose_context = OsPurposeContext::new(
                    os.clone(),
                    context_settings.purpose().clone(),
                    context_settings.context().clone(),
                );

                let technology_settings = match TechnologySettings::from_config_with_defaults(
                    context_settings.config_technology_settings(),
                    TechnologySettingsDefaults {
                        built_in_defaults: &defaults,
                        os_defaults: oses.get(os),
                    },
                ) {
                    Ok(settings) => settings,
                    Err(error) => {
                        error!(
                            "Unable to create technology settings for OS/purpose/context {os_purpose_context} due to an error: {error}"
                        );
                        continue;
                    }
                };

                contexts.insert(os_purpose_context, technology_settings);
            }
        }

        // Extract all context-level technology settings from default configuration files.
        //
        // Context-level settings found in drop-in configuration files have a higher
        // priority than those found in default configuration files or previously found drop-in
        // configuration files (with lower priority).
        // Context-level settings of lower priority are overridden.
        for (os, config_files) in value.drop_ins() {
            for config_file in config_files.iter() {
                // NOTE: Each ConfigOsSettings can only contain a list of unique context overrides.
                for context_settings in config_file.contexts().iter() {
                    let os_purpose_context = OsPurposeContext::new(
                        os.clone(),
                        context_settings.purpose().clone(),
                        context_settings.context().clone(),
                    );

                    let technology_settings = match TechnologySettings::from_config_with_defaults(
                        context_settings.config_technology_settings(),
                        TechnologySettingsDefaults {
                            built_in_defaults: &defaults,
                            os_defaults: oses.get(os),
                        },
                    ) {
                        Ok(technology_settings) => technology_settings,
                        Err(error) => {
                            error!(
                                "Unable to create technology settings for OS/purpose/context {os_purpose_context} due to an error: {error}"
                            );
                            continue;
                        }
                    };

                    contexts.insert(os_purpose_context, technology_settings);
                }
            }
        }

        Self {
            defaults,
            oses,
            contexts,
        }
    }
}
