//! Resource discovery and registry generation

#[allow(unused_imports)] // Used in generated code via string literals
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, PathBuf};

/// Embed routes.toml content for both SSR and client builds
/// Discovers all route files in config/routes/ directory and combines them
pub fn embed_routes_config(content_root: &str, out_dir: &str) -> Result<(), String> {
    // Get project root more reliably using CARGO_MANIFEST_DIR
    let project_root = if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
        // Go up two levels from crates/shared to get project root
        Path::new(&manifest_dir)
            .parent()
            .and_then(|p| p.parent())
            .unwrap_or_else(|| Path::new("."))
            .to_path_buf()
    } else {
        // Fallback: try to detect from content_root
        if content_root.ends_with("/site/content") {
            PathBuf::from(content_root.trim_end_matches("/site/content"))
        } else if content_root.ends_with("site/content") {
            PathBuf::from(content_root.trim_end_matches("site/content"))
        } else {
            PathBuf::from(".")
        }
    };

    // Use SITE_CONFIG_PATH environment variable or default
    let site_config_path =
        std::env::var("SITE_CONFIG_PATH").unwrap_or_else(|_| "site/config".to_string());

    let config_dir = if Path::new(&site_config_path).is_absolute() {
        PathBuf::from(&site_config_path)
    } else {
        project_root.join(&site_config_path)
    };

    let routes_dir = config_dir.join("routes");
    let fallback_routes_file = config_dir.join("routes.toml");

    // Set up rerun triggers for the routes directory
    if routes_dir.exists() {
        println!("cargo:rerun-if-changed={}", routes_dir.to_string_lossy());

        // Also watch individual route files
        if let Ok(entries) = fs::read_dir(&routes_dir) {
            for entry in entries.flatten() {
                let path = entry.path();
                if path.is_file() && path.extension().is_some_and(|ext| ext == "toml") {
                    println!("cargo:rerun-if-changed={}", path.to_string_lossy());
                }
            }
        }
    }

    // Also watch for fallback routes.toml
    println!(
        "cargo:rerun-if-changed={}",
        fallback_routes_file.to_string_lossy()
    );

    let mut combined_routes_content = String::new();
    let mut found_route_files = false;

    // Try to discover and combine route files from content_root/routes/ directory
    if routes_dir.exists() {
        if let Ok(entries) = fs::read_dir(&routes_dir) {
            let mut route_files: Vec<_> = entries
                .flatten()
                .filter(|entry| {
                    let path = entry.path();
                    path.is_file() && path.extension().is_some_and(|ext| ext == "toml")
                })
                .collect();

            // Sort by filename for consistent order
            route_files.sort_by_key(|a| a.file_name());

            for entry in route_files {
                let path = entry.path();
                if let Ok(content) = fs::read_to_string(&path) {
                    // Add a comment header for each file
                    if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
                        combined_routes_content.push_str(&format!(
                            "# {} Configuration\n",
                            filename.trim_end_matches(".toml").to_uppercase()
                        ));
                    }
                    combined_routes_content.push_str(&content);
                    combined_routes_content.push('\n');
                    found_route_files = true;
                }
            }
        }
    }

    // Fallback to single routes.toml file if no individual route files found
    if !found_route_files {
        if let Ok(content) = fs::read_to_string(&fallback_routes_file) {
            combined_routes_content = content;
            found_route_files = true;
        }
    }

    // Final fallback to minimal default config
    if !found_route_files {
        combined_routes_content = r#"
# Default Fallback Routes Configuration
[[routes]]
path = "/"
component = "Home"
page_component = "HomePage"
title_key = "home-page-title"
description_key = "home-page-description"
language = "en"
enabled = true
priority = 1.0
menu_group = "main"
menu_order = 1
menu_icon = "home"
show_in_sitemap = true
keywords = ["home", "rust", "developer"]
canonical_path = "/"
"#
        .to_string();
        eprintln!(
            "Warning: No route files found in {}/routes/ or {}, using minimal fallback \
             configuration",
            config_dir.display(),
            fallback_routes_file.display()
        );
    }

    // Write the combined routes content to a file that can be included with
    // include_str!
    let dest_path = Path::new(out_dir).join("embedded_routes.toml");
    fs::write(&dest_path, &combined_routes_content)
        .map_err(|e| format!("Failed to write embedded routes: {e}"))?;

    if found_route_files {
        let short = config_dir
            .strip_prefix(&project_root)
            .unwrap_or(&config_dir);
        println!(
            "cargo:warning=Successfully embedded route configurations from {}/routes/",
            short.display()
        );
    }

    Ok(())
}

