// SPDX-FileCopyrightText: 2025 Simon Brummer
//
// SPDX-License-Identifier: MPL-2.0

//! # (System[d)aemon]
//!
//! (System[d)aemon] provides building blocks for composing daemonized applications.
//! A daemon is a long-running background process that typically runs without direct user interaction.
//!
//! **This crate is designed for async.** If you need a synchronous solution, this is not the right fit,
//! though the source code may serve as inspiration.
//!
//! Daemons are typically tightly coupled to the underlying operating system. This crate
//! provides thin abstractions for the following tasks:
//!
//! - Logging/Tracing setup with syslog integration
//! - Configuration management (with feature-gated file formats or custom formats)
//! - Graceful shutdown
//! - Daemon reloading
//! - Watchdog interval querying and watchdog triggering
//! - Daemon lifecycle notifications
//! - Behavioral policies on errors (daemon logic returns an error, config failed to load, etc.)
//! - Customizable system integrations
//!
//! ## Design
//!
//! This crate is OS-agnostic in the sense that all system-interfacing functionality is abstracted into a few traits.
//! Types implementing these traits are called integrations, and integrations offered by this crate are feature-gated.
//! Users can implement custom integrations if daemons require specialized behavior.
//!
//! This crate relies heavily on async. It offers a runtime-agnostic variant as well as a Tokio variant, both feature-gated.
//! Due to how features work, both flavors are not mutually exclusive, but Tokio types take precedence over the runtime-agnostic
//! types in case both are defined.
//!
//! As of now, the only supported integration is for systemd since it is the de-facto standard on most linux systems nowadays.
//!
//! ## Features
//!
//! This crate relies heavily on conditional compilation depending on the users needs. Users must
//! select the features they actually need, therefore this crates default feature does not enable
//! anything.
//!
//! The following features are available:
//!
//! - systemd: Enables "systemd" integration on unix platforms.
//! - toml: Enables "toml" as daemon configuration file format.
//! - yaml: Enables "yaml" as daemon configuration file format.
//! - json: Enables "json" as daemon configuration file format.
//! - agnostic: Uses runtime agnostic async crates.
//! - tokio: Uses tokio async crates. Takes precedence over agnostic
//!
//! ## LLM usage disclaimer
//!
//! LLM usage is a controversial topic in the open source scene (for many good reasons). Neither the implementation nor tests were touched by an LLM because
//! I as an author have the responsibility to understand how my published code works. The easiest way to ensure this is by writing it by hand.
//!
//! Documentation however was streamlined using Claude Sonnet 4.6 via GitHub Copilot since I have a mild form of dyslexia and
//! LLMs can be quite handy when it comes to improving/correcting plain text.
//!
//! ## Additional Notes
//!
//! This crate started as a simple wrapper around systemd. That is the reason behind the funky name of this project.
//! Outside of the documentation its simply written as `systemdaemon`.
//!
//! If you want to see more integrations for other init systems and/or operating systems, feel free to open a PR.
//! Be aware that if such a PR gets merged, you are becoming effectively a maintainer of this crate and you are responsible for the PRs contents.

#![deny(missing_docs)]

// Ensure that at least one mandatory feature flag is set.
#[cfg(not(any(feature = "agnostic", feature = "tokio")))]
compile_error!("Neither feature 'agnostic' nor 'tokio' are enabled. Check this crates features.");

#[cfg(all(feature = "systemd", target_os = "linux"))]
use integration::systemd::SystemdIntegration as DefaultIntegration;

use serde::Serialize;
use serde::de::DeserializeOwned;
use std::error::Error;
use std::fmt::Debug;
use std::fs::{File, canonicalize};
use std::future::{Future, pending};
use std::io::{ErrorKind, Read, Write};
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::time::Duration;
use tracing::{Level, debug, error, info, instrument, warn};

use crate::error::{LoadError, SaveError};

