// Copyright (c) Subzero Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

//! Simplified configuration manager for the Rialo CDK.
//!
//! This module provides a clean interface to the layered configuration system
//! using schema-generated types directly, eliminating the need for complex
//! conversion layers.

use std::{
    path::{Path, PathBuf},
    sync::{Arc, RwLock},
};

use anyhow::{anyhow, Result};
#[cfg(feature = "file-storage")]
use dirs;
#[cfg(feature = "file-storage")]
use rialo_modular_config::FileLayer;
use rialo_modular_config::{
    ConfigLayer, ConfigLayerSource, ConfigValue, InMemoryLayer, LayeredConfig, LayeredConfigBuilder,
};
use serde_json::Value;

use crate::config::generated::RialoCdkConfigurationSchema;

/// Embedded JSON schema for CDK configuration
#[cfg(feature = "schema-validation")]
const CDK_CONFIG_SCHEMA: &str = include_str!("../../schemas/cdk-config.json");

/// Embedded default TOML configuration for CDK
const CDK_DEFAULTS_TOML: &str = include_str!("../../config/defaults.toml");

/// Simplified configuration manager that wraps LayeredConfig with schema-aware operations.
///
/// This manager provides a clean interface for loading and saving configuration
/// using the schema-generated types directly, eliminating complex conversion layers.
pub struct ConfigManager {
    layered_config: Arc<RwLock<LayeredConfig>>,
    user_config_path: PathBuf,
    #[allow(dead_code)]
    overrides: Option<std::collections::HashMap<String, String>>,
}

impl ConfigManager {
    /// Create a new configuration manager with the specified user config path.
    ///
    /// # Arguments
    ///
    /// * `user_config_path` - Path to the user's configuration file
    ///
    /// # Returns
    ///
    /// A configured ConfigManager or an error if initialization fails
    pub fn new(user_config_path: impl Into<PathBuf>) -> Result<Self> {
        let user_config_path = user_config_path.into();

        // Create minimal layer stack: Default -> User
        let default_layer = Self::create_default_layer()?;

        let layered_config = LayeredConfigBuilder::new().with_layer(default_layer);

        #[cfg(feature = "file-storage")]
        let layered_config = layered_config.with_layer(
            FileLayer::new(&user_config_path)
                .with_source(ConfigLayerSource::User(user_config_path.clone()))
                .required(false) // File might not exist initially
                .build()?,
        );

        #[cfg(feature = "schema-validation")]
        let layered_config = layered_config.with_schema(Self::load_schema()?).build()?;

        #[cfg(not(feature = "schema-validation"))]
        let layered_config = layered_config.build()?;

        Ok(Self {
            layered_config: Arc::new(RwLock::new(layered_config)),
            user_config_path,
            overrides: None,
        })
    }

    /// Create a new configuration manager with overrides.
    ///
    /// # Arguments
    ///
    /// * `user_config_path` - Path to the user's configuration file
    /// * `overrides` - argument overrides as key-value pairs
    ///
    /// # Returns
    ///
    /// A configured ConfigManager with overrides applied
    pub fn with_overrides(
        user_config_path: impl Into<PathBuf>,
        overrides: std::collections::HashMap<String, String>,
    ) -> Result<Self> {
        let user_config_path = user_config_path.into();

        // Create layer stack: Default -> [User] -> Overrides
        let default_layer = Self::create_default_layer()?;

        #[cfg(feature = "file-storage")]
        let mut layered_config = LayeredConfigBuilder::new().with_layer(default_layer);
        #[cfg(not(feature = "file-storage"))]
        let layered_config = LayeredConfigBuilder::new().with_layer(default_layer);

        // Add user file layer only if file-storage is enabled
        #[cfg(feature = "file-storage")]
        {
            let user_layer = FileLayer::new(&user_config_path)
                .with_source(ConfigLayerSource::User(user_config_path.clone()))
                .required(false)
                .build()?;
            layered_config = layered_config.with_layer(user_layer);
        }

        // Add overrides layer
        let overrides_layer = Self::create_overrides_layer(&overrides)?;
        let layered_config = layered_config.with_layer(overrides_layer);

        #[cfg(feature = "schema-validation")]
        let layered_config = layered_config.with_schema(Self::load_schema()?).build()?;

        #[cfg(not(feature = "schema-validation"))]
        let layered_config = layered_config.build()?;

        Ok(Self {
            layered_config: Arc::new(RwLock::new(layered_config)),
            user_config_path,
            overrides: Some(overrides),
        })
    }

