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

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NativeBackend {
    Cc,
    CMake,
}

impl NativeBackend {
    pub fn parse(value: &str) -> Option<Self> {
        match value.trim().to_ascii_lowercase().as_str() {
            "cc" => Some(Self::Cc),
            "cmake" => Some(Self::CMake),
            _ => None,
        }
    }

    pub fn from_env(var_name: &str) -> Option<Self> {
        env::var(var_name).ok().as_deref().and_then(Self::parse)
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NativePackage {
    name: String,
    version: Option<String>,
    components: Vec<String>,
    link_targets: Vec<String>,
}

impl NativePackage {
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            version: None,
            components: Vec::new(),
            link_targets: Vec::new(),
        }
    }

    pub fn version(mut self, version: impl Into<String>) -> Self {
        self.version = Some(version.into());
        self
    }

    pub fn component(mut self, component: impl Into<String>) -> Self {
        self.components.push(component.into());
        self
    }

    pub fn link_target(mut self, target: impl Into<String>) -> Self {
        self.link_targets.push(target.into());
        self
    }
}

#[derive(Debug, Clone)]
pub struct NativeBuildConfig {
    backend: NativeBackend,
    library_name: String,
    cxx_standard: u32,
    roplat_cpp_include_dir: Option<PathBuf>,
    include_dirs: Vec<PathBuf>,
    source_files: Vec<PathBuf>,
    defines: Vec<(String, Option<String>)>,
    link_libs: Vec<String>,
    link_search_dirs: Vec<PathBuf>,
    packages: Vec<NativePackage>,
}

impl Default for NativeBuildConfig {
    fn default() -> Self {
        Self {
            backend: NativeBackend::Cc,
            library_name: "roplat_nodes_cpp".to_string(),
            cxx_standard: 17,
            roplat_cpp_include_dir: None,
            include_dirs: Vec::new(),
            source_files: Vec::new(),
            defines: Vec::new(),
            link_libs: Vec::new(),
            link_search_dirs: Vec::new(),
            packages: Vec::new(),
        }
    }
}

impl NativeBuildConfig {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn backend(mut self, backend: NativeBackend) -> Self {
        self.backend = backend;
        self
    }

    pub fn library_name(mut self, name: impl Into<String>) -> Self {
        self.library_name = name.into();
        self
    }

    pub fn cxx_standard(mut self, standard: u32) -> Self {
        self.cxx_standard = standard;
        self
    }

    pub fn roplat_cpp_include(mut self, dir: impl Into<PathBuf>) -> Self {
        self.roplat_cpp_include_dir = Some(dir.into());
        self
    }

    pub fn include_dir(mut self, dir: impl Into<PathBuf>) -> Self {
        self.include_dirs.push(dir.into());
        self
    }

    pub fn source(mut self, file: impl Into<PathBuf>) -> Self {
        self.source_files.push(file.into());
        self
    }

    pub fn define(mut self, key: impl Into<String>, value: Option<impl Into<String>>) -> Self {
        self.defines
            .push((key.into(), value.map(|item| item.into())));
        self
    }

    pub fn link_lib(mut self, lib: impl Into<String>) -> Self {
        self.link_libs.push(lib.into());
        self
    }

    pub fn link_search_dir(mut self, dir: impl Into<PathBuf>) -> Self {
        self.link_search_dirs.push(dir.into());
        self
    }

    pub fn package(mut self, package: NativePackage) -> Self {
        self.packages.push(package);
        self
    }

    pub fn build(&self, manifest_dir: &Path, gen_cpp_dir: &Path) -> io::Result<()> {
        let mut source_files = discover_generated_ffi_sources(gen_cpp_dir)?;
        source_files.extend(resolve_paths(manifest_dir, &self.source_files));
        if source_files.is_empty() {
            return Ok(());
        }

        let roplat_cpp_include = match &self.roplat_cpp_include_dir {
            Some(dir) => resolve_path(manifest_dir, dir),
            None => find_roplat_cpp_include(manifest_dir)?,
        };
        let mut include_dirs = vec![roplat_cpp_include, gen_cpp_dir.to_path_buf()];
        include_dirs.extend(resolve_paths(manifest_dir, &self.include_dirs));

        match self.backend {
            NativeBackend::Cc => self.build_with_cc(&include_dirs, &source_files),
            NativeBackend::CMake => {
                self.build_with_cmake(manifest_dir, gen_cpp_dir, &include_dirs, &source_files)
            }
        }
    }