mod config_format;
mod error;
mod integration;
mod lib_agnostic_tests;
mod lib_tests;
mod lib_tokio_tests;

#[cfg(all(not(feature = "tokio"), feature = "agnostic"))]
use async_watch::{Receiver, Sender, channel};

#[cfg(feature = "tokio")]
use tokio::sync::watch::{Receiver, Sender, channel};

/// Configures logging/tracing facilities of a system integration.
#[cfg_attr(test, mockall::automock)]
pub trait Logger {
    /// Initializes logging with the specified maximum severity level.
    ///
    /// # Arguments
    /// * `level` - Messages above this severity level are filtered out.
    fn setup_logger(&self, level: Level);
}

/// Queries and triggers watchdog.
#[cfg_attr(test, mockall::automock)]
pub trait Watchdog {
    /// Returns the watchdog trigger interval if the watchdog is enabled.
    ///
    /// # Returns
    /// * `Some(Duration)` - The watchdog is enabled and must be notified within this interval.
    /// * `None` - The watchdog is disabled.
    fn notify_interval(&self) -> Option<Duration>;

    /// Notifies the watchdog that the service is still alive.
    fn notify(&self);
}

/// Waits for a global shutdown signal.
#[cfg_attr(test, mockall::automock)]
pub trait Shutdown {
    /// Returns a future that completes when a shutdown is requested.
    fn wait_for_shutdown(&self) -> impl Future<Output = ()>;
}

/// Waits for a global reload signal.
#[cfg_attr(test, mockall::automock)]
pub trait Reload {
    /// Returns a future that completes when a reload is requested.
    fn wait_for_reload(&self) -> impl Future<Output = ()>;
}

/// Sends daemon lifecycle notifications to the operating system.
#[cfg_attr(test, mockall::automock)]
pub trait Notify {
    /// Notifies the system that the daemon is fully initialized and ready.
    fn notify_ready(&self);

    /// Notifies the system that the daemon is reloading its configuration.
    fn notify_reloading(&self);

    /// Notifies the system that the daemon is shutting down.
    fn notify_stopping(&self);
}

/// Defines configuration file formats.
///
/// Built-in implementations are provided for JSON, TOML, and YAML via feature flags.
/// Implement this trait directly to support additional formats.
#[cfg_attr(test, mockall::automock(type Data=String;))]
pub trait ConfigFormat {
    /// The concrete data type stored in the configuration.
    type Data: Serialize + DeserializeOwned;

    /// Tries to save configuration data to the given path.
    ///
    /// # Arguments
    /// * `path` - The file path to save the `data` to
    /// * `data` - The configuration data to save
    ///
    /// # Returns
    /// * `Ok(())` - Data was stored successfully
    /// * `Err(SaveError)` - Failed to save the data
    #[instrument(skip_all)]
    fn try_save(&self, path: &Path, data: &Self::Data) -> Result<(), SaveError> {
        debug!("Canonicalize path '{}'.", path.display());
        let path = match canonicalize(path) {
            Ok(path) => {
                debug!("Canonicalized path is '{}'", path.display());
                path
            }
            Err(err) => {
                if err.kind() == ErrorKind::NotFound {
                    debug!(
                        "Failed to canonicalize path '{}', it does not exist (yet)",
                        path.display()
                    );
                    path.to_path_buf()
                } else {
                    return Err(err.into());
                }
            }
        };

        let data = self.serialize(data)?;
        Ok(File::create(&path)?
            .write_all(data.as_bytes())
            .inspect(|()| debug!("Stored config successfully under '{}'", path.display()))?)
    }