    /// Load the complete configuration as a schema type.
    ///
    /// # Returns
    ///
    /// The resolved configuration as a RialoCdkConfigurationSchema
    pub fn load(&self) -> Result<RialoCdkConfigurationSchema> {
        let layered_config = self
            .layered_config
            .read()
            .map_err(|e| anyhow!("Failed to acquire read lock on layered config: {}", e))?;
        let resolved = layered_config.get_resolved_values();

        // Convert flat keys to nested structure and then to JSON
        let nested_config = ConfigValue::from_flat_map(resolved);
        let json_value = nested_config.to_json_value();

        serde_json::from_value(json_value)
            .map_err(|e| anyhow!("Failed to deserialize configuration to schema type: {}", e))
    }

    /// Save the complete configuration to the user layer.
    ///
    /// This will write all configuration values to the user's config file,
    /// overwriting any existing values but preserving the layered structure.
    ///
    /// **Note**: This method is only available with the `file-storage` feature.
    ///
    /// # Arguments
    ///
    /// * `config` - The configuration to save
    ///
    /// # Returns
    ///
    /// Success or an error if saving fails
    #[cfg(feature = "file-storage")]
    pub fn save(&self, config: &RialoCdkConfigurationSchema) -> Result<()> {
        let layered_config = self
            .layered_config
            .read()
            .map_err(|e| anyhow!("Failed to acquire read lock on layered config: {}", e))?;
        let user_layer = layered_config
            .find_user_layer()
            .ok_or_else(|| anyhow!("No user layer available for saving"))?;

        // Convert config to JSON (already has cdk structure)
        let json_value = serde_json::to_value(config)?;
        Self::save_json_to_layer(&layered_config, &json_value, user_layer, None)?;

        // Drop the borrow before reloading
        drop(layered_config);

        // Reload to pick up the saved changes
        self.reload()?;

        Ok(())
    }

    /// Get a specific configuration value by key.
    ///
    /// # Arguments
    ///
    /// * `key` - The configuration key (supports dot notation)
    ///
    /// # Returns
    ///
    /// The configuration value if found
    pub fn get<T>(&self, key: &str) -> Option<T>
    where
        T: serde::de::DeserializeOwned,
    {
        // Namespace the key under "cdk"
        let namespaced_key = format!("cdk.{}", key);
        self.layered_config
            .read()
            .map_err(|e| anyhow!("Failed to acquire read lock on layered config: {}", e))
            .ok()?
            .get(&namespaced_key)
    }

    /// Set a specific configuration value.
    ///
    /// With `file-storage` enabled: Saves to the user's config file and persists between sessions.
    /// Without `file-storage`: Returns an error - use overrides or modify defaults at startup instead.
    ///
    /// # Arguments
    ///
    /// * `key` - The configuration key (supports dot notation)
    /// * `value` - The value to set
    ///
    /// # Returns
    ///
    /// Success or an error if setting fails (includes validation errors)
    #[cfg(feature = "file-storage")]
    pub fn set<T>(&self, key: &str, value: T) -> Result<()>
    where
        T: serde::Serialize,
    {
        let layered_config = self
            .layered_config
            .write()
            .map_err(|e| anyhow!("Failed to acquire write lock on layered config: {}", e))?;
        let user_layer = layered_config
            .find_user_layer()
            .ok_or_else(|| anyhow!("No user layer available for setting values"))?;
        // Namespace the key under "cdk"
        let namespaced_key = format!("cdk.{}", key);
        layered_config.set_in_layer(&namespaced_key, value, user_layer)?;

        // Drop the borrow before reloading
        drop(layered_config);

        // Reload to ensure the changes are reflected immediately
        self.reload()?;
        Ok(())
    }

    #[cfg(not(feature = "file-storage"))]
    pub fn set<T>(&self, _key: &str, _value: T) -> Result<()>
    where
        T: serde::Serialize,
    {
        Err(anyhow!(
            "Configuration setting is not available without the 'file-storage' feature. \
            Use overrides when creating the ConfigManager instead."
        ))
    }

    /// Get direct access to the underlying LayeredConfig for advanced use cases.
    ///
    /// Note: This returns a borrowed reference through RwLock. Be careful not to
    /// cause panics by borrowing mutably while this reference is active.
    ///
    /// # Returns
    ///
    /// Reference to the underlying LayeredConfig
    pub fn with_layered_config<F, R>(&self, f: F) -> Result<R>
    where
        F: FnOnce(&LayeredConfig) -> R,
    {
        let layered_config = self
            .layered_config
            .read()
            .map_err(|e| anyhow!("Failed to acquire read lock on layered config: {}", e))?;
        Ok(f(&layered_config))
    }