    fn build_with_cc(&self, include_dirs: &[PathBuf], source_files: &[PathBuf]) -> io::Result<()> {
        println!("cargo:warning=[Phase 3] Native backend: cc");

        let mut build = cc::Build::new();
        build.cpp(true);
        for include_dir in include_dirs {
            build.include(include_dir);
        }
        for (key, value) in &self.defines {
            build.define(key, value.as_deref());
        }
        for file in source_files {
            build.file(file);
            println!(
                "cargo:warning=[Phase 3] Compiling C++ source: {}",
                file.display()
            );
        }

        build.flag_if_supported(format!("/std:c++{}", self.cxx_standard));
        build.flag_if_supported(format!("-std=c++{}", self.cxx_standard));
        build.compile(&self.library_name);

        self.emit_link_directives();
        Ok(())
    }

    fn build_with_cmake(
        &self,
        manifest_dir: &Path,
        gen_cpp_dir: &Path,
        include_dirs: &[PathBuf],
        source_files: &[PathBuf],
    ) -> io::Result<()> {
        println!("cargo:warning=[Phase 3] Native backend: cmake");

        let cmake_lists = render_cmake_lists(
            &self.library_name,
            self.cxx_standard,
            include_dirs,
            source_files,
            &self.defines,
            &self.link_libs,
            &self.packages,
        );
        let cmake_path = gen_cpp_dir.join("CMakeLists.txt");
        fs::write(&cmake_path, cmake_lists)?;
        println!(
            "cargo:warning=[Phase 3] Generated CMakeLists: {}",
            cmake_path.display()
        );

        let dst = cmake::Config::new(gen_cpp_dir)
            .define("CMAKE_EXPORT_COMPILE_COMMANDS", "ON")
            .define("CMAKE_POSITION_INDEPENDENT_CODE", "ON")
            .build_target(&self.library_name)
            .build();

        let build_dir = dst.join("build");
        let source_json = build_dir.join("compile_commands.json");
        let dest_json = manifest_dir.join("compile_commands.json");
        if source_json.exists() {
            let _ = fs::copy(&source_json, &dest_json);
        }

        for link_dir in candidate_cmake_link_dirs(&dst) {
            println!("cargo:rustc-link-search=native={}", link_dir.display());
        }
        println!("cargo:rustc-link-lib=static={}", self.library_name);
        self.emit_link_directives();
        Ok(())
    }

    fn emit_link_directives(&self) {
        for dir in &self.link_search_dirs {
            println!("cargo:rustc-link-search=native={}", dir.display());
        }
        for lib in &self.link_libs {
            println!("cargo:rustc-link-lib={lib}");
        }
    }
}

fn discover_generated_ffi_sources(gen_cpp_dir: &Path) -> io::Result<Vec<PathBuf>> {
    let mut files = Vec::new();
    if !gen_cpp_dir.exists() {
        return Ok(files);
    }
    for entry in fs::read_dir(gen_cpp_dir)? {
        let path = entry?.path();
        let is_ffi = path
            .file_name()
            .and_then(|n| n.to_str())
            .is_some_and(|name| name.ends_with("_ffi.cpp"));
        if is_ffi {
            files.push(path);
        }
    }
    files.sort();
    Ok(files)
}

fn find_roplat_cpp_include(manifest_dir: &Path) -> io::Result<PathBuf> {
    for ancestor in manifest_dir.ancestors() {
        for include_dir in [
            ancestor.join("roplat_cpp").join("include"),
            ancestor.join("roplat").join("roplat_cpp").join("include"),
        ] {
            if include_dir.join("roplat").join("node.h").exists() {
                return Ok(include_dir);
            }
        }
    }
    Err(io::Error::new(
        io::ErrorKind::NotFound,
        format!(
            "could not locate roplat_cpp/include from manifest dir {}",
            manifest_dir.display()
        ),
    ))
}

fn resolve_paths(manifest_dir: &Path, paths: &[PathBuf]) -> Vec<PathBuf> {
    paths
        .iter()
        .map(|path| resolve_path(manifest_dir, path))
        .collect()
}

fn resolve_path(manifest_dir: &Path, path: &Path) -> PathBuf {
    if path.is_absolute() {
        path.to_path_buf()
    } else {
        manifest_dir.join(path)
    }
}

fn normalize_cmake_path(path: &Path) -> String {
    path.to_string_lossy().replace('\\', "/")
}

fn candidate_cmake_link_dirs(dst: &Path) -> Vec<PathBuf> {
    let mut dirs = Vec::new();
    for path in [
        dst.join("lib"),
        dst.join("build"),
        dst.join("build").join("Debug"),
        dst.join("build").join("Release"),
        dst.join("build").join("RelWithDebInfo"),
        dst.join("build").join("MinSizeRel"),
    ] {
        if path.exists() && !dirs.contains(&path) {
            dirs.push(path);
        }
    }
    if dirs.is_empty() {
        dirs.push(dst.join("lib"));
    }
    dirs
}