    /// Tries to load configuration data from the given path.
    ///
    /// # Arguments
    /// * `path` - The file path to load data from
    ///
    /// # Returns
    /// * `Ok(Data)` - Data was loaded successfully
    /// * `Err(LoadError)` - Failed to load data
    #[instrument(skip_all)]
    fn try_load(&self, path: &Path) -> Result<Self::Data, LoadError> {
        debug!("Canonicalize path '{}'.", path.display());
        let path = canonicalize(path)?;

        debug!("Canonicalized path is '{}'", path.display());
        let mut file = File::open(&path)?;
        let mut data = String::default();
        file.read_to_string(&mut data)?;

        self.deserialize(data.as_str()).inspect(|_| {
            debug!("Loaded data successfully from path '{}'", path.display());
        })
    }

    /// Serializes `data` into this formats string representation.
    ///
    /// # Arguments
    /// * `data` - The data to serialize
    ///
    /// # Returns
    /// * `Ok(String)` - Successfully serialized data
    /// * `Err(SaveError)` - Failed to serialize data
    fn serialize(&self, data: &Self::Data) -> Result<String, SaveError>;

    /// Deserializes a string into `Self::Data`.
    ///
    /// # Arguments
    /// * `data` - The string to deserialize
    ///
    /// # Returns
    /// * `Ok(Data)` - Successfully deserialized data
    /// * `Err(LoadError)` - Failed to deserialize data
    fn deserialize(&self, data: &str) -> Result<Self::Data, LoadError>;
}

/// A long-running async task managed by the daemon.
///
/// Implement this trait to define the daemon's application logic. The daemon object
/// manages execution, including restarts and configuration updates.
#[cfg_attr(test, mockall::automock(type Config=String; type Error=crate::error::mocks::MockError;))]
pub trait Service {
    /// The service's configuration data type.
    type Config: Default + Clone + Serialize + DeserializeOwned + Debug;

    /// The error type returned by the service.
    type Error: Error;

    /// The application logic's main entry point.
    ///
    /// Called by the daemon each time the service starts or restarts.
    /// Whether the service is restarted after returning depends on the configured [`RestartPolicy`].
    ///
    /// # Arguments
    /// * `config` - The current service configuration
    ///
    /// # Returns
    /// A future containing the service's core logic.
    ///
    /// # Additional Notes
    /// Avoid blocking calls with this function.
    fn run(&self, config: Self::Config) -> impl Future<Output = Result<(), Self::Error>>;
}

/// Controls when a service is restarted after its [`Service::run`] method returns.
#[derive(Debug, PartialEq, Default)]
pub enum RestartPolicy {
    /// Always restart the service regardless of the returned result.
    #[default]
    Always,
    /// Only restart the service if it returned an `Ok` result.
    OnSuccess,
    /// Only restart the service if it returned an `Err` result.
    OnError,
    /// Never restart the service.
    Never,
}

/// Controls daemon behavior when configuration reloading fails.
#[derive(Debug, PartialEq, Default)]
pub enum ReloadPolicy {
    /// Keep the last successfully loaded configuration.
    #[default]
    KeepOnError,
    /// Fall back to the default configuration.
    DefaultOnError,
    /// Initiate daemon shutdown instead of reloading.
    ShutdownOnError,
}

/// Daemon object encapsulating handler functions.
///
/// # Type Parameters
/// * `T` - The system integration type the daemon uses. Depending on which traits `T` implements,
///   the [`Daemon`] exposes additional functionality.
pub struct Daemon<T> {
    integration: T,
    #[cfg(all(not(feature = "tokio"), feature = "agnostic"))]
    shutdown: cancellation_token::CancellationTokenSource,
    #[cfg(feature = "tokio")]
    shutdown: tokio_util::sync::CancellationToken,
}

impl Default for Daemon<DefaultIntegration> {
    fn default() -> Self {
        Daemon::<DefaultIntegration>::new(DefaultIntegration)
    }
}

impl<T> Daemon<T> {
    /// Creates a daemon based on a system integration.
    ///
    /// Use this to provide a custom integration. The default constructor
    /// (via [`Default`]) uses the platform's built-in integration where available.
    ///
    /// # Arguments
    /// * `integration` - The integration the daemon shall use.
    pub fn new(integration: T) -> Self {
        Self {
            integration,
            #[cfg(all(not(feature = "tokio"), feature = "agnostic"))]
            shutdown: cancellation_token::CancellationTokenSource::new(),
            #[cfg(feature = "tokio")]
            shutdown: tokio_util::sync::CancellationToken::new(),
        }
    }
}