    /// Reload the configuration by reconstructing the LayeredConfig.
    ///
    /// This picks up any changes made to the underlying files and preserves
    /// overrides if they were originally provided.
    ///
    /// # Returns
    ///
    /// Success or an error if reloading fails
    #[cfg(feature = "file-storage")]
    fn reload(&self) -> Result<()> {
        // Reconstruct the layered config with the same structure as new()
        let default_layer = Self::create_default_layer()?;

        let mut builder = LayeredConfigBuilder::new().with_layer(default_layer);

        // Add user file layer only if file-storage is enabled
        #[cfg(feature = "file-storage")]
        {
            let user_layer = FileLayer::new(&self.user_config_path)
                .with_source(ConfigLayerSource::User(self.user_config_path.clone()))
                .required(false)
                .build()?;
            builder = builder.with_layer(user_layer);
        }

        // Re-add overrides if they exist
        if let Some(ref overrides) = self.overrides {
            let overrides_layer = Self::create_overrides_layer(overrides)?;
            builder = builder.with_layer(overrides_layer);
        }

        #[cfg(feature = "schema-validation")]
        let new_layered_config = builder.with_schema(Self::load_schema()?).build()?;

        #[cfg(not(feature = "schema-validation"))]
        let new_layered_config = builder.build()?;

        // Replace the current layered config
        *self
            .layered_config
            .write()
            .map_err(|e| anyhow!("Failed to acquire write lock on layered config: {}", e))? =
            new_layered_config;

        Ok(())
    }

    /// Get the path to the user configuration file.
    ///
    /// # Returns
    ///
    /// Path to the user's configuration file
    pub fn user_config_path(&self) -> &Path {
        &self.user_config_path
    }

    /// Create a overrides layer from the provided overrides.
    fn create_overrides_layer(
        overrides: &std::collections::HashMap<String, String>,
    ) -> Result<ConfigLayer> {
        let mut overrides_layer_builder = InMemoryLayer::new(ConfigLayerSource::CommandLineArgs);
        for (key, value) in overrides {
            // Namespace the key under "cdk"
            let namespaced_key = format!("cdk.{}", key);

            // Convert string values to appropriate types based on key
            match key.as_str() {
                "verbose_mode" => {
                    let bool_value: bool = value
                        .parse()
                        .map_err(|_| anyhow!("Invalid boolean value for {}: {}", key, value))?;
                    overrides_layer_builder =
                        overrides_layer_builder.with_value(&namespaced_key, bool_value);
                }
                _ => {
                    // For other values, keep as strings
                    overrides_layer_builder =
                        overrides_layer_builder.with_value(&namespaced_key, value.as_str());
                }
            }
        }
        Ok(overrides_layer_builder.build())
    }

    /// Create the default configuration layer from embedded TOML.
    fn create_default_layer() -> Result<ConfigLayer> {
        let toml_value: toml::Value = toml::from_str(CDK_DEFAULTS_TOML)
            .map_err(|e| anyhow!("Failed to parse embedded CDK defaults: {}", e))?;
        let config_value = ConfigValue::from_toml_value(toml_value);

        let values = if let ConfigValue::Object(map) = config_value {
            map
        } else {
            return Err(anyhow!("CDK defaults must be a TOML table"));
        };

        Ok(InMemoryLayer::new(ConfigLayerSource::Default)
            .with_config_values(values)
            .build())
    }

    /// Load the JSON schema for validation.
    /// Since the CDK should only validate the "cdk" section, we extract that schema portion.
    #[cfg(feature = "schema-validation")]
    fn load_schema() -> Result<Value> {
        let full_schema: Value = serde_json::from_str(CDK_CONFIG_SCHEMA)
            .map_err(|e| anyhow!("Failed to parse embedded CDK schema: {}", e))?;

        // Use the full schema since we validate the complete nested structure
        Ok(full_schema)
    }

    /// Get the embedded CDK JSON schema for external use (e.g., CLI merging).
    ///
    /// # Returns
    ///
    /// The CDK configuration JSON schema as a serde_json::Value
    #[cfg(feature = "schema-validation")]
    pub fn get_cdk_schema() -> Result<Value> {
        Self::load_schema()
    }

    /// Get the embedded CDK JSON schema for external use (no-op when schema-validation is disabled)
    #[cfg(not(feature = "schema-validation"))]
    pub fn get_cdk_schema() -> Result<Value> {
        Err(anyhow!(
            "Schema validation is not available when 'schema-validation' feature is disabled"
        ))
    }

