use super::ConfigSource;
use crate::config::errors::ConfigError;
use serde::{de::DeserializeOwned, Serialize};
use std::{
    fmt::Debug,
    io::{BufWriter, Write},
    marker::PhantomData,
    path::PathBuf,
};
use tracing::{debug, trace};

/// Specifies a configuration source that reads from a file and deserializes it into a type, `T`.
///
/// # Optional files
///
/// Normally, if the file is not found, the source will return an error.
/// You can mark the source as optional to ignore the error.
///
/// ```
/// # use cipherstash_client::config::{
/// #     CipherStashConfigFile,
/// #     FileSource,
/// # };
/// let source = FileSource::<CipherStashConfigFile>::default().optional();
/// ```
///
/// Alternatively, you can specify that a file is required using the `REQUIRED` type parameter:
///
/// ```
/// # use cipherstash_client::config::{
/// #     CipherStashConfigFile,
/// #     FileSource,
/// # };
/// let source = FileSource::<CipherStashConfigFile, true>::default();
/// ```
///
/// # Specifying a path
///
/// By default, the file is loaded from the current working directory.
/// You can specify the directory path with the `with_path` method.
///
/// ```
/// # use cipherstash_client::config::{
/// #     CipherStashConfigFile,
/// #     FileSource,
/// # };
/// // Load the file from `some/place/cipherstash.toml`
/// // Ignore if the file is not found
/// let source = FileSource::<CipherStashConfigFile>::default()
///     .with_path("some/place")
///     .optional();
/// ```
///
/// ## Specifying a filename
///
/// You can also specify the filename to use with the `with_filename` method.
///
/// ```
/// # use cipherstash_client::config::{
/// #     CipherStashConfigFile,
/// #     FileSource,
/// # };
/// // Load the file from `some/place/myconfig.toml`
/// let source = FileSource::<CipherStashConfigFile>::default()
///    .with_filename("myconfig.toml");
/// ```
///
/// ## Built in file sources
///
/// The following built-in file sources are available:
///
/// * [CipherStashConfigFile]: Loads the `cipherstash.toml` file.
/// * [CipherStashSecretConfigFile]: Loads the `cipherstash_secret.toml` file.
/// * [UserConfigSource]: Loads the `config.toml` file normally found in `~/.cipherstash/`.
///
/// See [CipherStashConfigFile] and [CipherStashSecretConfigFile] for details on the configuration files.
///
/// ## Custom file sources
///
/// To create a custom file source, implement the [IntoFileSource] trait.
///
/// ```
/// use cipherstash_client::config::{IntoFileSource, FileSource};
/// use std::path::PathBuf;
/// use cts_common::Crn;
/// use serde::{Deserialize, Serialize};
///
/// #[derive(Debug, Serialize, Deserialize)]
/// pub struct MyConfig {
///     workspace_crn: Crn,
/// }
///
/// impl IntoFileSource for MyConfig {
///     fn default_path() -> PathBuf {
///         PathBuf::from("~/.cipherstash/")
///     }
///
///     fn filename() -> String {
///         "myconfig.toml".to_string()
///     }
/// }
/// ```
///
#[derive(Debug)]
pub struct FileSource<T, const REQUIRED: bool = true>(
    PhantomData<T>,
    Option<PathBuf>,
    Option<String>,
);

impl<T, const REQUIRED: bool> Default for FileSource<T, REQUIRED> {
    fn default() -> Self {
        FileSource(PhantomData, None, None)
    }
}

