use flate2::{read::GzDecoder, write::GzEncoder, Compression};
use std::env;
use std::fs::{self, File};
use std::io::{BufReader, BufWriter, Read, Write};
use std::path::Path;

const CONSTITUTION_DIRS: [&str; 7] = [
    "core", "specs", "plugins", "interfaces", "methodology", "architecture", "docs",
];

fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("cargo:rerun-if-env-changed=DECAPOD_CONSTITUTION_DIR");
    println!("cargo:rerun-if-changed=constitution");
    println!("cargo:rerun-if-changed=build/compress_constitution.rs");

    let manifest_dir = env::var("CARGO_MANIFEST_DIR")?;
    let out_dir = env::var("OUT_DIR")?;
    let constitution_path = Path::new(&manifest_dir).join("constitution");
    let compressed_dir = Path::new(&out_dir).join("constitution_compressed");

    // Create output directory
    fs::create_dir_all(&compressed_dir)?;

    let mut doc_count = 0;
    let mut total_original = 0u64;
    let mut total_compressed = 0u64;

    for dir_name in &CONSTITUTION_DIRS {
        let dir_path = constitution_path.join(dir_name);

        if !dir_path.exists() {
            eprintln!("Warning: Directory {} does not exist", dir_path.display());
            continue;
        }

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

            if path.extension().and_then(|s| s.to_str()) == Some("md") {
                // Read original file
                let original = fs::read(&path)?;
                let original_len = original.len() as u64;
                total_original += original_len;

                // Compress
                let compressed = gzip_compress(&original);
                let compressed_len = compressed.len() as u64;
                total_compressed += compressed_len;

                // Write compressed file with .gz extension
                let relative_path = path.strip_prefix(&constitution_path).unwrap();
                let out_path = compressed_dir.join(relative_path.with_extension("md.gz"));
                if let Some(parent) = out_path.parent() {
                    fs::create_dir_all(parent)?;
                }
                fs::write(&out_path, &compressed)?;

                // Track for rebuild
                println!("cargo:rerun-if-changed={}", path.display());
                doc_count += 1;
            }
        }
    }

    // Generate Rust source with compressed bytes
    generate_rust_module(&compressed_dir, &out_dir)?;

    let ratio = if total_original > 0 {
        total_original as f64 / total_compressed as f64
    } else {
        0.0
    };

    eprintln!(
        "Constitution compressed: {} docs, {}{} original -> {}{} compressed ({:.1}x ratio)",
        doc_count,
        format_bytes(total_original),
        if total_original > 1024 * 1024 { "" } else { "B" },
        format_bytes(total_compressed),
        if total_compressed > 1024 { "" } else { "B" },
        ratio
    );

    Ok(())
}

fn gzip_compress(data: &[u8]) -> Vec<u8> {
    let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
    encoder.write_all(data).unwrap();
    encoder.finish().unwrap()
}

fn format_bytes(bytes: u64) -> String {
    if bytes >= 1024 * 1024 {
        format!("{:.1}MB", bytes as f64 / (1024.0 * 1024.0))
    } else if bytes >= 1024 {
        format!("{:.1}KB", bytes as f64 / 1024.0)
    } else {
        format!("{}B", bytes)
    }
}

fn generate_rust_module(compressed_dir: &Path, out_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
    let out_path = out_dir.join("constitution_compressed.rs");
    let mut file = File::create(&out_path)?;

    writeln!(file, "// Auto-generated by build/compress_constitution.rs")?;
    writeln!(file, "// Do not edit manually")?;
    writeln!(file)?;
    writeln!(file, "use flate2::read::GzDecoder;")?;
    writeln!(file, "use std::io::Read;")?;
    writeln!(file)?;
    writeln!(file, "/// Decompress gzipped constitution document")?;
    writeln!(file, "pub fn decompress(bytes: &[u8]) -> Result<String, std::io::Error> {{")?;
    writeln!(file, "    let mut decoder = GzDecoder::new(bytes);")?;
    writeln!(file, "    let mut s = String::new();")?;
    writeln!(file, "    decoder.read_to_string(&mut s)?;")?;
    writeln!(file, "    Ok(s)")?;
    writeln!(file, "}}")?;
    writeln!(file)?;

    // Generate include_bytes for each compressed file
    let mut paths: Vec<_> = Vec::new();
    collect_paths(compressed_dir, compressed_dir, &mut paths)?;

    for (relative_path, absolute_path) in &paths {
        let struct_name = path_to_struct_name(relative_path);
        let compressed = fs::read(absolute_path)?;

        writeln!(file)?;
        writeln!(file, "/// Compressed constitution: {}", relative_path)?;
        writeln!(file, "pub const {}_GZ: &[u8] = &{:?};", struct_name, compressed)?;
    }

    writeln!(file)?;
    writeln!(file, "/// Get decompressed constitution document by path")?;
    writeln!(file, "pub fn get_decompressed(path: &str) -> Option<String> {{")?;
    writeln!(file, "    match path {{")?;

    for (relative_path, _) in &paths {
        let struct_name = path_to_struct_name(relative_path);
        let key = relative_path.replace('\\', "/"); // Normalize path separators
        writeln!(file, "        \"{}\" => {{", key)?;
        writeln!(file, "            let mut decoder = GzDecoder::new({}_GZ);", struct_name)?;
        writeln!(file, "            let mut s = String::new();")?;
        writeln!(file, "            decoder.read_to_string(&mut s).ok()?;")?;
        writeln!(file, "            Some(s)")?;
        writeln!(file, "        }}")?;
    }

    writeln!(file, "        _ => None,")?;
    writeln!(file, "    }}")?;
    writeln!(file, "}}")?;

    // Generate list function
    writeln!(file)?;
    writeln!(file, "/// List all available constitution paths")?;
    writeln!(file, "pub fn list_paths() -> Vec<&'static str> {{")?;
    writeln!(file, "    vec![")?;
    for (relative_path, _) in &paths {
        let key = relative_path.replace('\\', "/");
        writeln!(file, "        \"{}\",", key)?;
    }
    writeln!(file, "    ]")?;
    writeln!(file, "}}")?;

    Ok(())
}

fn collect_paths(dir: &Path, base: &Path, paths: &mut Vec<(String, std::path::PathBuf)>) -> std::io::Result<()> {
    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();

        if path.is_dir() {
            collect_paths(&path, base, paths)?;
        } else if path.extension().and_then(|s| s.to_str()) == Some("gz") {
            let relative = path.strip_prefix(base).unwrap();
            let relative_str = relative.to_string_lossy().replace('\\', "/");
            paths.push((relative_str, path));
        }
    }
    Ok(())
}

fn path_to_struct_name(path: &str) -> String {
    path.replace('/', "_")
        .replace('.', "_")
        .replace('-', "_")
        .chars()
        .filter(|c| c.is_alphanumeric() || *c == '_')
        .collect::<String>()
        .to_uppercase()
}