    /// Get the embedded CDK defaults TOML as a string for external use (e.g., CLI merging).
    ///
    /// # Returns
    ///
    /// The CDK defaults TOML content as a string
    pub fn get_cdk_defaults_toml() -> &'static str {
        CDK_DEFAULTS_TOML
    }

    /// Recursively save a JSON value to a configuration layer.
    #[cfg(feature = "file-storage")]
    fn save_json_to_layer(
        layered_config: &LayeredConfig,
        json_value: &Value,
        layer: &ConfigLayer,
        prefix: Option<&str>,
    ) -> Result<()> {
        match json_value {
            Value::Object(obj) => {
                for (key, value) in obj {
                    let full_key = match prefix {
                        Some(p) => format!("{}.{}", p, key),
                        None => key.clone(),
                    };

                    match value {
                        Value::Object(_) => {
                            // Recursively handle nested objects
                            Self::save_json_to_layer(
                                layered_config,
                                value,
                                layer,
                                Some(&full_key),
                            )?;
                        }
                        _ => {
                            // Save primitive values directly
                            layered_config.set_in_layer(&full_key, value, layer)?;
                        }
                    }
                }
            }
            _ => {
                // If we have a non-object at the root, save it directly
                let key = prefix.unwrap_or("root");
                layered_config.set_in_layer(key, json_value, layer)?;
            }
        }
        Ok(())
    }
}

/// Utility functions for common configuration operations.
pub struct ConfigUtils;

impl ConfigUtils {
    /// Get the default user configuration directory.
    ///
    /// # Returns
    ///
    /// Path to the default configuration directory
    #[cfg(feature = "file-storage")]
    pub fn default_config_dir() -> Result<PathBuf> {
        // Preferred OS-specific config directory (`<os-specific-config-dir>/rialo`).
        let preferred = dirs::config_dir()
            .map(|config_dir| config_dir.join("rialo"))
            .ok_or_else(|| anyhow!("Cannot determine config directory"))?;

        // Legacy directory (~/.rialo).
        let legacy = dirs::home_dir()
            .map(|home| home.join(".rialo"))
            .ok_or_else(|| anyhow!("Cannot determine home directory"))?;

        // Use legacy if it exists AND preferred does not exist.
        // Otherwise, use preferred.
        let dir = if legacy.exists() && !preferred.exists() {
            legacy
        } else {
            preferred
        };

        Ok(dir)
    }

    /// Get the default user configuration file path.
    ///
    /// # Returns
    ///
    /// Path to the default configuration file
    #[cfg(feature = "file-storage")]
    pub fn default_config_file() -> Result<PathBuf> {
        Ok(Self::default_config_dir()?.join("config.toml"))
    }

    /// Create a configuration manager with default paths.
    ///
    /// # Returns
    ///
    /// A ConfigManager using the default configuration file location
    #[cfg(feature = "file-storage")]
    pub fn create_default_manager() -> Result<ConfigManager> {
        ConfigManager::new(Self::default_config_file()?)
    }

    /// Create a configuration manager (in-memory only when file-storage is disabled).
    ///
    /// # Returns
    ///
    /// A ConfigManager using only default configuration
    #[cfg(not(feature = "file-storage"))]
    pub fn create_default_manager() -> Result<ConfigManager> {
        ConfigManager::new(PathBuf::new()) // Path is ignored without file-storage
    }

    /// Get RPC URL for the specified network from a configuration.
    ///
    /// # Arguments
    ///
    /// * `config` - The configuration to query
    /// * `network` - The network name (if None, uses current network)
    ///
    /// # Returns
    ///
    /// The RPC URL for the network
    pub fn get_rpc_url(
        config: &RialoCdkConfigurationSchema,
        network: Option<&str>,
    ) -> Result<String> {
        let cdk_config = config
            .cdk
            .as_ref()
            .ok_or_else(|| anyhow!("CDK configuration section is missing"))?;

        let current_network_str = cdk_config.network.to_string();
        let target_network = network.unwrap_or(&current_network_str);

        cdk_config
            .rpc_urls
            .get(target_network)
            .cloned()
            .ok_or_else(|| anyhow!("No RPC URL configured for network: {}", target_network))
    }
}

#[cfg(test)]
mod tests {
    use tempfile::TempDir;

    use super::*;

    #[test]
    fn test_config_manager_creation() {
        let temp_dir = TempDir::new().unwrap();
        let config_path = temp_dir.path().join("test_config.toml");

        let manager = ConfigManager::new(&config_path).unwrap();

        // Should be able to load default configuration
        let config = manager.load().unwrap();
        assert_eq!(config.cdk.as_ref().unwrap().network.to_string(), "localnet");
        assert_eq!(
            config
                .cdk
                .as_ref()
                .unwrap()
                .default_balance_unit
                .to_string(),
            "rlo"
        );
        assert!(!config.cdk.as_ref().unwrap().verbose_mode);
    }