/// Generate resource registry with automatic discovery
pub fn generate_resource_registry(content_root: &str, out_dir: &str) -> Result<(), String> {
    let content_dir = Path::new(content_root);
    let output_path = Path::new(out_dir).join("resource_registry.rs");

    let mut file =
        File::create(&output_path).map_err(|e| format!("Failed to create output file: {e}"))?;

    writeln!(file, "// Auto-generated resource registry")
        .map_err(|e| format!("Failed to write to file: {e}"))?;
    writeln!(file, "// Contains ALL resources discovered at build time")
        .map_err(|e| format!("Failed to write to file: {e}"))?;
    writeln!(file, "// No hardcoded paths needed!")
        .map_err(|e| format!("Failed to write to file: {e}"))?;
    writeln!(file).map_err(|e| format!("Failed to write to file: {e}"))?;

    writeln!(file, "use std::collections::HashMap;")
        .map_err(|e| format!("Failed to write to file: {e}"))?;
    writeln!(file, "use once_cell::sync::Lazy;")
        .map_err(|e| format!("Failed to write to file: {e}"))?;
    writeln!(file).map_err(|e| format!("Failed to write to file: {e}"))?;

    if content_dir.exists() {
        generate_resource_maps(content_dir, &mut file)
            .map_err(|e| format!("Failed to generate resource maps: {e}"))?;
    } else {
        // Generate empty maps if content directory doesn't exist
        generate_empty_maps(&mut file)
            .map_err(|e| format!("Failed to generate empty maps: {e}"))?;
    }

    Ok(())
}

fn generate_resource_maps(content_dir: &Path, file: &mut File) -> Result<(), String> {
    // Get project root for UI resources
    let project_root = if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
        Path::new(&manifest_dir)
            .parent()
            .and_then(|p| p.parent())
            .unwrap_or_else(|| Path::new("."))
            .to_path_buf()
    } else {
        Path::new(".").to_path_buf()
    };

    // Get UI path from environment variables
    let ui_path = std::env::var("SITE_UI_PATH").unwrap_or_else(|_| {
        let site_root = std::env::var("SITE_ROOT_PATH").unwrap_or_else(|_| "site".to_string());
        format!("{}/ui", site_root)
    });
    let ui_dir = project_root.join(&ui_path);

    // Get themes path (in config/themes, not content)
    let config_path = std::env::var("SITE_CONFIG_PATH").unwrap_or_else(|_| {
        let site_root = std::env::var("SITE_ROOT_PATH").unwrap_or_else(|_| "site".to_string());
        format!("{}/config", site_root)
    });
    let config_dir = project_root.join(&config_path);

    // Discover all resource types from correct locations
    let menus = discover_resources(&ui_dir, "menus");
    let footers = discover_resources(&ui_dir, "footer");
    let themes = discover_resources(&config_dir, "themes");
    let ftl_files = discover_ftl_resources(content_dir);
    let content_json = discover_content_json_resources(content_dir);

    // Generate menu resources
    writeln!(
        file,
        "pub static MENU_RESOURCES: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {{"
    )
    .map_err(|e| format!("Failed to write MENU_RESOURCES header: {e}"))?;
    // Only make the map mutable if we have entries to insert
    if menus.is_empty() {
        writeln!(file, "    let map = HashMap::new();")
            .map_err(|e| format!("Failed to write MENU_RESOURCES map init: {e}"))?;
    } else {
        writeln!(file, "    let mut map = HashMap::new();")
            .map_err(|e| format!("Failed to write MENU_RESOURCES map init: {e}"))?;
    }

    for (name, path) in &menus {
        // Use absolute path from root_path instead of fragile relative paths
        let absolute_path = path.to_string_lossy().replace('\\', "/");
        writeln!(
            file,
            "    map.insert(\"{name}\", include_str!(\"{absolute_path}\"));"
        )
        .map_err(|e| format!("Failed to write menu resource {name}: {e}"))?;
    }

    writeln!(file, "    map").map_err(|e| format!("Failed to write MENU_RESOURCES return: {e}"))?;
    writeln!(file, "}});").map_err(|e| format!("Failed to write MENU_RESOURCES close: {e}"))?;
    writeln!(file).map_err(|e| format!("Failed to write newline after MENU_RESOURCES: {e}"))?;

    // Generate footer resources
    writeln!(
        file,
        "pub static FOOTER_RESOURCES: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {{"
    )
    .unwrap();
    // Only make the map mutable if we have entries to insert
    if footers.is_empty() {
        writeln!(file, "    let map = HashMap::new();").unwrap();
    } else {
        writeln!(file, "    let mut map = HashMap::new();").unwrap();
    }

    for (name, path) in &footers {
        let absolute_path = path.to_string_lossy().replace('\\', "/");
        writeln!(
            file,
            "    map.insert(\"{name}\", include_str!(\"{absolute_path}\"));"
        )
        .unwrap();
    }

    writeln!(file, "    map").unwrap();
    writeln!(file, "}});").unwrap();
    writeln!(file).unwrap();

    // Generate theme resources
    writeln!(
        file,
        "pub static THEME_RESOURCES: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {{"
    )
    .unwrap();
    // Only make the map mutable if we have entries to insert
    if themes.is_empty() {
        writeln!(file, "    let map = HashMap::new();").unwrap();
    } else {
        writeln!(file, "    let mut map = HashMap::new();").unwrap();
    }

    for (name, path) in &themes {
        let absolute_path = path.to_string_lossy().replace('\\', "/");
        writeln!(
            file,
            "    map.insert(\"{name}\", include_str!(\"{absolute_path}\"));"
        )
        .unwrap();
    }

    writeln!(file, "    map").unwrap();
    writeln!(file, "}});").unwrap();
    writeln!(file).unwrap();

    // Generate FTL resources
    writeln!(
        file,
        "pub static FTL_RESOURCES: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {{"
    )
    .unwrap();
    // Only make the map mutable if we have entries to insert
    if ftl_files.is_empty() {
        writeln!(file, "    let map = HashMap::new();").unwrap();
    } else {
        writeln!(file, "    let mut map = HashMap::new();").unwrap();
    }

    for (name, path) in &ftl_files {
        let absolute_path = path.to_string_lossy().replace('\\', "/");
        writeln!(
            file,
            "    map.insert(\"{name}\", include_str!(\"{absolute_path}\"));"
        )
        .unwrap();
    }

    writeln!(file, "    map").unwrap();
    writeln!(file, "}});").unwrap();
    writeln!(file).unwrap();

    // Generate content JSON resources
    writeln!(
        file,
        "pub static CONTENT_RESOURCES: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {{"
    )
    .unwrap();
    // Only make the map mutable if we have entries to insert
    if content_json.is_empty() {
        writeln!(file, "    let map = HashMap::new();").unwrap();
    } else {
        writeln!(file, "    let mut map = HashMap::new();").unwrap();
    }

    for (name, path) in &content_json {
        let absolute_path = path.to_string_lossy().replace('\\', "/");
        writeln!(
            file,
            "    map.insert(\"{name}\", include_str!(\"{absolute_path}\"));"
        )
        .unwrap();
    }

    writeln!(file, "    map").unwrap();
    writeln!(file, "}});").unwrap();
    writeln!(file).unwrap();

    // Generate discovery functions
    generate_discovery_functions(file, &menus, &footers, &themes, &ftl_files, &content_json)
        .map_err(|e| format!("Failed to generate discovery functions: {e}"))?;

    Ok(())
}

