use std::{
    collections::{BTreeMap, BTreeSet},
    env,
    fmt::Write,
    fs::{self},
    io::{self},
    path::{Path, PathBuf},
    sync::Arc,
};
use assembly::{
    Assembler, DefaultSourceManager, KernelLibrary, Library, LibraryNamespace, Report,
    diagnostics::{IntoDiagnostic, Result, WrapErr},
    utils::Serializable,
};
use regex::Regex;
use walkdir::WalkDir;
type ErrorCategoryMap = BTreeMap<ErrorCategory, Vec<NamedError>>;
const BUILD_GENERATED_FILES_IN_SRC: bool = option_env!("BUILD_GENERATED_FILES_IN_SRC").is_some();
const ASSETS_DIR: &str = "assets";
const ASM_DIR: &str = "asm";
const ASM_MIDEN_DIR: &str = "miden";
const ASM_NOTE_SCRIPTS_DIR: &str = "note_scripts";
const ASM_ACCOUNT_COMPONENTS_DIR: &str = "account_components";
const SHARED_DIR: &str = "shared";
const ASM_TX_KERNEL_DIR: &str = "kernels/transaction";
const KERNEL_V0_RS_FILE: &str = "src/transaction/procedures/kernel_v0.rs";
const TX_KERNEL_ERRORS_FILE: &str = "src/errors/tx_kernel_errors.rs";
const NOTE_SCRIPT_ERRORS_FILE: &str = "src/errors/note_script_errors.rs";
const TX_KERNEL_ERRORS_ARRAY_NAME: &str = "TX_KERNEL_ERRORS";
const NOTE_SCRIPT_ERRORS_ARRAY_NAME: &str = "NOTE_SCRIPT_ERRORS";
const TX_KERNEL_ERROR_CATEGORIES: [TxKernelErrorCategory; 11] = [
    TxKernelErrorCategory::Kernel,
    TxKernelErrorCategory::Prologue,
    TxKernelErrorCategory::Epilogue,
    TxKernelErrorCategory::Tx,
    TxKernelErrorCategory::Note,
    TxKernelErrorCategory::Account,
    TxKernelErrorCategory::ForeignAccount,
    TxKernelErrorCategory::Faucet,
    TxKernelErrorCategory::FungibleAsset,
    TxKernelErrorCategory::NonFungibleAsset,
    TxKernelErrorCategory::Vault,
];
fn main() -> Result<()> {
        println!("cargo:rerun-if-changed={ASM_DIR}");
    println!("cargo::rerun-if-env-changed=BUILD_GENERATED_FILES_IN_SRC");
        let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
    let build_dir = env::var("OUT_DIR").unwrap();
    let src = Path::new(&crate_dir).join(ASM_DIR);
    let dst = Path::new(&build_dir).to_path_buf();
    copy_directory(src, &dst);
        let source_dir = dst.join(ASM_DIR);
        let target_dir = Path::new(&build_dir).join(ASSETS_DIR);
        let mut assembler =
        compile_tx_kernel(&source_dir.join(ASM_TX_KERNEL_DIR), &target_dir.join("kernels"))?;
        let miden_lib = compile_miden_lib(&source_dir, &target_dir, assembler.clone())?;
    assembler.add_library(miden_lib)?;
        compile_note_scripts(
        &source_dir.join(ASM_NOTE_SCRIPTS_DIR),
        &target_dir.join(ASM_NOTE_SCRIPTS_DIR),
        assembler.clone(),
    )?;
        compile_account_components(
        &source_dir.join(ASM_ACCOUNT_COMPONENTS_DIR),
        &target_dir.join(ASM_ACCOUNT_COMPONENTS_DIR),
        assembler,
    )?;
    generate_error_constants(&source_dir)?;
    Ok(())
}
fn compile_tx_kernel(source_dir: &Path, target_dir: &Path) -> Result<Assembler> {
    let shared_path = Path::new(ASM_DIR).join(SHARED_DIR);
    let kernel_namespace = LibraryNamespace::new("kernel").expect("namespace should be valid");
    let mut assembler = build_assembler(None)?;
        assembler.add_modules_from_dir(kernel_namespace.clone(), &shared_path)?;
        let kernel_lib = KernelLibrary::from_dir(
        source_dir.join("api.masm"),
        Some(source_dir.join("lib")),
        assembler,
    )?;
        generate_kernel_proc_hash_file(kernel_lib.clone())?;
    let output_file = target_dir.join("tx_kernel").with_extension(Library::LIBRARY_EXTENSION);
    kernel_lib.write_to_file(output_file).into_diagnostic()?;
    let assembler = build_assembler(Some(kernel_lib))?;
        let mut main_assembler = assembler.clone();
        main_assembler.add_modules_from_dir(kernel_namespace.clone(), &shared_path)?;
    main_assembler.add_modules_from_dir(kernel_namespace, &source_dir.join("lib"))?;
    let main_file_path = source_dir.join("main.masm");
    let kernel_main = main_assembler.clone().assemble_program(main_file_path)?;
    let masb_file_path = target_dir.join("tx_kernel.masb");
    kernel_main.write_to_file(masb_file_path).into_diagnostic()?;
        compile_tx_script_main(source_dir, target_dir, main_assembler)?;
    #[cfg(any(feature = "testing", test))]
    {
        let mut kernel_lib_assembler = assembler.clone();
                                let kernel_namespace =
            "kernel".parse::<LibraryNamespace>().expect("invalid base namespace");
                kernel_lib_assembler.add_modules_from_dir(kernel_namespace.clone(), &shared_path)?;
        let test_lib =
            Library::from_dir(source_dir.join("lib"), kernel_namespace, kernel_lib_assembler)
                .unwrap();
        let masb_file_path =
            target_dir.join("kernel_library").with_extension(Library::LIBRARY_EXTENSION);
        test_lib.write_to_file(masb_file_path).into_diagnostic()?;
    }
    Ok(assembler)
}
fn compile_tx_script_main(
    source_dir: &Path,
    target_dir: &Path,
    main_assembler: Assembler,
) -> Result<()> {
            let tx_script_main_file_path = source_dir.join("tx_script_main.masm");
    let tx_script_main = main_assembler.assemble_program(tx_script_main_file_path)?;
    let masb_file_path = target_dir.join("tx_script_main.masb");
    tx_script_main.write_to_file(masb_file_path).into_diagnostic()
}
fn generate_kernel_proc_hash_file(kernel: KernelLibrary) -> Result<()> {
            if !BUILD_GENERATED_FILES_IN_SRC {
        return Ok(());
    }
    let (_, module_info, _) = kernel.into_parts();
    let to_exclude = BTreeSet::from_iter(["exec_kernel_proc"]);
    let offsets_filename = Path::new(ASM_DIR).join(ASM_MIDEN_DIR).join("kernel_proc_offsets.masm");
    let offsets = parse_proc_offsets(&offsets_filename)?;
    let generated_procs: BTreeMap<usize, String> = module_info
        .procedures()
        .filter(|(_, proc_info)| !to_exclude.contains::<str>(proc_info.name.as_ref()))
        .map(|(_, proc_info)| {
            let name = proc_info.name.to_string();
            let Some(&offset) = offsets.get(&name) else {
                panic!("Offset constant for function `{name}` not found in `{offsets_filename:?}`");
            };
            (offset, format!("    // {name}\n    digest!(\"{}\"),", proc_info.digest))
        })
        .collect();
    let proc_count = generated_procs.len();
    let generated_procs: String = generated_procs.into_iter().enumerate().map(|(index, (offset, txt))| {
        if index != offset {
            panic!("Offset constants in the file `{offsets_filename:?}` are not contiguous (missing offset: {index})");
        }
        txt
    }).collect::<Vec<_>>().join("\n");
    fs::write(
        KERNEL_V0_RS_FILE,
        format!(
            r#"//! This file is generated by build.rs, do not modify
use miden_objects::{{digest, Digest}};
// KERNEL V0 PROCEDURES
// ================================================================================================
/// Hashes of all dynamically executed procedures from the kernel 0.
pub const KERNEL0_PROCEDURES: [Digest; {proc_count}] = [
{generated_procs}
];
"#,
        ),
    )
    .into_diagnostic()
}
fn parse_proc_offsets(filename: impl AsRef<Path>) -> Result<BTreeMap<String, usize>> {
    let regex: Regex = Regex::new(r"^const\.(?P<name>\w+)_OFFSET\s*=\s*(?P<offset>\d+)").unwrap();
    let mut result = BTreeMap::new();
    for line in fs::read_to_string(filename).into_diagnostic()?.lines() {
        if let Some(captures) = regex.captures(line) {
            result.insert(
                captures["name"].to_string().to_lowercase(),
                captures["offset"].parse().into_diagnostic()?,
            );
        }
    }
    Ok(result)
}
fn compile_miden_lib(
    source_dir: &Path,
    target_dir: &Path,
    mut assembler: Assembler,
) -> Result<Library> {
    let source_dir = source_dir.join(ASM_MIDEN_DIR);
    let shared_path = Path::new(ASM_DIR).join(SHARED_DIR);
    let miden_namespace = "miden".parse::<LibraryNamespace>().expect("invalid base namespace");
        assembler.add_modules_from_dir(miden_namespace.clone(), &shared_path)?;
    let miden_lib = Library::from_dir(source_dir, miden_namespace, assembler)?;
    let output_file = target_dir.join("miden").with_extension(Library::LIBRARY_EXTENSION);
    miden_lib.write_to_file(output_file).into_diagnostic()?;
    Ok(miden_lib)
}
fn compile_note_scripts(source_dir: &Path, target_dir: &Path, assembler: Assembler) -> Result<()> {
    if let Err(e) = fs::create_dir_all(target_dir) {
        println!("Failed to create note_scripts directory: {}", e);
    }
    for masm_file_path in get_masm_files(source_dir).unwrap() {
                let code = assembler.clone().assemble_program(masm_file_path.clone())?;
        let bytes = code.to_bytes();
                let masb_file_name = masm_file_path.file_name().unwrap().to_str().unwrap();
        let mut masb_file_path = target_dir.join(masb_file_name);
                masb_file_path.set_extension("masb");
        fs::write(masb_file_path, bytes).unwrap();
    }
    Ok(())
}
fn compile_account_components(
    source_dir: &Path,
    target_dir: &Path,
    assembler: Assembler,
) -> Result<()> {
    if !target_dir.exists() {
        fs::create_dir_all(target_dir).unwrap();
    }
    for masm_file_path in get_masm_files(source_dir).unwrap() {
        let component_name = masm_file_path
            .file_stem()
            .expect("masm file should have a file stem")
            .to_str()
            .expect("file stem should be valid UTF-8")
            .to_owned();
                        let component_source_code = fs::read_to_string(masm_file_path)
            .expect("reading the component's MASM source code should succeed");
        let component_library = assembler
            .clone()
            .assemble_library([component_source_code])
            .expect("library assembly should succeed");
        let component_file_path =
            target_dir.join(component_name).with_extension(Library::LIBRARY_EXTENSION);
        component_library.write_to_file(component_file_path).into_diagnostic()?;
    }
    Ok(())
}
fn build_assembler(kernel: Option<KernelLibrary>) -> Result<Assembler> {
    kernel
        .map(|kernel| Assembler::with_kernel(Arc::new(DefaultSourceManager::default()), kernel))
        .unwrap_or_default()
        .with_debug_mode(cfg!(feature = "with-debug-info"))
        .with_library(miden_stdlib::StdLibrary::default())
}
fn copy_directory<T: AsRef<Path>, R: AsRef<Path>>(src: T, dst: R) {
    let mut prefix = src.as_ref().canonicalize().unwrap();
        prefix.pop();
    let target_dir = dst.as_ref().join(ASM_DIR);
    if !target_dir.exists() {
        fs::create_dir_all(target_dir).unwrap();
    }
    let dst = dst.as_ref();
    let mut todo = vec![src.as_ref().to_path_buf()];
    while let Some(goal) = todo.pop() {
        for entry in fs::read_dir(goal).unwrap() {
            let path = entry.unwrap().path();
            if path.is_dir() {
                let src_dir = path.canonicalize().unwrap();
                let dst_dir = dst.join(src_dir.strip_prefix(&prefix).unwrap());
                if !dst_dir.exists() {
                    fs::create_dir_all(&dst_dir).unwrap();
                }
                todo.push(src_dir);
            } else {
                let dst_file = dst.join(path.strip_prefix(&prefix).unwrap());
                fs::copy(&path, dst_file).unwrap();
            }
        }
    }
}
fn get_masm_files<P: AsRef<Path>>(dir_path: P) -> io::Result<Vec<PathBuf>> {
    let mut files = Vec::new();
    let path = dir_path.as_ref();
    if path.is_dir() {
        match fs::read_dir(path) {
            Ok(entries) => {
                for entry in entries {
                    match entry {
                        Ok(file) => {
                            let file_path = file.path();
                            if is_masm_file(&file_path)? {
                                files.push(file_path);
                            }
                        },
                        Err(e) => println!("Error reading directory entry: {}", e),
                    }
                }
            },
            Err(e) => println!("Error reading directory: {}", e),
        }
    } else {
        println!("cargo:rerun-The specified path is not a directory.");
    }
    Ok(files)
}
fn is_masm_file(path: &Path) -> io::Result<bool> {
    if let Some(extension) = path.extension() {
        let extension = extension
            .to_str()
            .ok_or_else(|| io::Error::other("invalid UTF-8 filename"))?
            .to_lowercase();
        Ok(extension == "masm")
    } else {
        Ok(false)
    }
}
fn generate_error_constants(asm_source_dir: &Path) -> Result<()> {
    if !BUILD_GENERATED_FILES_IN_SRC {
        return Ok(());
    }
    let categories =
        extract_all_masm_errors(asm_source_dir).context("failed to extract all masm errors")?;
    for (category, errors) in categories {
                let error_file_content = generate_error_file_content(category, errors)?;
        std::fs::write(category.error_file_name(), error_file_content).into_diagnostic()?;
    }
    Ok(())
}
fn extract_all_masm_errors(asm_source_dir: &Path) -> Result<ErrorCategoryMap> {
                let mut errors = BTreeMap::new();
        for entry in WalkDir::new(asm_source_dir) {
        let entry = entry.into_diagnostic()?;
        if !is_masm_file(entry.path()).into_diagnostic()? {
            continue;
        }
        let file_contents = std::fs::read_to_string(entry.path()).into_diagnostic()?;
        extract_masm_errors(&mut errors, &file_contents)?;
    }
    let mut category_map: BTreeMap<ErrorCategory, Vec<NamedError>> = BTreeMap::new();
    for (error_name, error) in errors.into_iter() {
        let category = ErrorCategory::match_category(&error_name)?;
        let named_error = NamedError { name: error_name, message: error.message };
        category_map.entry(category).or_default().push(named_error);
    }
    Ok(category_map)
}
fn extract_masm_errors(
    errors: &mut BTreeMap<ErrorName, ExtractedError>,
    file_contents: &str,
) -> Result<()> {
    let regex = Regex::new(r#"const\.ERR_(?<name>.*)="(?<message>.*)""#).unwrap();
    for capture in regex.captures_iter(file_contents) {
        let error_name = capture
            .name("name")
            .expect("error name should be captured")
            .as_str()
            .trim()
            .to_owned();
        let error_message = capture
            .name("message")
            .expect("error code should be captured")
            .as_str()
            .trim()
            .to_owned();
        if let Some(ExtractedError { message: existing_error_message, .. }) =
            errors.get(&error_name)
        {
            if existing_error_message != &error_message {
                return Err(Report::msg(format!(
                    "Transaction kernel error constant ERR_{error_name} is already defined elsewhere but its error message is different"
                )));
            }
        }
                if error_message.ends_with(".") {
            return Err(Report::msg(format!(
                "Error messages should not end with a period: `ERR_{error_name}: {error_message}`"
            )));
        }
        errors.insert(error_name, ExtractedError { message: error_message });
    }
    Ok(())
}
fn is_new_error_category<'a>(last_error: &mut Option<&'a str>, current_error: &'a str) -> bool {
    let is_new = match last_error {
        Some(last_err) => {
            let last_category =
                last_err.split("_").next().expect("there should be at least one entry");
            let new_category =
                current_error.split("_").next().expect("there should be at least one entry");
            last_category != new_category
        },
        None => false,
    };
    last_error.replace(current_error);
    is_new
}
fn generate_error_file_content(category: ErrorCategory, errors: Vec<NamedError>) -> Result<String> {
    let mut output = String::new();
    writeln!(output, "use crate::errors::MasmError;\n").unwrap();
    writeln!(
        output,
        "// This file is generated by build.rs, do not modify manually.
// It is generated by extracting errors from the masm files in the `miden-lib/asm` directory.
//
// To add a new error, define a constant in masm of the pattern `const.ERR_<CATEGORY>_...`.
// Try to fit the error into a pre-existing category if possible (e.g. Account, Prologue,
// Non-Fungible-Asset, ...).
"
    )
    .unwrap();
    writeln!(
        output,
        "// {}
// ================================================================================================
",
        category.array_name().replace("_", " ")
    )
    .unwrap();
    let mut last_error = None;
    for named_error in errors.iter() {
        let NamedError { name, message } = named_error;
                if is_new_error_category(&mut last_error, name) {
            writeln!(output).into_diagnostic()?;
        }
        writeln!(output, "/// Error Message: \"{message}\"").into_diagnostic()?;
        writeln!(
            output,
            r#"pub const ERR_{name}: MasmError = MasmError::from_static_str("{message}");"#
        )
        .into_diagnostic()?;
    }
    Ok(output)
}
type ErrorName = String;
#[derive(Debug, Clone)]
struct ExtractedError {
    message: String,
}
#[derive(Debug, Clone)]
struct NamedError {
    name: ErrorName,
    message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum ErrorCategory {
    TxKernel,
    NoteScript,
}
impl ErrorCategory {
    pub const fn error_file_name(&self) -> &'static str {
        match self {
            ErrorCategory::TxKernel => TX_KERNEL_ERRORS_FILE,
            ErrorCategory::NoteScript => NOTE_SCRIPT_ERRORS_FILE,
        }
    }
    pub const fn array_name(&self) -> &'static str {
        match self {
            ErrorCategory::TxKernel => TX_KERNEL_ERRORS_ARRAY_NAME,
            ErrorCategory::NoteScript => NOTE_SCRIPT_ERRORS_ARRAY_NAME,
        }
    }
    pub fn match_category(error_name: &ErrorName) -> Result<Self> {
        for kernel_category in TX_KERNEL_ERROR_CATEGORIES {
            if error_name.starts_with(kernel_category.category_name()) {
                return Ok(ErrorCategory::TxKernel);
            }
        }
                Ok(ErrorCategory::NoteScript)
    }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum TxKernelErrorCategory {
    Kernel,
    Prologue,
    Epilogue,
    Tx,
    Note,
    Account,
    ForeignAccount,
    Faucet,
    FungibleAsset,
    NonFungibleAsset,
    Vault,
}
impl TxKernelErrorCategory {
    pub const fn category_name(&self) -> &'static str {
        match self {
            TxKernelErrorCategory::Kernel => "KERNEL",
            TxKernelErrorCategory::Prologue => "PROLOGUE",
            TxKernelErrorCategory::Epilogue => "EPILOGUE",
            TxKernelErrorCategory::Tx => "TX",
            TxKernelErrorCategory::Note => "NOTE",
            TxKernelErrorCategory::Account => "ACCOUNT",
            TxKernelErrorCategory::ForeignAccount => "FOREIGN_ACCOUNT",
            TxKernelErrorCategory::Faucet => "FAUCET",
            TxKernelErrorCategory::FungibleAsset => "FUNGIBLE_ASSET",
            TxKernelErrorCategory::NonFungibleAsset => "NON_FUNGIBLE_ASSET",
            TxKernelErrorCategory::Vault => "VAULT",
        }
    }
}