use std::env;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};

fn main() {
    // Let rustc know that `cfg(rust_analyzer)` is an expected configuration
    println!("cargo:rustc-check-cfg=cfg(rust_analyzer)");

    // Where this crate's Cargo.toml lives.
    let manifest_dir =
        PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"));

    // Assume crate is at repo_root/src/rust/cep-core
    let repo_root = manifest_dir
        .parent()
        .and_then(|p| p.parent())
        .expect("Expected cep-core at repo_root/src/rust/cep-core; could not find repo root");

    let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
    let generated_path = out_dir.join("assets_generated.rs");
    let mut out = fs::File::create(&generated_path)
        .unwrap_or_else(|error| panic!("Failed to create {}: {}", generated_path.display(), error));

    // Header
    writeln!(
        out,
        "// @generated by cep-core/build.rs - DO NOT EDIT BY HAND\n"
    )
    .unwrap();

    // schemas -> SCHEMAS (namespace-aware keys, e.g. \"cep.entity\")
    generate_schemas_group(&repo_root, &out_dir, &mut out);

    // vocabularies -> VOCABULARIES (relative path without .json)
    generate_simple_group(
        &repo_root,
        &out_dir,
        &mut out,
        "vocabularies",
        "VOCABULARIES",
    );

    // test_vectors -> TEST_VECTORS (relative path without .json)
    generate_simple_group(
        &repo_root,
        &out_dir,
        &mut out,
        "test_vectors",
        "TEST_VECTORS",
    );
}

/// Generate SCHEMAS with namespace-aware keys.
///
/// Any file named `cep.<name>.schema.json` becomes key `\"cep.<name>\"`.
/// Examples:
/// - cep.entity.schema.json                    -> \"cep.entity\"
/// - cep.relationship.schema.json              -> \"cep.relationship\"
/// - cep.exchange.schema.json                  -> \"cep.exchange\"
/// - cep.entity.identifier-scheme.schema.json  -> \"cep.entity.identifier-scheme\"
/// - cep.vocabulary.schema.json                -> \"cep.vocabulary\"
///
/// Any other .json file under schemas/ falls back to:
///   \"relative/path/without.json\"
fn generate_schemas_group(repo_root: &Path, out_dir: &Path, out: &mut fs::File) {
    let folder = "schemas";
    let static_name = "SCHEMAS";

    let source_dir = repo_root.join(folder);
    if !source_dir.is_dir() {
        writeln!(out, "pub static {}: &[(&str, &str)] = &[];\n", static_name).unwrap();
        return;
    }

    println!("cargo:rerun-if-changed={}", source_dir.display());

    let rel_paths = collect_json_relative_paths(&source_dir);

    // Map to keys using the schema naming convention
    let mut entries: Vec<(PathBuf, String)> = rel_paths
        .into_iter()
        .map(|rel| {
            let key = schema_key_for(&rel);
            (rel, key)
        })
        .collect();

    // Deterministic order
    entries.sort_by(|a, b| a.1.cmp(&b.1));

    let group_out_dir = out_dir.join(folder);
    fs::create_dir_all(&group_out_dir)
        .unwrap_or_else(|error| panic!("Failed to create {}: {}", group_out_dir.display(), error));

    writeln!(out, "pub static {}: &[(&str, &str)] = &[", static_name).unwrap();

    for (rel_path, key) in entries {
        let src_path = source_dir.join(&rel_path);
        let dest_path = group_out_dir.join(&rel_path);

        if let Some(parent) = dest_path.parent() {
            fs::create_dir_all(parent)
                .unwrap_or_else(|error| panic!("Failed to create {}: {}", parent.display(), error));
        }

        fs::copy(&src_path, &dest_path).unwrap_or_else(|error| {
            panic!(
                "Failed to copy {} to {}: {}",
                src_path.display(),
                dest_path.display(),
                error
            )
        });

        let rel_str = rel_path.to_string_lossy().replace('\\', "/");

        writeln!(
            out,
            "    (\"{}\", include_str!(concat!(env!(\"OUT_DIR\"), \"/{}/{}\"))),",
            key, folder, rel_str
        )
        .unwrap();
    }

    writeln!(out, "];\n").unwrap();
}

