//! `config_traits` is a crate that broke out from the requirement to manage
//! various different config files, including parsing from different formats and
//! updating them from previous versions where fields or names are changed in
//! some way.
//!
//! The end canonical file format is `.ron` as this supports rust types well

use std::fs::{self, create_dir, File, OpenOptions};
use std::io::{Read, Write};
use std::path::PathBuf;

use log::{error, warn};
pub use ron;
use ron::ser::PrettyConfig;
use serde::de::DeserializeOwned;
use serde::Serialize;

/// Config file helper traits. Only `new()` and `file_name()` are required to be
/// implemented, the rest are intended to be free methods.
pub trait StdConfig
where
    Self: Serialize + DeserializeOwned,
{
    /// Taking over the standard `new()` to ensure things can be generic
    fn new() -> Self;

    /// Return the config files names, such as `wibble.cfg`
    fn file_name(&self) -> String;

    /// Return the full path to the directory the config file resides in
    fn config_dir() -> PathBuf;

    /// Return the full path to the config file
    fn file_path(&self) -> PathBuf {
        let mut config = Self::config_dir();
        if !config.exists() {
            create_dir(config.as_path())
                .unwrap_or_else(|e| panic!("Could not create {:?} {e}", Self::config_dir()));
        }
        config.push(self.file_name());
        let mut do_rename = !config.exists();
        let mut cfg_old = config.clone();
        // Migrating all configs to .ron format, so we do need to check for older ones
        if do_rename {
            warn!("Config {cfg_old:?} does not exist, looking for .cfg next");
            cfg_old.pop();
            let tmp = self.file_name();
            let parts: Vec<_> = tmp.split('.').collect();
            cfg_old.push(format!("{}.cfg", parts[0]));
        }
        if do_rename && cfg_old.exists() {
            // Now we gotta rename it
            warn!("Renaming {cfg_old:?} to {config:?}");
            std::fs::rename(&cfg_old, &config).unwrap_or_else(|err| {
                error!(
                    "Could not rename. Please remove {} then restart service: Error {}",
                    self.file_name(),
                    err
                );
            });
            do_rename = false;
        }
        if do_rename && !cfg_old.exists() {
            warn!("Config {cfg_old:?} does not exist, looking for .conf next");
            cfg_old.pop();
            let tmp = self.file_name();
            let parts: Vec<_> = tmp.split('.').collect();
            cfg_old.push(format!("{}.conf", parts[0]));
        }
        if do_rename && cfg_old.exists() {
            // Now we gotta rename it
            warn!("Renaming {cfg_old:?} to {config:?}");
            std::fs::rename(&cfg_old, &config).unwrap_or_else(|err| {
                error!(
                    "Could not rename. Please remove {} then restart service: Error {}",
                    self.file_name(),
                    err
                );
            });
        }
        config
    }

    /// Directly open the config file for read and write. If the config file
    /// does not exist it is created, including the directories the file
    /// resides in.
    fn file_open(&self) -> File {
        OpenOptions::new()
            .read(true)
            .write(true)
            .create(true)
            .truncate(false)
            .open(self.file_path())
            .unwrap_or_else(|e| panic!("Could not open {:?} {e}", self.file_path()))
    }

    /// Open and parse the config file to self from ron format
    fn read(&mut self) {
        if let Ok(data) = fs::read_to_string(self.file_path()) {
            if data.is_empty() {
                warn!("File is empty {:?}", self.file_path());
            } else if let Ok(data) = ron::from_str(&data) {
                *self = data;
            } else {
                warn!("Could not deserialise {:?}", self.file_path());
            }
        }
    }

    /// Open and parse the config file to self from ron format
    fn read_new(&self) -> Option<Self> {
        if let Ok(data) = fs::read_to_string(self.file_path()) {
            if data.is_empty() {
                warn!("File is empty {:?}", self.file_path());
            } else if let Ok(data) = ron::from_str(&data) {
                return Some(data);
            } else {
                warn!("Could not deserialise {:?}", self.file_path());
            }
        }
        None
    }

    /// Write the config file data to pretty ron format
    fn write(&self) {
        let mut file = match File::create(self.file_path()) {
            Ok(data) => data,
            Err(e) => {
                error!(
                    "Couldn't overwrite config {:?}, error: {e}",
                    self.file_path()
                );
                return;
            }
        };
        let ron = match ron::ser::to_string_pretty(&self, PrettyConfig::new().depth_limit(4)) {
            Ok(data) => data,
            Err(e) => {
                error!("Parse {:?} to RON failed, error: {e}", self.file_path());
                return;
            }
        };
        file.write_all(ron.as_bytes())
            .unwrap_or_else(|err| error!("Could not write config: {}", err));
    }

    /// Renames the existing file to `<file>-old`
    fn rename_file_old(&self) {
        warn!("Renaming {} to {}-old", self.file_name(), self.file_name());
        let mut cfg_old = self.file_path().to_string_lossy().to_string();
        cfg_old.push_str("-old");
        std::fs::rename(self.file_path(), cfg_old).unwrap_or_else(|err| {
            error!(
                "Could not rename. Please remove {} then restart service: Error {}",
                self.file_name(),
                err
            );
        });
    }
}

