//! Build-time configuration loader for Rustelo website
//!
//! This crate provides shared configuration loading utilities for all build.rs scripts
//! in the workspace, following DRY principles and ensuring consistent configuration
//! across server, client, shared, and pages builds.
//!
//! # Modules
//!
//! - [`route_config`]: Typed route configuration loading (NCL + TOML)
//! - [`content_kind_config`]: Typed content kinds configuration loading (NCL + TOML)

pub mod content_kind_config;
pub mod rbac_config;
pub mod route_config;
pub mod site_config;

use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

/// Site configuration from config.toml or config.dev.toml
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SiteConfig {
    pub root: String,
    pub content_path: String,
    pub config_path: String,
    pub ui_path: String,
    pub i18n_path: String,
    pub assets_path: String,
}

/// Complete configuration loaded from TOML file
#[derive(Debug, Clone, Deserialize)]
pub struct Config {
    pub root_path: String,
    pub site: SiteConfig,
}

/// Environment type for determining which config file to use
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Environment {
    Development,
    Production,
}

impl Environment {
    /// Detect environment from Cargo profile or ENVIRONMENT env var
    pub fn detect() -> Self {
        // Check ENVIRONMENT env var first (explicit override)
        if let Ok(env_str) = env::var("ENVIRONMENT") {
            if env_str.to_lowercase().contains("prod") {
                return Environment::Production;
            }
        }

        // Fall back to Cargo profile (debug = development, release = production)
        let profile = env::var("PROFILE").unwrap_or_else(|_| "debug".to_string());
        if profile == "release" {
            Environment::Production
        } else {
            Environment::Development
        }
    }

    /// Get the config filename for this environment
    pub fn config_filename(&self) -> &'static str {
        match self {
            Environment::Development => "config.dev.toml",
            Environment::Production => "config.toml",
        }
    }
}

/// Load configuration from the appropriate TOML file
///
/// # Behavior
///
/// 1. Detects environment from `ENVIRONMENT` env var or Cargo `PROFILE`
/// 2. Looks for config file in the project root:
///    - Development: `config.dev.toml`
///    - Production: `config.toml`
/// 3. Returns error if the config file doesn't exist
/// 4. Parses the TOML file and returns configuration
///
/// # Errors
///
/// Returns error if:
/// - Configuration file doesn't exist
/// - Configuration file is malformed TOML
/// - Required fields are missing from configuration
pub fn load_config() -> Result<Config> {
    let env = Environment::detect();
    let config_filename = env.config_filename();

    // Start from CARGO_MANIFEST_DIR and search upward for the config file
    let mut current_dir =
        PathBuf::from(env::var("CARGO_MANIFEST_DIR").context("CARGO_MANIFEST_DIR not set")?);

    // Search up the directory tree for the config file (max 5 levels up)
    let mut found_path = None;
    for _ in 0..5 {
        let config_path = current_dir.join(config_filename);
        if config_path.exists() {
            found_path = Some(config_path);
            break;
        }
        // Go up one directory
        if !current_dir.pop() {
            break;
        }
    }

    let config_path = found_path.ok_or_else(|| {
        anyhow!(
            "Configuration file not found: {}\nSearched from: {}\nEnvironment: {:?}",
            config_filename,
            env::var("CARGO_MANIFEST_DIR").unwrap_or_default(),
            env
        )
    })?;

    let config_content = fs::read_to_string(&config_path)
        .with_context(|| format!("Failed to read config file: {}", config_path.display()))?;

    let config: Config =
        toml::from_str(&config_content).context("Failed to parse configuration file as TOML")?;

    Ok(config)
}

/// Get the i18n directory path from configuration
///
/// Resolves the i18n path relative to the project root if it's relative,
/// or returns it as-is if it's absolute.
pub fn get_i18n_dir(config: &Config) -> PathBuf {
    let i18n_path = Path::new(&config.site.i18n_path);
    if i18n_path.is_absolute() {
        i18n_path.to_path_buf()
    } else {
        Path::new(&config.root_path).join(i18n_path)
    }
}

/// Get the content directory path from configuration
pub fn get_content_dir(config: &Config) -> PathBuf {
    let content_path = Path::new(&config.site.content_path);
    if content_path.is_absolute() {
        content_path.to_path_buf()
    } else {
        Path::new(&config.root_path).join(content_path)
    }
}

/// Get the config directory path from configuration
pub fn get_config_dir(config: &Config) -> PathBuf {
    let config_path = Path::new(&config.site.config_path);
    if config_path.is_absolute() {
        config_path.to_path_buf()
    } else {
        Path::new(&config.root_path).join(config_path)
    }
}

/// Get the UI directory path from configuration
pub fn get_ui_dir(config: &Config) -> PathBuf {
    let ui_path = Path::new(&config.site.ui_path);
    if ui_path.is_absolute() {
        ui_path.to_path_buf()
    } else {
        Path::new(&config.root_path).join(ui_path)
    }
}

/// Get the assets directory path from configuration
pub fn get_assets_dir(config: &Config) -> PathBuf {
    let assets_path = Path::new(&config.site.assets_path);
    if assets_path.is_absolute() {
        assets_path.to_path_buf()
    } else {
        Path::new(&config.root_path).join(assets_path)
    }
}