fn discover_resources(content_dir: &Path, subdir: &str) -> Vec<(String, PathBuf)> {
    let mut resources = Vec::new();
    let resource_dir = content_dir.join(subdir);

    if resource_dir.exists() {
        if let Ok(entries) = fs::read_dir(&resource_dir) {
            for entry in entries.flatten() {
                let path = entry.path();
                if path.is_file() {
                    if let Some(extension) = path.extension() {
                        if extension == "toml" {
                            if let Some(stem) = path.file_stem() {
                                if let Some(name) = stem.to_str() {
                                    resources.push((name.to_string(), path));
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    resources
}

fn discover_ftl_resources(_content_dir: &Path) -> Vec<(String, PathBuf)> {
    let mut resources = Vec::new();

    // Get project root for i18n resources
    let project_root = if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
        Path::new(&manifest_dir)
            .parent()
            .and_then(|p| p.parent())
            .unwrap_or_else(|| Path::new("."))
            .to_path_buf()
    } else {
        Path::new(".").to_path_buf()
    };

    // Get i18n/locales path from environment variables
    let i18n_path = std::env::var("SITE_I18N_PATH").unwrap_or_else(|_| {
        let site_root = std::env::var("SITE_ROOT_PATH").unwrap_or_else(|_| "site".to_string());
        format!("{}/i18n", site_root)
    });
    let locales_dir = project_root.join(&i18n_path).join("locales");

    if locales_dir.exists() {
        // Walk through language directories (en, es, etc.)
        if let Ok(lang_entries) = fs::read_dir(&locales_dir) {
            for lang_entry in lang_entries.flatten() {
                let lang_path = lang_entry.path();
                if lang_path.is_dir() {
                    if let Some(lang_name) = lang_path.file_name().and_then(|n| n.to_str()) {
                        collect_ftl_files(&lang_path, lang_name, &mut resources);
                    }
                }
            }
        }
    }

    resources
}

fn collect_ftl_files(dir: &Path, lang: &str, resources: &mut Vec<(String, PathBuf)>) {
    collect_ftl_files_with_prefix(dir, lang, "", resources);
}

fn collect_ftl_files_with_prefix(
    dir: &Path,
    lang: &str,
    prefix: &str,
    resources: &mut Vec<(String, PathBuf)>,
) {
    if let Ok(entries) = fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_file() {
                if let Some(extension) = path.extension() {
                    if extension == "ftl" {
                        if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
                            let key = if prefix.is_empty() {
                                format!("{lang}_{stem}")
                            } else {
                                format!("{lang}_{prefix}_{stem}")
                            };
                            resources.push((key, path));
                        }
                    }
                }
            } else if path.is_dir() {
                // Recursively collect FTL files from subdirectories
                if let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) {
                    let new_prefix = if prefix.is_empty() {
                        dir_name.to_string()
                    } else {
                        format!("{prefix}_{dir_name}")
                    };
                    collect_ftl_files_with_prefix(&path, lang, &new_prefix, resources);
                }
            }
        }
    }
}

fn discover_content_json_resources(content_dir: &Path) -> Vec<(String, PathBuf)> {
    let mut resources = Vec::new();

    // Build script runs from crate directory, need to find workspace root
    let current_dir = std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf());

    // Find workspace root by looking for Cargo.toml with [workspace]
    let workspace_root = find_workspace_root(&current_dir).unwrap_or(current_dir);

    let processed_content_root = {
        let public_path =
            std::env::var("SITE_PUBLIC_PATH").unwrap_or_else(|_| "public".to_string());
        let server_root_content =
            std::env::var("SITE_SERVER_ROOT_CONTENT").unwrap_or_else(|_| "r".to_string());
        format!("{}/{}", public_path, server_root_content)
    };

    // Make path relative to workspace root
    let processed_content_dir = workspace_root.join(&processed_content_root);

    if !processed_content_dir.exists() {
        println!(
            "cargo:warning=DEBUG: Processed content directory {} does not exist - content \
             resources will be empty",
            processed_content_dir.display()
        );
        return resources;
    }

    // Dynamically discover content types from content-kinds.toml (use original
    // content_dir for config)
    let content_types = discover_available_content_types(content_dir);

    for content_type in content_types {
        let content_type_dir = processed_content_dir.join(&content_type);
        if content_type_dir.exists() {
            collect_json_files(&content_type_dir, &content_type, &mut resources);
            collect_markdown_files(&content_type_dir, &content_type, &mut resources);
        }
    }

    resources
}

/// Discover available content types from content-kinds.toml
fn discover_available_content_types(content_dir: &Path) -> Vec<String> {
    let config_path = content_dir.join("content-kinds.toml");

    if !config_path.exists() {
        // Fallback to directory-based discovery if config is missing
        return discover_content_types_from_directories(content_dir);
    }

    match std::fs::read_to_string(&config_path) {
        Ok(config_content) => {
            match toml::from_str::<crate::build::build_tasks::content_types::ContentKindsConfig>(
                &config_content,
            ) {
                Ok(config) => config
                    .content_kinds
                    .into_iter()
                    .filter(|kind| kind.enabled)
                    .map(|kind| kind.directory)
                    .collect(),
                Err(e) => {
                    eprintln!("Warning: Failed to parse content-kinds.toml: {}", e);
                    discover_content_types_from_directories(content_dir)
                }
            }
        }
        Err(e) => {
            eprintln!("Warning: Could not read content-kinds.toml: {}", e);
            discover_content_types_from_directories(content_dir)
        }
    }
}

/// Fallback: discover content types by scanning directories
fn discover_content_types_from_directories(content_dir: &Path) -> Vec<String> {
    let mut content_types = Vec::new();

    if let Ok(entries) = std::fs::read_dir(content_dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_dir() {
                if let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) {
                    // Skip known non-content directories
                    if !["locales", "themes", "menus", "footer", "routes", "tmp"]
                        .contains(&dir_name)
                    {
                        content_types.push(dir_name.to_string());
                    }
                }
            }
        }
    }

    content_types.sort();
    content_types
}