/// Generate a group where keys are relative paths without `.json`.
///
/// Used for:
/// - vocabularies -> VOCABULARIES
/// - test_vectors -> TEST_VECTORS
fn generate_simple_group(
    repo_root: &Path,
    out_dir: &Path,
    out: &mut fs::File,
    folder: &str,
    static_name: &str,
) {
    let source_dir = repo_root.join(folder);
    if !source_dir.is_dir() {
        writeln!(out, "pub static {}: &[(&str, &str)] = &[];\n", static_name).unwrap();
        return;
    }

    println!("cargo:rerun-if-changed={}", source_dir.display());

    let rel_paths = collect_json_relative_paths(&source_dir);

    let mut entries: Vec<(PathBuf, String)> = rel_paths
        .into_iter()
        .map(|rel| {
            let key = default_key_for(&rel);
            (rel, key)
        })
        .collect();

    entries.sort_by(|a, b| a.1.cmp(&b.1));

    let group_out_dir = out_dir.join(folder);
    fs::create_dir_all(&group_out_dir)
        .unwrap_or_else(|error| panic!("Failed to create {}: {}", group_out_dir.display(), error));

    writeln!(out, "pub static {}: &[(&str, &str)] = &[", static_name).unwrap();

    for (rel_path, key) in entries {
        let src_path = source_dir.join(&rel_path);
        let dest_path = group_out_dir.join(&rel_path);

        if let Some(parent) = dest_path.parent() {
            fs::create_dir_all(parent)
                .unwrap_or_else(|error| panic!("Failed to create {}: {}", parent.display(), error));
        }

        fs::copy(&src_path, &dest_path).unwrap_or_else(|error| {
            panic!(
                "Failed to copy {} to {}: {}",
                src_path.display(),
                dest_path.display(),
                error
            )
        });

        let rel_str = rel_path.to_string_lossy().replace('\\', "/");

        writeln!(
            out,
            "    (\"{}\", include_str!(concat!(env!(\"OUT_DIR\"), \"/{}/{}\"))),",
            key, folder, rel_str
        )
        .unwrap();
    }

    writeln!(out, "];\n").unwrap();
}

/// Recursively collect all `.json` files under `root`,
/// returning paths relative to `root`.
fn collect_json_relative_paths(root: &Path) -> Vec<PathBuf> {
    let mut out_paths = Vec::new();
    collect_json_recursive(root, root, &mut out_paths);
    out_paths
}

fn collect_json_recursive(dir: &Path, base: &Path, out: &mut Vec<PathBuf>) {
    let entries = match fs::read_dir(dir) {
        Ok(entries) => entries,
        Err(_) => return,
    };

    for entry in entries.flatten() {
        let path = entry.path();
        if path.is_dir() {
            collect_json_recursive(&path, base, out);
        } else if path.extension().and_then(|e| e.to_str()) == Some("json") {
            let rel = path
                .strip_prefix(base)
                .expect("strip_prefix failed")
                .to_owned();
            out.push(rel);
        }
    }
}

/// Default key = relative path without `.json`, forward slashes.
///
/// Example:
///   vocabularies/entity-type.json       -> \"entity-type\"
///   test_vectors/entity/minimal.json    -> \"entity/minimal\"
fn default_key_for(rel: &Path) -> String {
    let mut key = rel.to_string_lossy().replace('\\', "/");
    if key.ends_with(".json") {
        key.truncate(key.len() - ".json".len());
    }
    key
}

/// Schema key rule:
///
/// If filename matches `cep.<name>.schema.json`, key is `\"cep.<name>\"`.
/// This covers:
/// - cep.entity.schema.json                    -> \"cep.entity\"
/// - cep.relationship.schema.json              -> \"cep.relationship\"
/// - cep.exchange.schema.json                  -> \"cep.exchange\"
/// - cep.entity.identifier-scheme.schema.json  -> \"cep.entity.identifier-scheme\"
/// - cep.vocabulary.schema.json                -> \"cep.vocabulary\"
/// - and future:
///   - cep.entity.identifier.schema.json            -> \"cep.entity.identifier\"
///   - cep.entity.identifier-record.schema.json     -> \"cep.entity.identifier-record\"
///   - cep.entity.identifier-status.schema.json     -> \"cep.entity.identifier-status\"
///   - cep.entity.identifier-mapping.schema.json    -> \"cep.entity.identifier-mapping\"
///   - cep.entity.identifier-rule.schema.json       -> \"cep.entity.identifier-rule\"
///   - cep.entity.identifier-normalization.schema.json -> \"cep.entity.identifier-normalization\"
///
/// Otherwise, fall back to `default_key_for(rel)`.
fn schema_key_for(rel: &Path) -> String {
    if let Some(file_name) = rel.file_name().and_then(|n| n.to_str()) {
        const PREFIX: &str = "cep.";
        const SUFFIX: &str = ".schema.json";

        if file_name.starts_with(PREFIX) && file_name.ends_with(SUFFIX) {
            let inner = &file_name[PREFIX.len()..file_name.len() - SUFFIX.len()];
            return format!("cep.{}", inner);
        }
    }

    default_key_for(rel)
}