    #[test]
    fn test_config_save_and_load() {
        let temp_dir = TempDir::new().unwrap();
        let config_path = temp_dir.path().join("test_save.toml");

        let manager = ConfigManager::new(&config_path).unwrap();

        // Load default config and modify it
        let mut config = manager.load().unwrap();
        if let Some(cdk_config) = &mut config.cdk {
            cdk_config.verbose_mode = true;
        }

        // Save and reload (should now work automatically with interior mutability)
        manager.save(&config).unwrap();
        let reloaded_config = manager.load().unwrap();

        assert!(reloaded_config.cdk.as_ref().unwrap().verbose_mode);
        assert_eq!(
            reloaded_config.cdk.as_ref().unwrap().network.to_string(),
            "localnet"
        ); // Should preserve other values
    }

    #[test]
    fn test_individual_set_and_get() {
        let temp_dir = TempDir::new().unwrap();
        let config_path = temp_dir.path().join("test_individual.toml");

        let manager = ConfigManager::new(&config_path).unwrap();

        // Set individual value
        manager.set("verbose_mode", true).unwrap();

        // Get individual value (should work with interior mutability)
        let verbose: bool = manager.get("verbose_mode").unwrap();
        assert!(verbose);

        // Load complete config to verify
        let config = manager.load().unwrap();
        assert!(config.cdk.as_ref().unwrap().verbose_mode);
    }

    #[test]
    fn test_overrides() {
        let temp_dir = TempDir::new().unwrap();
        let config_path = temp_dir.path().join("test_overrides.toml");

        let mut overrides = std::collections::HashMap::new();
        overrides.insert("verbose_mode".to_string(), "true".to_string());
        overrides.insert("network".to_string(), "testnet".to_string());

        let manager = ConfigManager::with_overrides(&config_path, overrides).unwrap();

        let config = manager.load().unwrap();
        assert!(config.cdk.as_ref().unwrap().verbose_mode); // From overrides
        assert_eq!(config.cdk.as_ref().unwrap().network.to_string(), "testnet");
        // From overrides
    }

    #[test]
    fn test_schema_validation() {
        let temp_dir = TempDir::new().unwrap();
        let config_path = temp_dir.path().join("test_validation.toml");

        let manager = ConfigManager::new(&config_path).unwrap();

        // Try to set an invalid network (should fail validation)
        let result = manager.set("network", "invalid_network");
        assert!(result.is_err());

        // Try to set a valid network (should succeed)
        let result = manager.set("network", "testnet");
        assert!(result.is_ok());

        // Verify the change (should work with interior mutability)
        let network: String = manager.get("network").unwrap();
        assert_eq!(network, "testnet");
    }

    #[cfg(not(feature = "file-storage"))]
    #[test]
    fn test_no_file_storage_behavior() {
        use std::path::PathBuf;

        // Should create manager successfully even without file-storage
        let manager = ConfigManager::new(PathBuf::from("/nonexistent/path")).unwrap();

        // Should be able to load defaults
        let config = manager.load().unwrap();
        assert_eq!(config.cdk.as_ref().unwrap().network.to_string(), "localnet");

        // Should NOT be able to set individual values (no file-storage)
        let result = manager.set("verbose_mode", true);
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("file-storage"));

        // Should be able to get values from defaults
        let verbose: bool = manager.get("verbose_mode").unwrap();
        assert!(!verbose); // Default is false
    }

    #[cfg(not(feature = "file-storage"))]
    #[test]
    fn test_no_file_storage_with_overrides() {
        use std::{collections::HashMap, path::PathBuf};

        // Create overrides
        let mut overrides = HashMap::new();
        overrides.insert("verbose_mode".to_string(), "true".to_string());
        overrides.insert("network".to_string(), "testnet".to_string());

        // Should work with overrides even without file-storage
        let manager = ConfigManager::with_overrides(PathBuf::from("/ignored"), overrides).unwrap();

        let config = manager.load().unwrap();
        assert!(config.cdk.as_ref().unwrap().verbose_mode); // From overrides
        assert_eq!(config.cdk.as_ref().unwrap().network.to_string(), "testnet"); // From overrides

        // Default config manager should also work
        let default_manager = ConfigUtils::create_default_manager().unwrap();
        let default_config = default_manager.load().unwrap();
        assert_eq!(
            default_config.cdk.as_ref().unwrap().network.to_string(),
            "localnet"
        );
    }
}
