/* This file is part of DarkFi (https://dark.fi)
 *
 * Copyright (C) 2020-2024 Dyne.org foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

use std::{
    fmt,
    path::{Path, PathBuf},
    str::FromStr,
};

use log::{debug, error};
use serde::Deserialize;
use structopt::StructOpt;
use url::Url;

use darkfi::{rpc::settings::RpcSettingsOpt, util::file::load_file, Error, Result};

/// Represents an explorer configuration
#[derive(Clone, Debug, Deserialize, StructOpt)]
pub struct ExplorerConfig {
    /// Current active network
    #[allow(dead_code)] // Part of the config file
    pub network: String,
    /// Supported network configurations
    pub network_config: NetworkConfigs,
    /// Path to the configuration if read from a file
    pub path: Option<String>,
}

impl ExplorerConfig {
    /// Creates a new configuration from a given file path.
    /// If the file cannot be loaded or parsed, an error is returned.
    pub fn new(config_path: String) -> Result<Self> {
        // Load the configuration file from the specified path
        let config_content = load_file(Path::new(&config_path)).map_err(|err| {
            Error::ConfigError(format!(
                "Failed to read the configuration file {}: {:?}",
                config_path, err
            ))
        })?;

        // Parse the loaded content into a configuration instance
        let mut config = toml::from_str::<Self>(&config_content).map_err(|e| {
            error!(target: "explorerd::config", "Failed parsing TOML config: {}", e);
            Error::ConfigError(format!("Failed to parse the configuration file {}", config_path))
        })?;

        // Set the configuration path
        config.path = Some(config_path);

        debug!(target: "explorerd::config", "Successfully loaded configuration: {:?}", config);

        Ok(config)
    }

    /// Returns the currently active network configuration.
    #[allow(dead_code)] // Test case currently using
    pub fn active_network_config(&self) -> Option<ExplorerNetworkConfig> {
        self.get_network_config(self.network.as_str())
    }

    /// Returns the network configuration for specified network.
    pub fn get_network_config(&self, network: &str) -> Option<ExplorerNetworkConfig> {
        match network {
            "localnet" => self.network_config.localnet.clone(),
            "testnet" => self.network_config.testnet.clone(),
            "mainnet" => self.network_config.mainnet.clone(),
            _ => None,
        }
    }
}

/// Provides a default `ExplorerConfig` configuration using the `testnet` network.
impl Default for ExplorerConfig {
    fn default() -> Self {
        Self {
            network: String::from("testnet"),
            network_config: NetworkConfigs::default(),
            path: None,
        }
    }
}

/// Attempts to convert a [`PathBuff`] to an [`ExplorerConfig`] by loading and parsing from specified file path.
impl TryFrom<&PathBuf> for ExplorerConfig {
    type Error = Error;
    fn try_from(path: &PathBuf) -> Result<Self> {
        let path_str = path.to_str().ok_or_else(|| {
            Error::ConfigError("Unable to convert PathBuf to a valid UTF-8 path string".to_string())
        })?;

        // Create configuration and return
        ExplorerConfig::new(path_str.to_string())
    }
}

/// Deserializes a `&str` containing explorer content in TOML format into an [`ExplorerConfig`] instance.
impl FromStr for ExplorerConfig {
    type Err = String;
    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        let config: ExplorerConfig =
            toml::from_str(s).map_err(|e| format!("Failed to parse ExplorerdConfig: {}", e))?;
        Ok(config)
    }
}

/// Represents network configurations for localnet, testnet, and mainnet.
#[derive(Debug, Clone, Deserialize, StructOpt)]
pub struct NetworkConfigs {
    /// Local network configuration
    pub localnet: Option<ExplorerNetworkConfig>,
    /// Testnet network configuration
    pub testnet: Option<ExplorerNetworkConfig>,
    /// Mainnet network configuration
    pub mainnet: Option<ExplorerNetworkConfig>,
}

/// Provides a default `NetworkConfigs` configuration using the `testnet` network.
impl Default for NetworkConfigs {
    fn default() -> Self {
        NetworkConfigs {
            localnet: None,
            testnet: Some(ExplorerNetworkConfig::default()),
            mainnet: None,
        }
    }
}

/// Deserializes a `&str` containing network configs content in TOML format into an [`NetworkConfigs`] instance.
impl FromStr for NetworkConfigs {
    type Err = String;
    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        let config: NetworkConfigs =
            toml::from_str(s).map_err(|e| format!("Failed to parse NetworkConfigs: {}", e))?;
        Ok(config)
    }
}

/// Struct representing the configuration for an explorer network.
#[derive(Clone, Deserialize, StructOpt)]
#[structopt()]
#[serde(default)]
pub struct ExplorerNetworkConfig {
    #[structopt(flatten)]
    /// JSON-RPC settings used to set up a server that the explorer listens on for incoming RPC requests.
    pub rpc: RpcSettingsOpt,

    #[structopt(long, default_value = "~/.local/share/darkfi/explorerd/testnet")]
    /// Path to the explorer's database.
    pub database: String,

    #[structopt(short, long, default_value = "tcp://127.0.0.1:8340")]
    /// Endpoint of the DarkFi node JSON-RPC server to sync with.
    pub endpoint: Url,
}

/// Attempts to convert a tuple `(PathBuf, &str)` representing a configuration file path
/// and network name into an `ExplorerNetworkConfig`.
impl TryFrom<(&PathBuf, &String)> for ExplorerNetworkConfig {
    type Error = Error;
    fn try_from(path_and_network: (&PathBuf, &String)) -> Result<Self> {
        // Load the ExplorerConfig from the given file path
        let config: ExplorerConfig = path_and_network.0.try_into()?;
        // Retrieve the network configuration for the specified network
        match config.get_network_config(path_and_network.1) {
            Some(config) => Ok(config),
            None => Err(Error::ConfigError(format!(
                "Failed to retrieve network configuration for network: {}",
                path_and_network.1
            ))),
        }
    }
}

/// Provides a default `ExplorerNetworkConfig` instance using `structopt` default values defined
/// in the `ExplorerNetworkConfig` struct.
impl Default for ExplorerNetworkConfig {
    fn default() -> Self {
        Self::from_iter(&[""])
    }
}

/// Provides a user-friendly debug view of the `ExplorerdNetworkConfig` configuration.
impl fmt::Debug for ExplorerNetworkConfig {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut debug_struct = f.debug_struct("ExplorerdConfig");
        debug_struct
            .field("rpc_listen", &self.rpc.rpc_listen.to_string().trim_end_matches('/'))
            .field("db_path", &self.database)
            .field("endpoint", &self.endpoint.to_string().trim_end_matches('/'));
        debug_struct.finish()
    }
}

/// Deserializes a `&str` containing network config content in TOML format into an [`ExplorerNetworkConfig`] instance.
impl FromStr for ExplorerNetworkConfig {
    type Err = String;
    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        let config: ExplorerNetworkConfig = toml::from_str(s)
            .map_err(|e| format!("Failed to parse ExplorerdNetworkConfig: {}", e))?;
        Ok(config)
    }
}

#[cfg(test)]
/// Contains test cases for validating the functionality and correctness of the `ExplorerConfig`
/// and related components using a configuration loaded from a TOML file.
mod tests {
    use std::path::Path;

    use super::*;

    use crate::test_utils::init_logger;

    /// Validates the functionality of initializing and interacting with `ExplorerConfig`
    /// loaded from a TOML file, ensuring correctness of the network-specific configurations.
    #[test]
    fn test_explorerd_config_from_file() {
        // Constants for expected configurations
        const CONFIG_PATH: &str = "explorerd_config.toml";
        const ACTIVE_NETWORK: &str = "testnet";

        const NETWORK_CONFIGS: &[(&str, &str, &str, &str)] = &[
            (
                "localnet",
                "~/.local/share/darkfi/explorerd/localnet",
                "tcp://127.0.0.1:8240/",
                "tcp://127.0.0.1:14567/",
            ),
            (
                "testnet",
                "~/.local/share/darkfi/explorerd/testnet",
                "tcp://127.0.0.1:8340/",
                "tcp://127.0.0.1:14667/",
            ),
            (
                "mainnet",
                "~/.local/share/darkfi/explorerd/mainnet",
                "tcp://127.0.0.1:8440/",
                "tcp://127.0.0.1:14767/",
            ),
        ];

        init_logger(simplelog::LevelFilter::Info, vec!["sled", "runtime", "net"]);

        // Ensure the configuration file exists
        assert!(Path::new(CONFIG_PATH).exists());

        // Load the configuration
        let config = ExplorerConfig::new(CONFIG_PATH.to_string())
            .expect("Failed to load configuration from file");

        // Validate the expected network
        assert_eq!(config.network, ACTIVE_NETWORK);

        // Validate the path is correctly set
        assert_eq!(config.path.as_deref(), Some(CONFIG_PATH));

        // Validate that `active_network_config` correctly retrieves the testnet configuration
        let active_config = config.active_network_config();
        assert!(active_config.is_some(), "Active network configuration should not be None.");
        let active_config = active_config.unwrap();
        assert_eq!(active_config.database, NETWORK_CONFIGS[1].1); // Testnet database
        assert_eq!(active_config.endpoint.to_string(), NETWORK_CONFIGS[1].2);
        assert_eq!(&active_config.rpc.rpc_listen.to_string(), NETWORK_CONFIGS[1].3);

        // Validate all network configurations values (localnet, testnet, mainnet)
        for &(network, expected_db, expected_endpoint, expected_rpc) in NETWORK_CONFIGS {
            let network_config = config.get_network_config(network);

            if let Some(config) = network_config {
                assert_eq!(config.database, expected_db);
                assert_eq!(config.endpoint.to_string(), expected_endpoint);
                assert_eq!(config.rpc.rpc_listen.to_string(), expected_rpc);
            } else {
                assert!(network_config.is_none(), "{} configuration is missing", network);
            }
        }

        // Validate (path, network).try_into()
        let config_path_buf = &PathBuf::from(CONFIG_PATH);
        let mainnet_string = &String::from("mainnet");
        let mainnet_config: ExplorerNetworkConfig = (config_path_buf, mainnet_string)
            .try_into()
            .expect("Failed to load explorer network config");
        assert_eq!(mainnet_config.database, NETWORK_CONFIGS[2].1); // Mainnet database
        assert_eq!(mainnet_config.endpoint.to_string(), NETWORK_CONFIGS[2].2);
        assert_eq!(&mainnet_config.rpc.rpc_listen.to_string(), NETWORK_CONFIGS[2].3);
    }
}