impl<T> Daemon<T>
where
    T: Logger,
{
    /// Initializes logging with the specified maximum severity level.
    ///
    /// # Arguments
    /// * `level` - Messages above this severity level are filtered out.
    pub fn setup_logger(&self, level: Level) {
        self.integration.setup_logger(level);
    }
}

impl<T> Daemon<T>
where
    T: Notify,
{
    /// Handles service logic execution, restarts, and teardown.
    ///
    /// This handler takes care of:
    /// * Starting a service
    /// * Restarting the service on terminating (normally or with error)
    /// * Restarting the service on configuration updates
    /// * Gracefully shutting down when requested
    ///
    /// # Arguments
    /// * `config` - Configuration for the service
    /// * `policy` - Policy specifying the restart behavior of the service.
    ///
    /// # Type Parameters
    /// * `Srv` - The service type to handle
    ///
    /// # Returns
    /// Nothing. This function returns either on its own depending on the given `policy`,
    /// or when the daemon's cancellation token is signaled.
    /// If this function returns on its own, the daemon's cancellation token is signaled, initiating
    /// a global shutdown.
    #[instrument(skip_all)]
    pub async fn service_handler<Srv>(
        &self,
        service: &Srv,
        config: &ServiceConfig<Srv::Config, impl ConfigFormat<Data = Srv::Config>>,
        policy: RestartPolicy,
    ) where
        Srv: Service,
    {
        let mut receiver = config.receiver().clone();

        // Note: Due to the different semantics of the async ecosystems
        // Flavor 'agnostic' and 'tokio' have two distinct implementations.
        // If you modify one, ensure to also modify the other along with the tests.
        #[cfg(all(not(feature = "tokio"), feature = "agnostic"))]
        let task = async {
            use futures::FutureExt;

            loop {
                let service_config = receiver.borrow().clone();
                self.integration.notify_ready();
                info!("Starting service...");

                futures::select! {
                    res = service.run(service_config).fuse() => {
                        match (&policy, res) {
                            (RestartPolicy::Always, Ok(())) | (RestartPolicy::OnSuccess, Ok(())) => {
                                info!("Service returned, restart due to policy '{policy:?}'.");
                            }
                            (RestartPolicy::Always, Err(err)) | (RestartPolicy::OnError, Err(err)) => {
                                warn!(
                                    "Service returned with error, restart due to policy '{policy:?}'. Error was: '{err}'"
                                );
                            }
                            (RestartPolicy::Never, Ok(())) | (RestartPolicy::OnError, Ok(())) => {
                                info!("Service returned, stop due to policy '{policy:?}'.");
                                break;
                            }
                            (RestartPolicy::Never, Err(err)) | (RestartPolicy::OnSuccess, Err(err)) => {
                                warn!(
                                    "Service returned with error, stop due to policy '{policy:?}'. Error was: '{err}'"
                                );
                                break;
                            }
                        }
                    }
                    res = receiver.changed().fuse() => {
                        res.expect("All senders are dropped. This is a bug.");
                        info!("Configuration changed. Restart service...");
                    }
                }
            }
        };

        #[cfg(feature = "tokio")]
        let task = async {
            loop {
                let service_config = receiver.borrow_and_update().clone();
                self.integration.notify_ready();
                info!("Starting service...");

                tokio::select! {
                    res = service.run(service_config) => {
                        match (&policy, res) {
                            (RestartPolicy::Always, Ok(())) | (RestartPolicy::OnSuccess, Ok(())) => {
                                info!("Service returned, restart due to policy '{policy:?}'.");
                            },
                            (RestartPolicy::Always, Err(err)) | (RestartPolicy::OnError, Err(err)) => {
                                warn!("Service returned with error, restart due to policy '{policy:?}'. Error was: '{err}'");
                            },
                            (RestartPolicy::Never, Ok(())) | (RestartPolicy::OnError, Ok(())) => {
                                info!("Service returned, stop due to policy '{policy:?}'.");
                                break;
                            },
                            (RestartPolicy::Never, Err(err)) | (RestartPolicy::OnSuccess, Err(err)) => {
                                warn!("Service returned with error, stop due to policy '{policy:?}'. Error was: '{err}'");
                                break;
                            },
                        }
                    },
                    res = receiver.changed() => {
                        res.expect("All senders are dropped. This is a bug.");
                        info!("Configuration changed. Restart service...");
                    }
                }
            }
        };

        #[cfg(all(not(feature = "tokio"), feature = "agnostic"))]
        let outcome = self.shutdown.token().run(task).await.ok();
        #[cfg(feature = "tokio")]
        let outcome = self.shutdown.run_until_cancelled(task).await;

        match outcome {
            Some(()) => {
                info!("Distribute shutdown event via cancellation token...");
                self.integration.notify_stopping();
                self.shutdown.cancel();
            }
            None => {
                info!("Task was canceled via token. Shutting down...");
            }
        }
    }
}

