guardy 0.2.4

Fast, secure git hooks in Rust with secret scanning and protected file synchronization
Documentation
//! Global file configuration loading and caching
//!
//! Provides a single static instance of the merged file configuration
//! that is loaded once and shared across all config modules.

use std::sync::{Arc, LazyLock, OnceLock};

use super::loader::ConfigFileLoader;

/// Global custom config path resolved lazily from CLI args or env vars
/// This is resolved only when file loading starts
static CUSTOM_CONFIG_PATH: OnceLock<Option<String>> = OnceLock::new();

/// Get the custom config path, resolving it lazily from CLI/env on first access
fn get_custom_config_path() -> Option<&'static str> {
    CUSTOM_CONFIG_PATH
        .get_or_init(|| {
            // 1. Check CLI --config parameter first
            if let Some(cli) = crate::cli::CLI.get()
                && let Some(config_path) = &cli.config
            {
                tracing::debug!("📁 Using custom config from CLI: {}", config_path);
                return Some(config_path.clone());
            }

            // 2. Check GUARDY_CONFIG env var as fallback
            if let Ok(config_path) = std::env::var("GUARDY_CONFIG") {
                tracing::debug!("📁 Using custom config from GUARDY_CONFIG: {}", config_path);
                return Some(config_path);
            }

            // 3. No custom config - use hierarchical discovery
            tracing::debug!("📁 No custom config specified, using discovery");
            None
        })
        .as_deref()
}

/// Determine if recursive config loading should be enabled
/// Checks CLI args first, then environment variables (before full config loading)
fn should_use_recursive_config() -> bool {
    // 1. Check CLI --recursive-config parameter first (highest priority)
    if let Some(cli) = crate::cli::CLI.get()
        && let Some(recursive) = cli.recursive_config
    {
        tracing::debug!(
            "📁 Recursive config set via CLI: --recursive-config={}",
            recursive
        );
        return recursive;
    }

    // 2. Check GUARDY_RECURSIVE_CONFIG environment variable
    // Use same parsing logic as the macro for consistency
    if let Ok(value) = std::env::var("GUARDY_RECURSIVE_CONFIG") {
        match value.as_str() {
            "1" | "true" | "TRUE" | "yes" | "YES" | "on" | "ON" => {
                tracing::debug!(
                    "📁 Recursive config enabled via GUARDY_RECURSIVE_CONFIG={}",
                    value
                );
                return true;
            }
            "0" | "false" | "FALSE" | "no" | "NO" | "off" | "OFF" => {
                tracing::debug!(
                    "📁 Recursive config disabled via GUARDY_RECURSIVE_CONFIG={}",
                    value
                );
                return false;
            }
            _ => tracing::warn!(
                "Invalid GUARDY_RECURSIVE_CONFIG value: {}, defaulting to true",
                value
            ),
        }
    }

    // 3. Default to true (maintain current behavior)
    true
}

/// Global merged file configuration as JSON - loaded once and cached
/// This eliminates duplicate struct definitions by storing as serde_json::Value
pub static MERGED_FILE_CONFIG: LazyLock<Arc<serde_json::Value>> = LazyLock::new(|| {
    let start = std::time::Instant::now();
    let config = Arc::new(load_merged_file_config());
    let duration = start.elapsed();
    tracing::debug!("⚡ File config cached in {:?}", duration);
    config
});

/// Load and merge all configuration files into a single JSON value
/// This function is called once by the LazyLock to initialize MERGED_FILE_CONFIG
fn load_merged_file_config() -> serde_json::Value {
    let start = std::time::Instant::now();

    // Check if custom config path was provided via CLI
    let custom_config_path = get_custom_config_path();

    let loader = ConfigFileLoader::new("guardy");

    let result = if let Some(config_path) = custom_config_path {
        // Load custom config file
        let path = std::path::Path::new(&config_path);
        if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
            if let Some(format) = super::loader::ConfigFileFormat::from_extension(ext) {
                match loader.load_config_file::<serde_json::Value>(path, format) {
                    Ok(config) => {
                        tracing::debug!("✅ Custom config file loaded: {}", path.display());
                        config
                    }
                    Err(e) => {
                        tracing::warn!("⚠️  Failed to load custom config: {}", e);
                        serde_json::Value::Object(serde_json::Map::new())
                    }
                }
            } else {
                tracing::warn!("⚠️  Unsupported config file format: {}", ext);
                serde_json::Value::Object(serde_json::Map::new())
            }
        } else {
            tracing::warn!("⚠️  Config file has no extension: {}", config_path);
            serde_json::Value::Object(serde_json::Map::new())
        }
    } else {
        // Check if recursive loading should be used
        let use_recursive = should_use_recursive_config();

        if use_recursive {
            // Normal hierarchical discovery with merging
            match loader.discover_and_load_merged::<serde_json::Value>() {
                Ok(Some(config)) => {
                    tracing::debug!("✅ File configuration discovered and merged (recursive mode)");
                    config
                }
                Ok(None) => {
                    tracing::debug!("📄 No config files found (recursive mode)");
                    serde_json::Value::Object(serde_json::Map::new())
                }
                Err(e) => {
                    tracing::warn!("⚠️  Failed to load file configuration: {}", e);
                    serde_json::Value::Object(serde_json::Map::new())
                }
            }
        } else {
            // Non-recursive mode: no discovery, just use defaults
            tracing::debug!("📄 Recursive config disabled - using defaults only");
            serde_json::Value::Object(serde_json::Map::new())
        }
    };

    let duration = start.elapsed();
    tracing::debug!("⚡ File config loading completed in {:?}", duration);

    result
}