fn render_cmake_lists(
    library_name: &str,
    cxx_standard: u32,
    include_dirs: &[PathBuf],
    source_files: &[PathBuf],
    defines: &[(String, Option<String>)],
    link_libs: &[String],
    packages: &[NativePackage],
) -> String {
    let mut out = String::new();
    out.push_str("# Auto-generated by roplat_build - DO NOT EDIT\n");
    out.push_str("cmake_minimum_required(VERSION 3.16)\n\n");
    out.push_str("project(roplat_native_bridge LANGUAGES CXX)\n\n");
    out.push_str(&format!("set(CMAKE_CXX_STANDARD {cxx_standard})\n"));
    out.push_str("set(CMAKE_CXX_STANDARD_REQUIRED ON)\n");
    out.push_str("set(CMAKE_POSITION_INDEPENDENT_CODE ON)\n");
    out.push_str("set(CMAKE_EXPORT_COMPILE_COMMANDS ON)\n\n");

    for package in packages {
        out.push_str("find_package(");
        out.push_str(&package.name);
        if let Some(version) = &package.version {
            out.push(' ');
            out.push_str(version);
        }
        out.push_str(" REQUIRED");
        if !package.components.is_empty() {
            out.push_str(" COMPONENTS ");
            out.push_str(&package.components.join(" "));
        }
        out.push_str(")\n");
    }
    if !packages.is_empty() {
        out.push('\n');
    }

    out.push_str(&format!("add_library({library_name} STATIC\n"));
    for file in source_files {
        out.push_str(&format!("  \"{}\"\n", normalize_cmake_path(file)));
    }
    out.push_str(")\n\n");

    out.push_str(&format!(
        "target_include_directories({library_name} PRIVATE\n"
    ));
    for dir in include_dirs {
        out.push_str(&format!("  \"{}\"\n", normalize_cmake_path(dir)));
    }
    out.push_str(")\n");

    if !defines.is_empty() {
        out.push_str(&format!(
            "target_compile_definitions({library_name} PRIVATE\n"
        ));
        for (key, value) in defines {
            match value {
                Some(value) => out.push_str(&format!("  {}={}\n", key, value)),
                None => out.push_str(&format!("  {}\n", key)),
            }
        }
        out.push_str(")\n");
    }

    let mut all_links = link_libs.to_vec();
    for package in packages {
        all_links.extend(package.link_targets.iter().cloned());
    }
    if !all_links.is_empty() {
        out.push_str(&format!("target_link_libraries({library_name} PRIVATE\n"));
        for link in all_links {
            out.push_str(&format!("  {}\n", link));
        }
        out.push_str(")\n");
    }

    out.push_str("\ninstall(TARGETS ");
    out.push_str(library_name);
    out.push_str("\n    ARCHIVE DESTINATION lib\n    LIBRARY DESTINATION lib\n    RUNTIME DESTINATION bin\n)\n");
    out
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn native_backend_parses_from_strings() {
        assert_eq!(NativeBackend::parse("cc"), Some(NativeBackend::Cc));
        assert_eq!(NativeBackend::parse("cmake"), Some(NativeBackend::CMake));
        assert_eq!(NativeBackend::parse("unknown"), None);
    }

    #[test]
    fn resolve_path_keeps_absolute_paths_and_roots_relative_paths() {
        let manifest_dir = Path::new("E:/workspace/my_crate");
        assert_eq!(
            resolve_path(manifest_dir, Path::new("cpp/include")),
            PathBuf::from("E:/workspace/my_crate").join("cpp/include")
        );

        let absolute = PathBuf::from("E:/sdk/roplat_cpp/include");
        assert_eq!(resolve_path(manifest_dir, &absolute), absolute);
    }

    #[test]
    fn render_cmake_lists_contains_expected_sections() {
        let package = NativePackage::new("Eigen3")
            .version("3.3")
            .component("Core")
            .link_target("Eigen3::Eigen");
        let cmake = render_cmake_lists(
            "roplat_nodes_cpp",
            20,
            &[PathBuf::from("E:/inc")],
            &[PathBuf::from("E:/src/node.cpp")],
            &[("USE_NATIVE".to_string(), None)],
            &["opencv_core".to_string()],
            &[package],
        );

        assert!(cmake.contains("project(roplat_native_bridge LANGUAGES CXX)"));
        assert!(cmake.contains("find_package(Eigen3 3.3 REQUIRED COMPONENTS Core)"));
        assert!(cmake.contains("add_library(roplat_nodes_cpp STATIC"));
        assert!(cmake.contains("target_include_directories(roplat_nodes_cpp PRIVATE"));
        assert!(cmake.contains("target_compile_definitions(roplat_nodes_cpp PRIVATE"));
        assert!(cmake.contains("Eigen3::Eigen"));
    }
}