fn collect_json_files(dir: &Path, content_type: &str, resources: &mut Vec<(String, PathBuf)>) {
    if let Ok(entries) = fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_dir() {
                // Check for language directories (en, es, etc.)
                if let Some(lang_name) = path.file_name().and_then(|n| n.to_str()) {
                    let index_file = path.join("index.json");
                    if index_file.exists() {
                        let key = format!("{content_type}_{lang_name}_index");
                        resources.push((key, index_file));
                    }
                }
            }
        }
    }
}

fn collect_markdown_files(dir: &Path, content_type: &str, resources: &mut Vec<(String, PathBuf)>) {
    if let Ok(entries) = fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_dir() {
                // Check for language directories (en, es, etc.)
                if let Some(lang_name) = path.file_name().and_then(|n| n.to_str()) {
                    // Collect all markdown files in this language directory
                    if let Ok(md_entries) = fs::read_dir(&path) {
                        for md_entry in md_entries.flatten() {
                            let md_path = md_entry.path();
                            if md_path.is_file() {
                                if let Some(extension) = md_path.extension() {
                                    if extension == "md" {
                                        if let Some(stem) =
                                            md_path.file_stem().and_then(|s| s.to_str())
                                        {
                                            let key = format!("{content_type}_{lang_name}_{stem}");
                                            resources.push((key, md_path));
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

fn generate_empty_maps(file: &mut File) -> Result<(), String> {
    writeln!(
        file,
        "pub static MENU_RESOURCES: Lazy<HashMap<&'static str, &'static str>> = \
         Lazy::new(HashMap::new);"
    )
    .map_err(|e| format!("Failed to write empty MENU_RESOURCES: {e}"))?;
    writeln!(
        file,
        "pub static FOOTER_RESOURCES: Lazy<HashMap<&'static str, &'static str>> = \
         Lazy::new(HashMap::new);"
    )
    .map_err(|e| format!("Failed to write empty FOOTER_RESOURCES: {e}"))?;
    writeln!(
        file,
        "pub static THEME_RESOURCES: Lazy<HashMap<&'static str, &'static str>> = \
         Lazy::new(HashMap::new);"
    )
    .map_err(|e| format!("Failed to write empty THEME_RESOURCES: {e}"))?;
    writeln!(
        file,
        "pub static FTL_RESOURCES: Lazy<HashMap<&'static str, &'static str>> = \
         Lazy::new(HashMap::new);"
    )
    .map_err(|e| format!("Failed to write empty FTL_RESOURCES: {e}"))?;
    writeln!(
        file,
        "pub static CONTENT_RESOURCES: Lazy<HashMap<&'static str, &'static str>> = \
         Lazy::new(HashMap::new);"
    )
    .map_err(|e| format!("Failed to write empty CONTENT_RESOURCES: {e}"))?;
    writeln!(file).map_err(|e| format!("Failed to write newline: {e}"))?;

    // Generate empty discovery functions
    writeln!(
        file,
        "pub fn available_menus() -> Vec<&'static str> {{ vec![] }}"
    )
    .map_err(|e| format!("Failed to write available_menus: {e}"))?;
    writeln!(
        file,
        "pub fn available_footers() -> Vec<&'static str> {{ vec![] }}"
    )
    .map_err(|e| format!("Failed to write available_footers: {e}"))?;
    writeln!(
        file,
        "pub fn available_themes() -> Vec<&'static str> {{ vec![] }}"
    )
    .map_err(|e| format!("Failed to write available_themes: {e}"))?;
    writeln!(
        file,
        "pub fn available_ftl_resources() -> Vec<&'static str> {{ vec![] }}"
    )
    .map_err(|e| format!("Failed to write available_ftl_resources: {e}"))?;
    writeln!(
        file,
        "pub fn available_content_resources() -> Vec<&'static str> {{ vec![] }}"
    )
    .map_err(|e| format!("Failed to write available_content_resources: {e}"))?;

    Ok(())
}

/// Find workspace root by looking for Cargo.toml with [workspace] section
fn find_workspace_root(start_dir: &Path) -> Option<PathBuf> {
    let mut current_dir = start_dir;

    loop {
        let cargo_toml = current_dir.join("Cargo.toml");

        if cargo_toml.exists() {
            // Check if this Cargo.toml contains [workspace]
            if let Ok(cargo_content) = std::fs::read_to_string(&cargo_toml) {
                if cargo_content.contains("[workspace]") {
                    return Some(current_dir.to_path_buf());
                }
            }
        }

        // Go up one directory
        if let Some(parent) = current_dir.parent() {
            current_dir = parent;
        } else {
            break;
        }
    }

    None
}

fn generate_discovery_functions(
    file: &mut File,
    menus: &[(String, PathBuf)],
    footers: &[(String, PathBuf)],
    themes: &[(String, PathBuf)],
    ftl_files: &[(String, PathBuf)],
    content_json: &[(String, PathBuf)],
) -> Result<(), String> {
    // Generate available_menus function
    writeln!(file, "pub fn available_menus() -> Vec<&'static str> {{").unwrap();
    writeln!(file, "    vec![").unwrap();
    for (name, _) in menus {
        writeln!(file, "        \"{name}\",").unwrap();
    }
    writeln!(file, "    ]").unwrap();
    writeln!(file, "}}").unwrap();
    writeln!(file).unwrap();

    // Generate available_footers function
    writeln!(file, "pub fn available_footers() -> Vec<&'static str> {{").unwrap();
    writeln!(file, "    vec![").unwrap();
    for (name, _) in footers {
        writeln!(file, "        \"{name}\",").unwrap();
    }
    writeln!(file, "    ]").unwrap();
    writeln!(file, "}}").unwrap();
    writeln!(file).unwrap();

    // Generate available_themes function
    writeln!(file, "pub fn available_themes() -> Vec<&'static str> {{").unwrap();
    writeln!(file, "    vec![").unwrap();
    for (name, _) in themes {
        writeln!(file, "        \"{name}\",").unwrap();
    }
    writeln!(file, "    ]").unwrap();
    writeln!(file, "}}").unwrap();
    writeln!(file).unwrap();

    // Generate available_ftl_resources function
    writeln!(
        file,
        "pub fn available_ftl_resources() -> Vec<&'static str> {{"
    )
    .unwrap();
    writeln!(file, "    vec![").unwrap();
    for (name, _) in ftl_files {
        writeln!(file, "        \"{name}\",").unwrap();
    }
    writeln!(file, "    ]").unwrap();
    writeln!(file, "}}").unwrap();
    writeln!(file).unwrap();

    // Generate available_content_resources function
    writeln!(
        file,
        "pub fn available_content_resources() -> Vec<&'static str> {{"
    )
    .unwrap();
    writeln!(file, "    vec![").unwrap();
    for (name, _) in content_json {
        writeln!(file, "        \"{name}\",").unwrap();
    }
    writeln!(file, "    ]").map_err(|e| format!("Failed to write content_resources close: {e}"))?;
    writeln!(file, "}}").map_err(|e| format!("Failed to write content_resources end: {e}"))?;

    Ok(())
}

/// Generate a `ResourceContributor` implementation from pre-loaded resource
/// maps.
///
/// Takes maps of `key → value` for each resource category and writes a Rust
/// source file (`resource_contributor.rs`) to `out_dir`.  All string values are
/// embedded literally (not via `include_str!`) so the caller controls how the
/// content is sourced.
///
/// # Arguments
/// * `struct_name` — Name of the generated struct, e.g.
///   `"WebsiteResourceContributor"`
/// * `menus`   — Per-language menu TOML strings (`"en" → toml`)
/// * `footers` — Per-language footer TOML strings
/// * `themes`  — Theme name → TOML strings
/// * `ftl`     — i18n key → FTL source text
/// * `routes`  — Per-language route TOML strings
/// * `out_dir` — Cargo `OUT_DIR` path
pub fn generate_resource_contributor_code(
    struct_name: &str,
    menus: &HashMap<String, String>,
    footers: &HashMap<String, String>,
    themes: &HashMap<String, String>,
    ftl: &HashMap<String, String>,
    routes: &HashMap<String, String>,
    out_dir: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
    let gen_method = |method: &str, entries: &HashMap<String, String>| -> String {
        let mut s = format!("    fn {method}(&self) -> HashMap<String, String> {{\n");
        if entries.is_empty() {
            s.push_str("        HashMap::new()\n    }\n\n");
        } else {
            s.push_str("        let mut map = HashMap::new();\n");
            for (k, v) in entries {
                let escaped = v.replace("\"#", "\"\\#");
                s.push_str(&format!(
                    "        map.insert(\"{k}\".to_string(), r#\"{escaped}\"#.to_string());\n"
                ));
            }
            s.push_str("        map\n    }\n\n");
        }
        s
    };

    let mut code = format!(
        "// Generated by rustelo_tools::build::generate_resource_contributor_code\n// DO NOT EDIT \
         — regenerated on every build\n\nuse std::collections::HashMap;\nuse \
         rustelo_core_lib::ResourceContributor;\n\npub struct {struct_name};\n\nimpl \
         ResourceContributor for {struct_name} {{\n"
    );

    code.push_str(&gen_method("contribute_menus", menus));
    code.push_str(&gen_method("contribute_footers", footers));
    code.push_str(&gen_method("contribute_themes", themes));
    code.push_str(&gen_method("contribute_ftl", ftl));
    code.push_str(&gen_method("contribute_routes", routes));
    code.push_str("    fn name(&self) -> &str {\n");
    code.push_str(&format!("        \"{struct_name}\"\n"));
    code.push_str("    }\n}\n");

    let out_file = out_dir.join("resource_contributor.rs");
    fs::write(&out_file, &code)
        .map_err(|e| format!("Failed to write resource_contributor.rs: {e}"))?;

    println!(
        "cargo:warning=Generated {struct_name}: {} menus, {} footers, {} themes, {} FTL, {} routes",
        menus.len(),
        footers.len(),
        themes.len(),
        ftl.len(),
        routes.len()
    );

    Ok(())
}

/// Generate `route_table.rs` — a compile-time BTreeMap from URL paths to
/// component names.
///
/// The caller is responsible for loading `routes` from whatever source (NCL,
/// TOML, etc.). This function only handles code generation; it writes
/// `{out_dir}/route_table.rs`.
pub fn generate_route_table(
    routes: &std::collections::BTreeMap<String, String>,
    out_dir: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut code = String::from(
        "// Auto-generated route table — do not edit directly.\n// Sourced from site \
         configuration at build time.\n\nuse std::collections::BTreeMap;\n\n/// Returns a static \
         reference to the path → component route table.\npub fn get_route_table() -> &'static \
         BTreeMap<&'static str, &'static str> {\nuse std::sync::OnceLock;\nstatic ROUTE_TABLE: \
         OnceLock<BTreeMap<&'static str, &'static str>> = \
         OnceLock::new();\nROUTE_TABLE.get_or_init(|| {\nlet mut map = BTreeMap::new();\n",
    );

    for (path, component) in routes {
        code.push_str(&format!(
            "        map.insert(\"{path}\", \"{component}\");\n"
        ));
    }

    code.push_str(
        "        map\n})\n}\n\nlazy_static::lazy_static! {\npub static ref ROUTE_TABLE: &'static \
         BTreeMap<&'static str, &'static str> = get_route_table();\n}\n",
    );

    let out_file = out_dir.join("route_table.rs");
    fs::write(&out_file, &code).map_err(|e| format!("Failed to write route_table.rs: {e}"))?;

    println!(
        "cargo:warning=Generated route_table.rs ({} entries)",
        routes.len()
    );
    Ok(())
}

/// Generate `server_routing.rs` — SSR dispatch function wired to all discovered
/// page components.
///
/// **Caller responsibilities** (data loading, stays in `build.rs`):
/// - Discover `pages_cache_dir` from the manifest's `cache_build_path`.
/// - Populate `route_mappings` (path → base component name, without "Page"
///   suffix).
/// - Populate `path_to_content_type` (path → content-type key) from site
///   config.
///
/// **`crate_relative_pages_dir`**: path from the server crate's
/// `CARGO_MANIFEST_DIR` to the pages cache directory, used inside generated
/// `include!()` macros. Example: `"../../target/rustelo-cache/pages"`.
///
/// Writes `{out_dir}/server_routing.rs`.  Falls back to a minimal no-op
/// implementation when `pages_cache_dir` does not exist or contains no
/// `page_*.rs` files.
pub fn generate_server_routing(
    pages_cache_dir: &Path,
    crate_relative_pages_dir: &str,
    route_mappings: &HashMap<String, String>,
    path_to_content_type: &HashMap<String, String>,
    out_dir: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
    // Discover page_*.rs files
    let page_files: Vec<String> = if pages_cache_dir.exists() {
        let mut names = Vec::new();
        for entry in fs::read_dir(pages_cache_dir)? {
            let entry = entry?;
            let path = entry.path();
            if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
                if let Some(component) = name
                    .strip_prefix("page_")
                    .and_then(|s| s.strip_suffix(".rs"))
                {
                    names.push(component.to_string());
                }
            }
        }
        names
    } else {
        Vec::new()
    };

    if page_files.is_empty() {
        return generate_server_routing_fallback(out_dir);
    }

    // server_routing.rs — route lookup only (no Leptos, no page component deps).
    // Always generated; safe to include in htmx-only builds.
    let mut code = String::from(
        "// Auto-generated server routing (core) — do not edit directly.\n// Sources: site \
         configuration.\n\n",
    );

    // get_component_for_path — OnceLock BTreeMap with parametric fallback
    code.push_str(
        "\n/// Resolve a URL path to its base component name (without \"Page\" suffix).\nfn \
         get_component_for_path(path: &str) -> &'static str {\nuse \
         std::collections::BTreeMap;\nuse std::sync::OnceLock;\nstatic ROUTES: \
         OnceLock<BTreeMap<&'static str, &'static str>> = OnceLock::new();\nlet routes = \
         ROUTES.get_or_init(|| {\nlet mut m = BTreeMap::new();\n",
    );
    for (route_path, component) in route_mappings {
        let base = component.strip_suffix("Page").unwrap_or(component);
        code.push_str(&format!(
            "        m.insert(\"{route_path}\", \"{base}\");\n"
        ));
    }
    code.push_str(
        "        m\n});\nif let Some(&comp) = routes.get(path) { return comp; }\nlet path_segs: \
         Vec<&str> = path.split('/').collect();\nfor (pattern, comp) in routes.iter() {\nif \
         !pattern.contains('{') { continue; }\nlet pat_segs: Vec<&str> = \
         pattern.split('/').collect();\nif pat_segs.len() != path_segs.len() { continue; }\nif \
         pat_segs.iter().zip(path_segs.iter()).all(|(p, s)| (p.starts_with('{') && \
         p.ends_with('}')) || p == s) {\nreturn comp;\n}\n}\n\"NotFound\"\n}\n\n",
    );

    // get_content_type_for_path — OnceLock BTreeMap with parametric fallback
    code.push_str(
        "/// Returns the content-type key for a URL path, if one was declared in site \
         config.\npub fn get_content_type_for_path(path: &str) -> Option<&'static str> {\nuse \
         std::collections::BTreeMap;\nuse std::sync::OnceLock;\nstatic CT: \
         OnceLock<BTreeMap<&'static str, &'static str>> = OnceLock::new();\nlet ct_map = \
         CT.get_or_init(|| {\nlet mut m = BTreeMap::new();\n",
    );
    for (route_path, ct) in path_to_content_type {
        code.push_str(&format!("        m.insert(\"{route_path}\", \"{ct}\");\n"));
    }
    code.push_str(
        "        m\n});\nif let Some(&ct) = ct_map.get(path) { return Some(ct); }\nlet path_segs: \
         Vec<&str> = path.split('/').collect();\nfor (pattern, ct) in ct_map.iter() {\nif \
         !pattern.contains('{') { continue; }\nlet pat_segs: Vec<&str> = \
         pattern.split('/').collect();\nif pat_segs.len() != path_segs.len() { continue; }\nif \
         pat_segs.iter().zip(path_segs.iter()).all(|(p, s)| (p.starts_with('{') && \
         p.ends_with('}')) || p == s) {\nreturn Some(ct);\n}\n}\nNone\n}\n\n",
    );

    // Write core routing file (no Leptos deps)
    let core_out = out_dir.join("server_routing.rs");
    fs::write(&core_out, &code).map_err(|e| format!("Failed to write server_routing.rs: {e}"))?;

    // server_routing_leptos.rs — Leptos page dispatch (render_website_page_ssr).
    // Only included in wasm-client builds; safe to omit in htmx-only builds.
    let mut leptos_code = String::from(
        "// Auto-generated Leptos page dispatch — do not edit directly.\n// Gate this file behind \
         #[cfg(feature = \"wasm-client\")] in lib.rs.\n\n#[allow(unused_imports)]\nuse \
         leptos::prelude::*;\n\n// Include all generated page wrapper components from cache\n",
    );
    for component in &page_files {
        leptos_code.push_str(&format!(
            "include!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \
             \"/{crate_relative_pages_dir}/page_{component}.rs\"));\n"
        ));
    }
    leptos_code.push('\n');

    // render_website_page_ssr dispatch
    leptos_code.push_str(
        "/// Render website page for SSR using configuration-driven routing.\npub fn \
         render_website_page_ssr(path: &str, language: &str) -> AnyView {\nlet lang = \
         language.to_string();\nlet component = get_component_for_path(path);\nlet content_type = \
         get_content_type_for_path(path).unwrap_or(\"blog\").to_string();\nmatch component {\n",
    );

    for component in &page_files {
        let arm = match component.as_str() {
            "ContentCategory" => format!(
                "        \"{component}\" => view! {{ <{component}Page _language=lang.clone() \
                 category=\"default\".to_string() content_type=content_type.clone() /> \
                 }}.into_any(),\n"
            ),
            "ContentIndex" => format!(
                "        \"{component}\" => view! {{ <{component}Page _language=lang.clone() \
                 content_type=content_type.clone() /> }}.into_any(),\n"
            ),
            "PostViewer" => format!(
                "        \"{component}\" => {{\nlet segs: Vec<&str> = path.split('/').filter(|s| \
                 !s.is_empty()).collect();\nlet slug = \
                 segs.last().copied().unwrap_or(\"\").to_string();\nview! {{ <{component}Page \
                 _language=lang.clone() slug=slug content_type=content_type.clone() /> \
                 }}.into_any()\n}},\n"
            ),
            _ => format!(
                "        \"{component}\" => view! {{ <{component}Page _language=lang.clone() /> \
                 }}.into_any(),\n"
            ),
        };
        leptos_code.push_str(&arm);
    }

    let not_found_arm = if page_files.iter().any(|n| n == "NotFound") {
        "        _ => {\nlet translator = rustelo_core_lib::SsrTranslator::new(lang.clone());\nlet \
         lang_content = rustelo_core_lib::build_page_content_patterns(&translator, \
         &[\"not-found-\"]);\nview! { <NotFoundPage _language=lang lang_content=lang_content /> \
         }.into_any()\n},\n"
    } else {
        "        _ => {\nlet translator = rustelo_core_lib::SsrTranslator::new(lang.clone());\nlet \
         lang_content = rustelo_core_lib::build_page_content_patterns(&translator, \
         &[\"not-found-\"]);\nview! { \
         <rustelo_pages_leptos::not_found::unified::UnifiedNotFoundPage _language=lang \
         lang_content=lang_content /> }.into_any()\n},\n"
    };
    leptos_code.push_str(not_found_arm);
    leptos_code.push_str("    }\n}\n");

    let leptos_out = out_dir.join("server_routing_leptos.rs");
    fs::write(&leptos_out, &leptos_code)
        .map_err(|e| format!("Failed to write server_routing_leptos.rs: {e}"))?;

    println!(
        "cargo:warning=Generated server_routing.rs + server_routing_leptos.rs ({} page components)",
        page_files.len()
    );
    Ok(())
}

/// Fallback when the pages cache is empty or missing.
///
/// Generates `server_routing.rs` (core) + `server_routing_leptos.rs` (Leptos
/// dispatch) with stub implementations so the server crate compiles before page
/// components exist.
fn generate_server_routing_fallback(out_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
    let core_code = "// Fallback server_routing.rs — no page files found in cache.\nfn \
                     get_component_for_path(_path: &str) -> &'static str { \"NotFound\" }\npub fn \
                     get_content_type_for_path(_path: &str) -> Option<&'static str> { None }\n";
    fs::write(out_dir.join("server_routing.rs"), core_code)
        .map_err(|e| format!("Failed to write fallback server_routing.rs: {e}"))?;

    let code = r#"// Fallback server_routing_leptos.rs — no page files found in cache.
// Re-run cargo build after page components have been generated.

use leptos::prelude::*;

pub fn render_website_page_ssr(path: &str, language: &str) -> AnyView {
    let lang = language.to_string();
    let translator = rustelo_core_lib::SsrTranslator::new(lang.clone());
    let lang_content = rustelo_core_lib::build_page_content_patterns(&translator, &["not-found-"]);
    view! {
        <rustelo_pages_leptos::not_found::unified::UnifiedNotFoundPage
            _language=lang
            lang_content=lang_content
        />
    }.into_any()
}
"#;

    fs::write(out_dir.join("server_routing_leptos.rs"), code)
        .map_err(|e| format!("Failed to write fallback server_routing_leptos.rs: {e}"))?;

    println!(
        "cargo:warning=Generated server_routing.rs + server_routing_leptos.rs (fallback — no page \
         cache found)"
    );
    Ok(())
}