impl<T> Daemon<T>
where
    T: Reload + Notify,
{
    /// Handles service reloads.
    ///
    /// This handler takes care of:
    /// * Waiting for reload events
    /// * Reloading configuration when a reload is requested
    /// * Broadcasting updated configuration to all services consuming them
    ///
    /// # Arguments
    /// * `config` - Reference to the service configuration
    /// * `policy` - Policy specifying the behavior on failing configuration reloading
    ///
    /// # Type Parameters
    /// * `Data` - The configuration data type to reload
    ///
    /// # Returns
    /// Nothing. This function returns either on its own depending on the given `policy`,
    /// or when the daemon's cancellation token is signaled.
    /// If this function returns on its own, the daemon's cancellation token is signaled, initiating
    /// application shutdown.
    #[instrument(skip_all)]
    pub async fn reload_handler<Data, Fmt>(
        &self,
        config: &ServiceConfig<Data, Fmt>,
        policy: ReloadPolicy,
    ) where
        Data: Default + DeserializeOwned + Serialize + Debug + Clone,
        Fmt: ConfigFormat<Data = Data>,
    {
        let task = async {
            loop {
                info!("Waiting for reload event.");
                self.integration.wait_for_reload().await;

                info!(
                    "Received reload event, try to reload config from '{}'.",
                    config.file.display()
                );

                let data = match config.file_format.try_load(&config.file) {
                    Ok(data) => {
                        info!("Loaded config successfully.");
                        data
                    }
                    Err(err) => match policy {
                        ReloadPolicy::KeepOnError => {
                            warn!(
                                "Failed to load config, re-use current config due to policy '{policy:?}'. Error was: '{err}'"
                            );
                            config.get()
                        }
                        ReloadPolicy::DefaultOnError => {
                            warn!(
                                "Failed to load config, use default config due to policy '{policy:?}'. Error was: '{err}'"
                            );
                            Data::default()
                        }
                        ReloadPolicy::ShutdownOnError => {
                            warn!(
                                "Failed to load config, stop due to policy '{policy:?}'. Error was: '{err}'"
                            );
                            break;
                        }
                    },
                };

                info!("Send configuration '{data:?}' to service");
                self.integration.notify_reloading();
                config.config_sender.send(data).unwrap();
            }
        };

        #[cfg(all(not(feature = "tokio"), feature = "agnostic"))]
        let outcome = self.shutdown.token().run(task).await.ok();
        #[cfg(feature = "tokio")]
        let outcome = self.shutdown.run_until_cancelled(task).await;

        match outcome {
            Some(()) => {
                info!("Distribute shutdown event via cancellation token...");
                self.integration.notify_stopping();
                self.shutdown.cancel();
            }
            None => {
                info!("Task was canceled via token. Shutting down...");
            }
        }
    }
}