impl<T, const REQUIRED: bool> FileSource<T, REQUIRED>
where
    T: IntoFileSource,
{
    fn get_filepath(&self) -> PathBuf {
        let default_path = T::default_path();
        self.1
            .as_deref()
            .unwrap_or(default_path.as_path())
            .to_owned()
            .join(self.get_filename())
    }

    fn get_filename(&self) -> String {
        self.2.clone().unwrap_or_else(|| T::filename())
    }

    pub fn save(&self, value: T) -> Result<(), ConfigError>
    where
        T: Serialize,
    {
        let path = self.get_filepath();

        // TODO: Write the meta header
        debug!(file = %path.display(), kind = std::any::type_name::<T>(), "Saving config");

        let mut writer = std::fs::File::create(&path)
            .inspect_err(|e| debug!(error = %e, "Failed to create file"))
            .map_err(ConfigError::invalid)
            .map(BufWriter::new)?;

        let toml = toml::to_string(&value).map_err(ConfigError::invalid)?;
        writer
            .write_all(toml.as_bytes())
            .and_then(|_| writer.flush())
            .map_err(|e| ConfigError::io(&e, path))
    }

    /// Specify the path of the file to load instead of the one returned via [IntoFileSource::default_path].
    ///
    /// # Example
    ///
    /// ```
    /// use cipherstash_client::config::{FileSource, UserConfigSource};
    ///
    /// FileSource::<UserConfigSource>::default()
    ///     .with_path("some_dir/myconfig.toml");
    /// ```
    ///
    pub fn with_path(mut self, path: impl Into<PathBuf>) -> Self {
        self.1 = Some(path.into());
        self
    }

    /// Like [Self::with_path], but allows the path to be optional.
    pub fn with_path_opt(mut self, path: Option<impl Into<PathBuf>>) -> Self {
        self.1 = path.map(|p| p.into());
        self
    }

    pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
        self.2 = Some(filename.into());
        self
    }
}

impl<T> FileSource<T, true> {
    pub fn load(&self) -> Result<T, ConfigError>
    where
        T: IntoFileSource + DeserializeOwned,
    {
        // TODO: Most of this code is duplicated
        let path = self.get_filepath();
        debug!(file = %path.display(), "Loading config");

        match std::fs::read_to_string(&path) {
            Ok(config) => {
                debug!(config = %config, "Loaded config");
                toml::from_str(&config).map_err(|e| ConfigError::InvalidConfigError(e.to_string()))
            }
            Err(e) => {
                debug!(error = %e, "Failed to read file");
                if matches!(e.kind(), std::io::ErrorKind::NotFound) {
                    Err(ConfigError::NotFound(format!(
                        "`{}`: {}",
                        path.to_string_lossy(),
                        e
                    )))
                } else {
                    Err(ConfigError::io(&e, path))
                }
            }
        }
    }

    /// Converts the source to an optional source.
    pub fn optional(self) -> FileSource<T, false> {
        FileSource::<T, false>(self.0, self.1, self.2)
    }
}

impl<T> FileSource<T, false> {
    pub fn load_opt(&self) -> Result<Option<T>, ConfigError>
    where
        T: IntoFileSource + DeserializeOwned,
    {
        let path = self.get_filepath();

        debug!(file = %path.display(), "Loading config");
        match std::fs::read_to_string(&path) {
            Ok(config) => {
                trace!(config = %config, "Loaded config");
                toml::from_str(&config)
                    .map_err(|e| ConfigError::InvalidConfigError(e.to_string()))
                    .map(Some)
            }
            Err(e) => {
                debug!(error = %e, "Failed to read file but ignoring because file source is optional");
                Ok(None)
            }
        }
    }

    /// Converts the source to a required source.
    pub fn required(self) -> FileSource<T, true> {
        FileSource::<T, true>(self.0, self.1, self.2)
    }
}

impl<B, T> ConfigSource<B> for FileSource<T, true>
where
    T: ConfigSource<B> + IntoFileSource + DeserializeOwned,
{
    fn add_to_builder(&self, builder: B) -> Result<B, ConfigError> {
        self.load()
            .and_then(|config| config.add_to_builder(builder))
    }
}

impl<B, T> ConfigSource<B> for FileSource<T, false>
where
    T: ConfigSource<B> + IntoFileSource + DeserializeOwned,
{
    fn add_to_builder(&self, builder: B) -> Result<B, ConfigError> {
        if let Some(config) = self.load_opt()? {
            config.add_to_builder(builder)
        } else {
            Ok(builder)
        }
    }
}

/// Any config source that can be serialized and deserialized to/from a file.
pub trait IntoFileSource: Debug {
    fn default_path() -> PathBuf;
    fn filename() -> String;

    fn meta(&self) -> Option<&str> {
        None
    }
}