#[macro_export]
macro_rules! std_config_load {
    ($trait_name:ident: $($generic:ident),*) => {
        /// Base trait for loading/parsing. This is intended to be used to help update
        /// configs to new versions
        ///
        /// # Example
        /// ```rust
        /// use std::path::PathBuf;
        /// use serde::{Deserialize, Serialize};
        /// use config_traits::{StdConfig, StdConfigLoad2};
        ///
        /// #[derive(Deserialize, Serialize, Debug)]
        /// struct FanCurveConfigOld {}
        ///
        /// #[derive(Deserialize, Serialize, Debug)]
        /// struct FanCurveConfigOlder {}
        ///
        /// #[derive(Deserialize, Serialize, Debug)]
        /// struct FanCurveConfig {}
        ///
        /// impl From<FanCurveConfigOld> for FanCurveConfig {
        ///     fn from(_: FanCurveConfigOld) -> Self { Self {} }
        /// }
        ///
        /// impl From<FanCurveConfigOlder> for FanCurveConfig {
        ///     fn from(_: FanCurveConfigOlder) -> Self { Self {} }
        /// }
        ///
        /// impl StdConfig for FanCurveConfig {
        ///     fn new() -> Self { Self {} }
        ///
        ///     fn file_name(&self) -> std::string::String { "test_name.conf".to_owned() }
        ///
        ///     fn config_dir() -> PathBuf { PathBuf::from("/tmp") }
        /// }
        ///
        /// impl StdConfigLoad2<FanCurveConfigOld, FanCurveConfigOlder> for FanCurveConfig {}
        /// ```
        ///
        /// If all of the generics fails to parse, then the old config is renamed and a
        /// new one created
        pub trait $trait_name<$($generic),*>
        where
            Self: $crate::StdConfig +std::fmt::Debug + DeserializeOwned + Serialize,
            $($generic: DeserializeOwned + Into<Self>),*
        {
            fn load(mut self) -> Self {
                let mut file = self.file_open();
                let mut buf = String::new();
                if let Ok(read_len) = file.read_to_string(&mut buf) {
                    if read_len != 0 {
                        if let Ok(data) = ron::from_str(&buf) {
                            self = data;
                            log::info!("Parsed RON for {:?}", std::any::type_name::<Self>());
                        }  $(else if let Ok(data) = ron::from_str::<$generic>(&buf) {
                            self = data.into();
                            log::info!("New version failed, trying previous: Parsed RON for {:?}", std::any::type_name::<$generic>());
                        })* else {
                            self.rename_file_old();
                            self = Self::new();
                        }
                    } else {
                        error!("Config file {} zero read length", self.file_name());
                    }
                }
                self.write();
                self
            }
        }
    };
}

std_config_load!(StdConfigLoad:);
std_config_load!(StdConfigLoad1: T1);
std_config_load!(StdConfigLoad2: T1, T2);
std_config_load!(StdConfigLoad3: T1, T2, T3);
std_config_load!(StdConfigLoad4: T1, T2, T3, T4);

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    #[test]
    fn check_macro_from_1() {
        #[derive(serde::Deserialize, serde::Serialize, Debug)]
        struct Test {}

        #[derive(serde::Deserialize, serde::Serialize, Debug)]
        struct Old1 {}

        impl crate::StdConfig for Test {
            fn new() -> Self {
                Self {}
            }

            fn file_name(&self) -> String {
                String::new()
            }

            fn config_dir() -> PathBuf {
                PathBuf::new()
            }
        }

        impl From<Old1> for Test {
            fn from(_: Old1) -> Self {
                Self {}
            }
        }

        let _ = Test {};

        impl crate::StdConfigLoad1<Old1> for Test {}
    }

    #[test]
    fn check_macro_from_3() {
        #[derive(serde::Deserialize, serde::Serialize, Debug)]
        struct Test {}

        #[derive(serde::Deserialize, serde::Serialize, Debug)]
        struct Old1 {}

        #[derive(serde::Deserialize, serde::Serialize, Debug)]
        struct Old2 {}

        #[derive(serde::Deserialize, serde::Serialize, Debug)]
        struct Old3 {}

        impl crate::StdConfig for Test {
            fn new() -> Self {
                Self {}
            }

            fn file_name(&self) -> String {
                String::new()
            }

            fn config_dir() -> PathBuf {
                PathBuf::new()
            }
        }

        impl From<Old1> for Test {
            fn from(_: Old1) -> Self {
                Self {}
            }
        }

        impl From<Old2> for Test {
            fn from(_: Old2) -> Self {
                Self {}
            }
        }

        impl From<Old3> for Test {
            fn from(_: Old3) -> Self {
                Self {}
            }
        }

        let _ = Test {};

        impl crate::StdConfigLoad3<Old1, Old2, Old3> for Test {}
    }
}