impl<T> Daemon<T>
where
    T: Watchdog,
{
    /// Periodically triggers the system watchdog at half the interval reported by the integration.
    ///
    /// If the watchdog is disabled, this future remains pending until the daemon's cancellation
    /// token is signaled. Returns when the cancellation token is signaled.
    #[instrument(skip_all)]
    pub async fn watchdog_handler(&self) {
        let task: Pin<Box<dyn Future<Output = ()>>> = match self.integration.notify_interval() {
            None => {
                info!("Watchdog disabled, do nothing.");
                Box::pin(pending())
            }

            Some(trigger_interval) => {
                let used_interval = (trigger_interval / 2) - Duration::from_micros(10);

                info!(
                    "Watchdog enabled, received interval is {trigger_interval:?}, used interval is {used_interval:?}."
                );
                let task = async move {
                    #[cfg(all(not(feature = "tokio"), feature = "agnostic"))]
                    let mut interval = async_timer::Interval::platform_new(used_interval);
                    #[cfg(feature = "tokio")]
                    let mut interval = tokio::time::interval(used_interval);

                    loop {
                        self.integration.notify();
                        debug!("Watchdog triggered.");
                        #[cfg(all(not(feature = "tokio"), feature = "agnostic"))]
                        interval.as_mut().await;
                        #[cfg(feature = "tokio")]
                        interval.tick().await;
                    }
                };
                Box::pin(task)
            }
        };

        #[cfg(all(not(feature = "tokio"), feature = "agnostic"))]
        let outcome = self.shutdown.token().run(task).await.ok();
        #[cfg(feature = "tokio")]
        let outcome = self.shutdown.run_until_cancelled(task).await;

        match outcome {
            Some(()) => {
                panic!("This task must never terminate on its own. This is a bug");
            }
            None => {
                info!("Task was canceled via token. Shutting down...");
            }
        }
    }
}

impl<T> Daemon<T>
where
    T: Shutdown + Notify,
{
    /// Waits for a shutdown signal from the integration, then signals the daemon cancellation token.
    ///
    /// Returns when a shutdown event is received or when the cancellation token is already signaled
    /// by another handler. If this function receives the shutdown event first, the cancellation
    /// token is signaled to initiate application-wide shutdown.
    #[instrument(skip_all)]
    pub async fn shutdown_handler(&self) {
        info!("Waiting for shutdown event.");

        #[cfg(all(not(feature = "tokio"), feature = "agnostic"))]
        let outcome = self
            .shutdown
            .token()
            .run(self.integration.wait_for_shutdown())
            .await
            .ok();
        #[cfg(feature = "tokio")]
        let outcome = self
            .shutdown
            .run_until_cancelled(self.integration.wait_for_shutdown())
            .await;

        match outcome {
            Some(()) => {
                info!(
                    "Received shutdown event. Distribute shutdown event via cancellation token..."
                );
                self.integration.notify_stopping();
                self.shutdown.cancel();
            }
            None => {
                info!("Task was canceled via token. Shutting down...");
            }
        }
    }
}

/// Holds a service's active configuration and distributes updates to all registered receivers.
///
/// Created via [`ServiceConfig::try_load`] or [`ServiceConfig::try_load_or_default`].
/// The [`Daemon`] uses this type to broadcast configuration changes to running services.
pub struct ServiceConfig<Data, Format> {
    file: PathBuf,
    file_format: Format,
    config_sender: Sender<Data>,
    config_receiver: Receiver<Data>,
}

impl<Data, Format> ServiceConfig<Data, Format> {
    /// Returns a reference to the configuration channel sender.
    pub fn sender(&self) -> &Sender<Data> {
        &self.config_sender
    }

