use serde::Deserialize;
use std::collections::BTreeSet;
use std::fs;
use std::path::Path;

/// Project indicator from pkl/builtins_meta.json
#[derive(Debug, Deserialize, Default)]
struct ProjectIndicator {
    #[serde(default)]
    file: Option<String>,
    #[serde(default)]
    glob: Option<String>,
    #[serde(default)]
    contains: Option<String>,
}

/// Metadata from pkl/builtins_meta.json (generated by `mise run pkl:gen`)
#[derive(Debug, Deserialize)]
struct BuiltinMeta {
    name: String,
    category: String,
    description: String,
    #[serde(default)]
    project_indicators: Vec<ProjectIndicator>,
}

/// Escape a string for use in Rust string literals
fn escape_string(s: &str) -> String {
    s.replace('\\', "\\\\").replace('"', "\\\"")
}

pub fn generate(out_dir: &Path) -> Result<(), std::io::Error> {
    // Try to read pre-generated metadata from pkl:gen
    // Falls back to just listing builtin names if not available
    let meta_path = Path::new("pkl/builtins_meta.json");
    let builtins_meta: Vec<BuiltinMeta> = if meta_path.exists() {
        let content = fs::read_to_string(meta_path)?;
        serde_json::from_str(&content).expect("failed to parse pkl/builtins_meta.json")
    } else {
        Vec::new()
    };

    // Also collect all builtin names from the directory (as fallback/validation)
    let builtins_dir = Path::new("pkl/builtins");
    let builtin_names = collect_builtin_names(builtins_dir)?;

    // Create the BUILTINS constant as a static array
    let builtins_array = generate_builtins_array(&builtin_names);

    // Create the BUILTINS_META constant with full metadata
    let builtins_meta_code = generate_builtins_meta(&builtins_meta, &builtin_names);

    // Combine and write to file
    let output = format!("{}\n{}", builtins_array, builtins_meta_code);
    let dest_path = out_dir.join("builtins.rs");
    fs::write(dest_path, output)?;

    Ok(())
}

fn collect_builtin_names(builtins_dir: &Path) -> Result<BTreeSet<String>, std::io::Error> {
    let mut builtins = BTreeSet::new();

    for entry in fs::read_dir(builtins_dir)? {
        let entry = entry?;
        let path = entry.path();

        if path.extension().and_then(|s| s.to_str()) == Some("pkl")
            && let Some(name) = path.file_stem().and_then(|s| s.to_str())
        {
            builtins.insert(name.to_string());
        }
    }

    Ok(builtins)
}

fn generate_builtins_array(builtins: &BTreeSet<String>) -> String {
    let items: Vec<String> = builtins.iter().map(|b| format!("    \"{}\"", b)).collect();

    format!(
        "/// List of all available builtin configurations\npub const BUILTINS: &[&str] = &[\n{},\n];",
        items.join(",\n")
    )
}

fn generate_builtins_meta(meta: &[BuiltinMeta], all_names: &BTreeSet<String>) -> String {
    let mut items = Vec::new();

    // Create a map for quick lookup
    let meta_map: std::collections::HashMap<&str, &BuiltinMeta> =
        meta.iter().map(|m| (m.name.as_str(), m)).collect();

    // Generate entries for all builtins, using metadata when available
    for name in all_names {
        let (category, description, indicators) = if let Some(m) = meta_map.get(name.as_str()) {
            (
                m.category.as_str(),
                m.description.as_str(),
                &m.project_indicators,
            )
        } else {
            ("", "", &Vec::new() as &Vec<ProjectIndicator>)
        };

        // Generate project indicators array
        let indicators_code: Vec<String> = indicators
            .iter()
            .map(|ind| {
                let file = ind
                    .file
                    .as_ref()
                    .map(|s| format!("Some(\"{}\")", escape_string(s)))
                    .unwrap_or_else(|| "None".to_string());
                let glob = ind
                    .glob
                    .as_ref()
                    .map(|s| format!("Some(\"{}\")", escape_string(s)))
                    .unwrap_or_else(|| "None".to_string());
                let contains = ind
                    .contains
                    .as_ref()
                    .map(|s| format!("Some(\"{}\")", escape_string(s)))
                    .unwrap_or_else(|| "None".to_string());
                format!(
                    "ProjectIndicator {{ file: {}, glob: {}, contains: {} }}",
                    file, glob, contains
                )
            })
            .collect();

        let indicators_str = if indicators_code.is_empty() {
            "&[]".to_string()
        } else {
            format!("&[{}]", indicators_code.join(", "))
        };

        items.push(format!(
            "    BuiltinMeta {{ name: \"{}\", category: \"{}\", description: \"{}\", project_indicators: {} }}",
            name,
            escape_string(category),
            escape_string(description),
            indicators_str,
        ));
    }

    format!(
        r#"
/// Project indicator for auto-detection
#[derive(Debug, Clone, Copy)]
pub struct ProjectIndicator {{
    /// File or directory to check for existence
    pub file: Option<&'static str>,
    /// Glob pattern to check for matching files
    pub glob: Option<&'static str>,
    /// Content to grep for in the file (requires file to be set)
    pub contains: Option<&'static str>,
}}

/// Metadata for a builtin configuration
#[derive(Debug, Clone, Copy)]
pub struct BuiltinMeta {{
    pub name: &'static str,
    pub category: &'static str,
    pub description: &'static str,
    pub project_indicators: &'static [ProjectIndicator],
}}

/// List of all builtin configurations with metadata
pub const BUILTINS_META: &[BuiltinMeta] = &[
{},
];"#,
        items.join(",\n")
    )
}