/// Load all FTL files from the i18n directory
///
/// Loads FTL files from both direct location (`site/i18n/*.ftl`)
/// and nested structure (`site/i18n/locales/{language}/*.ftl`)
///
/// File naming convention: `{filename}` or `{language}_{filename}` for language-specific files
pub fn load_ftl_files(config: &Config) -> Result<HashMap<String, String>> {
    let mut ftl_files = HashMap::new();
    let i18n_dir = get_i18n_dir(config);

    if !i18n_dir.exists() {
        return Ok(ftl_files); // No FTL files is not an error
    }

    // Load from direct FTL files in i18n directory (no language prefix)
    load_ftl_files_from_dir(&i18n_dir, &mut ftl_files, None)?;

    // Load from locales subdirectories with language prefix
    let locales_dir = i18n_dir.join("locales");
    if locales_dir.exists() {
        for entry in fs::read_dir(&locales_dir)? {
            let entry = entry?;
            let path = entry.path();
            if path.is_dir() {
                // Use directory name as language code
                if let Some(lang) = path.file_name().and_then(|n| n.to_str()) {
                    load_ftl_files_from_dir(&path, &mut ftl_files, Some(lang.to_string()))?;
                }
            }
        }
    }

    Ok(ftl_files)
}

/// Helper function to load FTL files from a specific directory (recursively)
///
/// If language is provided, prepends it to filenames as `{language}_{filename}`
/// This prevents collisions when the same filename exists in multiple language directories
fn load_ftl_files_from_dir(
    dir: &Path,
    ftl_files: &mut HashMap<String, String>,
    language: Option<String>,
) -> Result<()> {
    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();

        if path.is_dir() {
            // Recursively load from subdirectories with same language prefix
            load_ftl_files_from_dir(&path, ftl_files, language.clone())?;
        } else if path.extension().is_some_and(|ext| ext == "ftl") {
            // Load FTL files
            if let Some(filename) = path.file_stem().and_then(|s| s.to_str()) {
                let content = fs::read_to_string(&path)?;
                // Construct key with language prefix if provided
                let key = if let Some(lang) = &language {
                    format!("{}_{}", lang, filename)
                } else {
                    filename.to_string()
                };
                ftl_files.insert(key, content);
            }
        }
    }
    Ok(())
}

/// Load all TOML files from a specified directory
///
/// Useful for loading menus, themes, and other configuration files
pub fn load_toml_files_from_dir(dir: &Path) -> Result<HashMap<String, String>> {
    let mut files = HashMap::new();

    if !dir.exists() {
        return Ok(files); // Missing directory is not an error
    }

    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();
        if path.extension().is_some_and(|ext| ext == "toml") {
            if let Some(filename) = path.file_stem().and_then(|s| s.to_str()) {
                let content = fs::read_to_string(&path)?;
                files.insert(filename.to_string(), content);
            }
        }
    }

    Ok(files)
}

/// Build cache management for smart incremental builds
#[derive(Debug)]
pub struct BuildCache {
    cache_dir: PathBuf,
}

impl BuildCache {
    /// Create a new build cache
    ///
    /// Creates the cache directory if it doesn't exist
    pub fn new(cache_dir: PathBuf) -> Result<Self> {
        fs::create_dir_all(&cache_dir)?;
        Ok(Self { cache_dir })
    }

    /// Compute SHA256 hash of files in a directory
    ///
    /// Useful for detecting when files have changed and need rebuilding
    pub fn compute_hash(&self, dir: &Path) -> Result<String> {
        let mut hasher = Sha256::new();

        if !dir.exists() {
            let hash = hasher.finalize();
            return Ok(hash.iter().fold(String::new(), |mut s, b| {
                use std::fmt::Write;
                let _ = write!(s, "{b:02x}");
                s
            })[..16]
                .to_string());
        }

        let mut files: Vec<_> = fs::read_dir(dir)?
            .filter_map(|entry| entry.ok())
            .filter(|entry| {
                entry
                    .path()
                    .extension()
                    .is_some_and(|ext| ext == "toml" || ext == "ftl")
            })
            .collect();

        files.sort_by_key(|entry| entry.path());

        for entry in files {
            let path = entry.path();
            let content = fs::read_to_string(&path)?;
            hasher.update(path.to_string_lossy().as_bytes());
            hasher.update(content.as_bytes());
        }

        let hash = hasher.finalize();
        Ok(hash.iter().fold(String::new(), |mut s, b| {
            use std::fmt::Write;
            let _ = write!(s, "{b:02x}");
            s
        })[..16]
            .to_string())
    }

    /// Check if a cache entry exists
    pub fn is_cached(&self, cache_key: &str, cache_type: &str) -> bool {
        let cache_file = self
            .cache_dir
            .join(format!("{}_{}.cache", cache_type, cache_key));
        cache_file.exists()
    }

    /// Save a cache entry
    pub fn save_cache(&self, cache_key: &str, cache_type: &str, content: &str) -> Result<()> {
        let cache_file = self
            .cache_dir
            .join(format!("{}_{}.cache", cache_type, cache_key));
        fs::write(cache_file, content)?;
        Ok(())
    }

    /// Load a cache entry
    pub fn load_cache(&self, cache_key: &str, cache_type: &str) -> Result<String> {
        let cache_file = self
            .cache_dir
            .join(format!("{}_{}.cache", cache_type, cache_key));
        Ok(fs::read_to_string(cache_file)?)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_environment_detection() {
        // Development by default (in debug builds)
        let env = Environment::detect();
        assert_eq!(env.config_filename(), "config.dev.toml");
    }

    #[test]
    fn test_environment_config_filenames() {
        assert_eq!(
            Environment::Development.config_filename(),
            "config.dev.toml"
        );
        assert_eq!(Environment::Production.config_filename(), "config.toml");
    }
}