    /// Returns a reference to the configuration channel receiver.
    pub fn receiver(&self) -> &Receiver<Data> {
        &self.config_receiver
    }

    /// Returns a mutable reference to the configuration channel receiver.
    pub fn receiver_mut(&mut self) -> &mut Receiver<Data> {
        &mut self.config_receiver
    }
}

impl<Data, Format> ServiceConfig<Data, Format>
where
    Data: Clone,
{
    /// Returns the last configuration.
    ///
    /// # Returns
    /// A clone of the last successfully loaded configuration.
    pub fn get(&self) -> Data {
        self.config_receiver.borrow().clone()
    }
}

impl<Data, Format> ServiceConfig<Data, Format>
where
    Data: Default + Serialize + DeserializeOwned + Debug,
    Format: ConfigFormat<Data = Data>,
{
    /// Tries to load configuration from the given path.
    ///
    /// If `file` does not exist, this function attempts to create a new config file
    /// containing the `Data` default.
    ///
    /// # Arguments
    /// * `file` - Location of the configuration file
    /// * `file_format` - The underlying configuration file format.
    ///
    /// # Returns
    /// * `Ok(Self)` - Successfully loaded configuration.
    /// * `Err(LoadError)` - An error containing the reason why configuration loading failed.
    #[instrument(skip_all)]
    pub fn try_load(file: &Path, file_format: Format) -> Result<Self, LoadError> {
        let data = Self::try_load_data(file, &file_format)?;
        let (config_sender, config_receiver) = channel(data);

        Ok(Self {
            file: file.to_path_buf(),
            file_format,
            config_sender,
            config_receiver,
        })
    }

    /// Tries to load configuration from the given path, falling back to the default if loading fails.
    ///
    /// If `file` does not exist, this function attempts to create a new config file
    /// containing the `Data` default.
    ///
    /// # Arguments
    /// * `file` - Location of the configuration file
    /// * `file_format` - Implementation of the underlying config file format
    ///
    /// # Returns
    /// Configuration either loaded from a file or constructed as default.
    #[instrument(skip_all)]
    pub fn try_load_or_default(file: &Path, file_format: Format) -> Self {
        let data = Self::try_load_data(file, &file_format).unwrap_or_else(|_| {
            let data = Data::default();
            warn!("Failed to load config. Use default configuration instead. Config is {data:?}.");
            data
        });

        let (config_sender, config_receiver) = channel(data);

        Self {
            file: file.to_path_buf(),
            file_format,
            config_sender,
            config_receiver,
        }
    }

    #[instrument(skip_all)]
    fn try_load_data(file: &Path, file_format: &Format) -> Result<Data, LoadError> {
        info!("Trying to load configuration from '{}'.", file.display());

        match file_format.try_load(file) {
            Ok(data) => {
                info!("Loaded config successfully. Config is {data:?}.");
                Ok(data)
            }
            Err(LoadError::Deserialize(err)) => {
                error!("Failed to deserialize config. Error was: '{err}'");
                Err(LoadError::from(err))
            }
            Err(LoadError::Io(err)) => {
                warn!("Failed to load config from IO error. Error was: '{err}'.");

                if let ErrorKind::NotFound = err.kind() {
                    info!("Config file was not found. Try to store default at given location.");

                    let data = Data::default();
                    if file_format.try_save(file, &data).is_err() {
                        error!("Failed to store default config.");
                        Err(LoadError::from(err))
                    } else {
                        info!("Default config successfully created.");
                        Ok(data)
                    }
                } else {
                    error!("Failed to load config. Error was: '{err}'");
                    Err(LoadError::from(err))
                }
            }
        }
    }
}

impl<Data, Format> AsRef<Path> for ServiceConfig<Data, Format> {
    fn as_ref(&self) -> &Path {
        self.file.as_ref()
    }
}

#[cfg(feature = "json")]
impl<Data> ServiceConfig<Data, config_format::Json<Data>>
where
    Data: Default + Serialize + DeserializeOwned + Debug,
{
    /// Tries to load configuration stored in JSON from the given path.
    ///
    /// If `file` does not exist, this function attempts to create a new config file
    /// containing the `Data` default.
    ///
    /// # Arguments
    /// * `file` - Location of the configuration file
    ///
    /// # Returns
    /// * `Ok(Self)` - Successfully loaded configuration.
    /// * `Err(LoadError)` - An error containing the reason why configuration loading failed.
    #[instrument(skip_all)]
    pub fn try_load_json(file: &Path) -> Result<Self, LoadError> {
        Self::try_load(file, config_format::Json::<Data>::default())
    }

    /// Tries to load configuration stored in JSON from the given path, falling back to the default if loading fails.
    ///
    /// If `file` does not exist, this function attempts to create a new config file
    /// containing the `Data` default.
    ///
    /// # Arguments
    /// * `file` - Location of the configuration file
    ///
    /// # Returns
    /// Configuration either loaded from a file or constructed as default.
    #[instrument(skip_all)]
    pub fn try_load_json_or_default(file: &Path) -> Self {
        Self::try_load_or_default(file, config_format::Json::<Data>::default())
    }
}

#[cfg(feature = "toml")]
impl<Data> ServiceConfig<Data, config_format::Toml<Data>>
where
    Data: Default + Serialize + DeserializeOwned + Debug,
{
    /// Tries to load configuration stored in TOML from the given path.
    ///
    /// If `file` does not exist, this function attempts to create a new config file
    /// containing the `Data` default.
    ///
    /// # Arguments
    /// * `file` - Location of the configuration file
    ///
    /// # Returns
    /// * `Ok(Self)` - Successfully loaded configuration.
    /// * `Err(LoadError)` - An error containing the reason why configuration loading failed.
    #[instrument(skip_all)]
    pub fn try_load_toml(file: &Path) -> Result<Self, LoadError> {
        Self::try_load(file, config_format::Toml::<Data>::default())
    }

    /// Tries to load configuration stored in TOML from the given path, falling back to the default if loading fails.
    ///
    /// If `file` does not exist, this function attempts to create a new config file
    /// containing the `Data` default.
    ///
    /// # Arguments
    /// * `file` - Location of the configuration file
    ///
    /// # Returns
    /// Configuration either loaded from a file or constructed as default.
    #[instrument(skip_all)]
    pub fn try_load_toml_or_default(file: &Path) -> Self {
        Self::try_load_or_default(file, config_format::Toml::<Data>::default())
    }
}

#[cfg(feature = "yaml")]
impl<Data> ServiceConfig<Data, config_format::Yaml<Data>>
where
    Data: Default + Serialize + DeserializeOwned + Debug,
{
    /// Tries to load configuration stored in YAML from the given path.
    ///
    /// If `file` does not exist, this function attempts to create a new config file
    /// containing the `Data` default.
    ///
    /// # Arguments
    /// * `file` - Location of the configuration file
    ///
    /// # Returns
    /// * `Ok(Self)` - Successfully loaded configuration.
    /// * `Err(LoadError)` - An error containing the reason why configuration loading failed.
    #[instrument(skip_all)]
    pub fn try_load_yaml(file: &Path) -> Result<Self, LoadError> {
        Self::try_load(file, config_format::Yaml::<Data>::default())
    }

    /// Tries to load configuration stored in YAML from the given path, falling back to the default if loading fails.
    ///
    /// If `file` does not exist, this function attempts to create a new config file
    /// containing the `Data` default.
    ///
    /// # Arguments
    /// * `file` - Location of the configuration file
    ///
    /// # Returns
    /// Configuration either loaded from a file or constructed as default.
    #[instrument(skip_all)]
    pub fn try_load_yaml_or_default(file: &Path) -> Self {
        Self::try_load_or_default(file, config_format::Yaml::<Data>::default())
    }
}
