//! Build script for pjsua bindings
//!
//! This script builds pjproject from source if not found, then generates
//! Rust bindings using bindgen.
//!
//! Set PJPROJECT_DIR to a pre-built pjproject install prefix to skip the
//! cmake build (used in Docker to separate the slow C build into its own layer).

use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;

fn main() {
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-env-changed=PJPROJECT_DIR");

    let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());

    // If PJPROJECT_DIR is set, use pre-built pjproject (e.g. from a separate Docker stage).
    // Otherwise build from source via cmake.
    let include_paths = if let Ok(prefix) = env::var("PJPROJECT_DIR") {
        let prefix = PathBuf::from(&prefix);
        println!(
            "cargo:warning=Using pre-built pjproject from: {}",
            prefix.display()
        );

        let lib_dir = prefix.join("lib");
        println!("cargo:rustc-link-search=native={}", lib_dir.display());

        // Link libraries in the correct dependency order (same as build-from-source path)
        let pj_libs = [
            "pjsua-lib",
            "pjsua2",
            "pjsip-ua",
            "pjsip-simple",
            "pjsip",
            "pjmedia-codec",
            "pjmedia",
            "pjmedia-audiodev",
            "pjnath",
            "pjlib-util",
            "pjlib",
            "srtp",
            "resample",
            "speex",
            "g7221",
            "gsm",
            "ilbc",
        ];
        for lib in &pj_libs {
            println!("cargo:rustc-link-lib=static={}", lib);
        }

        // cmake --install may place headers in a multiarch subdirectory
        // (e.g. include/aarch64-linux-gnu/) instead of include/ directly.
        // Scan for the actual pjsua-lib directory.
        let base_include = prefix.join("include");
        let mut include_dirs = vec![base_include.clone()];
        if let Ok(entries) = std::fs::read_dir(&base_include) {
            for entry in entries.flatten() {
                let path = entry.path();
                if path.is_dir() && path.join("pjsua-lib/pjsua.h").exists() {
                    include_dirs.insert(0, path);
                }
            }
        }
        include_dirs
    } else {
        build_from_source(&out_dir)
    };

    // ---- System libraries (common to both paths) ----

    #[cfg(target_os = "macos")]
    {
        println!("cargo:rustc-link-lib=framework=AudioToolbox");
        println!("cargo:rustc-link-lib=framework=AudioUnit");
        println!("cargo:rustc-link-lib=framework=CoreAudio");
        println!("cargo:rustc-link-lib=framework=CoreServices");
        println!("cargo:rustc-link-lib=framework=Foundation");
        println!("cargo:rustc-link-lib=framework=AVFoundation");
        println!("cargo:rustc-link-lib=framework=CoreMedia");
        println!("cargo:rustc-link-lib=framework=CoreVideo");
        println!("cargo:rustc-link-lib=framework=VideoToolbox");
        println!("cargo:rustc-link-lib=framework=Security");
    }

    #[cfg(target_os = "linux")]
    {
        println!("cargo:rustc-link-lib=asound");
        println!("cargo:rustc-link-lib=pthread");
        println!("cargo:rustc-link-lib=m");
        println!("cargo:rustc-link-lib=rt");
        println!("cargo:rustc-link-lib=uuid");
        println!("cargo:rustc-link-lib=opencore-amrnb");
        println!("cargo:rustc-link-lib=opencore-amrwb");
        println!("cargo:rustc-link-lib=opus");
    }

    // OpenSSL
    #[cfg(target_os = "macos")]
    {
        let openssl_paths = [
            "/opt/homebrew/opt/openssl@3/lib",
            "/opt/homebrew/opt/openssl/lib",
            "/usr/local/opt/openssl@3/lib",
            "/usr/local/opt/openssl/lib",
        ];
        for path in &openssl_paths {
            if std::path::Path::new(path).exists() {
                println!("cargo:rustc-link-search=native={}", path);
                break;
            }
        }

        let amr_paths = [
            "/opt/homebrew/opt/opencore-amr/lib",
            "/usr/local/opt/opencore-amr/lib",
        ];
        for path in &amr_paths {
            if std::path::Path::new(path).exists() {
                println!("cargo:rustc-link-search=native={}", path);
                println!("cargo:rustc-link-lib=opencore-amrnb");
                println!("cargo:rustc-link-lib=opencore-amrwb");
                break;
            }
        }

        let opus_paths = ["/opt/homebrew/opt/opus/lib", "/usr/local/opt/opus/lib"];
        for path in &opus_paths {
            if std::path::Path::new(path).exists() {
                println!("cargo:rustc-link-search=native={}", path);
                println!("cargo:rustc-link-lib=opus");
                break;
            }
        }
    }

    println!("cargo:rustc-link-lib=ssl");
    println!("cargo:rustc-link-lib=crypto");

    #[cfg(target_os = "macos")]
    println!("cargo:rustc-link-lib=c++");
    #[cfg(target_os = "linux")]
    println!("cargo:rustc-link-lib=stdc++");

    // ---- Generate bindings ----

    let mut clang_args = Vec::new();

    for path in &include_paths {
        clang_args.push(format!("-I{}", path.display()));
    }

    #[cfg(target_endian = "little")]
    {
        clang_args.push("-DPJ_IS_LITTLE_ENDIAN=1".to_string());
        clang_args.push("-DPJ_IS_BIG_ENDIAN=0".to_string());
    }
    #[cfg(target_endian = "big")]
    {
        clang_args.push("-DPJ_IS_LITTLE_ENDIAN=0".to_string());
        clang_args.push("-DPJ_IS_BIG_ENDIAN=1".to_string());
    }

    #[cfg(target_os = "macos")]
    {
        clang_args.push("-DPJ_DARWINOS=1".to_string());
        clang_args.push("-DPJ_HAS_LIMITS_H=1".to_string());
    }
    #[cfg(target_os = "linux")]
    {
        clang_args.push("-DPJ_LINUX=1".to_string());
        clang_args.push("-DPJ_HAS_LIMITS_H=1".to_string());
    }

    #[cfg(target_pointer_width = "64")]
    clang_args.push("-DPJ_HAS_INT64=1".to_string());

    clang_args.push("-DPJ_AUTOCONF=1".to_string());

    let pjsua_header = include_paths
        .iter()
        .find_map(|p| {
            let header = p.join("pjsua-lib/pjsua.h");
            if header.exists() {
                return Some(header);
            }
            let header = p.join("pjsua.h");
            if header.exists() {
                Some(header)
            } else {
                None
            }
        })
        .expect("Could not find pjsua.h header in installed location");

    println!(
        "cargo:warning=Using pjsua.h from: {}",
        pjsua_header.display()
    );
    println!("cargo:warning=Include paths: {:?}", include_paths);

    let bindings = bindgen::Builder::default()
        .header(pjsua_header.to_str().unwrap())
        .clang_args(&clang_args)
        .generate_comments(false)
        .allowlist_type(r"pj.*")
        .allowlist_type(r"PJ.*")
        .allowlist_var(r"pj.*")
        .allowlist_var(r"PJ.*")
        .allowlist_function(r"pj.*")
        .allowlist_function(r"PJ.*")
        .generate()
        .expect("Unable to generate bindings");

    let bindings_path = out_dir.join("bindings.rs");
    bindings
        .write_to_file(&bindings_path)
        .expect("Couldn't write bindings!");

    println!(
        "cargo:warning=Bindings written to: {}",
        bindings_path.display()
    );
}

/// Build pjproject from source and return include paths.
fn build_from_source(out_dir: &Path) -> Vec<PathBuf> {
    let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
    let pjproject_src = manifest_dir.join("pjproject");

    let pjproject_build = out_dir.join("pjproject-build");
    let pjproject_install = out_dir.join("pjproject-install");

    std::fs::create_dir_all(&pjproject_build).expect("Failed to create build directory");
    std::fs::create_dir_all(&pjproject_install).expect("Failed to create install directory");

    let include_dir = pjproject_install.join("include");
    let lib_dir = pjproject_install.join("lib");

    build_pjproject(&pjproject_src, &pjproject_build, &pjproject_install);

    let include_paths = vec![include_dir.clone()];
    let lib_paths = vec![lib_dir.clone()];

    // Set up library paths
    for path in &lib_paths {
        println!("cargo:rustc-link-search=native={}", path.display());
    }

    // For built-from-source pjproject, libraries are in the build directory subdirs
    let pjproject_build_for_libs = out_dir.join("pjproject-build");
    if pjproject_build_for_libs.exists() {
        let lib_subdirs = [
            "pjlib",
            "pjlib-util",
            "pjmedia",
            "pjnath",
            "pjsip",
            "third_party/resample",
            "third_party/speex",
            "third_party/g7221",
            "third_party/yuv",
            "third_party/gsm",
            "third_party/srtp",
            "third_party/ilbc",
        ];

        for subdir in &lib_subdirs {
            let lib_path = pjproject_build_for_libs.join(subdir);
            if lib_path.exists() {
                println!("cargo:rustc-link-search=native={}", lib_path.display());
            }
        }

        // Link libraries in the correct order (dependencies matter!)
        let pj_libs = [
            "pjsua-lib",        // main pjsua library
            "pjsua2",           // C++ wrapper (may be needed)
            "pjsip-ua",         // SIP user agent
            "pjsip-simple",     // SIP SIMPLE presence
            "pjsip",            // Core SIP
            "pjmedia-codec",    // Media codecs
            "pjmedia",          // Media framework
            "pjmedia-audiodev", // Audio device
            "pjnath",           // NAT traversal
            "pjlib-util",       // Utility functions
            "pjlib",            // Core library
            // Third party
            "srtp",
            "resample",
            "speex",
            "g7221",
            "gsm",
            "ilbc",
        ];

        for lib in &pj_libs {
            println!("cargo:rustc-link-lib=static={}", lib);
        }
    } else {
        // Link against pjproject libraries from install directory (static)
        for lib_path in &lib_paths {
            if let Ok(entries) = std::fs::read_dir(lib_path) {
                for entry in entries.flatten() {
                    let path = entry.path();
                    if let Some(ext) = path.extension() {
                        if ext == "a" {
                            if let Some(name) = path.file_stem() {
                                let name = name.to_string_lossy();
                                if name.starts_with("lib") {
                                    let lib_name = name.strip_prefix("lib").unwrap();
                                    println!("cargo:rustc-link-lib=static={}", lib_name);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    include_paths
}

fn build_pjproject(
    pjproject_src: &std::path::Path,
    pjproject_build: &std::path::Path,
    pjproject_install: &std::path::Path,
) {
    // Check for .pc file in build dir (CMake install doesn't always copy it to install dir)
    let pc_file = pjproject_build.join("libpjproject.pc");

    if !pc_file.exists() {
        println!("cargo:warning=Building pjproject from source (this may take several minutes)...");

        // Detect cross-compilation target
        let target = env::var("TARGET").unwrap_or_default();
        let host = env::var("HOST").unwrap_or_default();
        let is_cross = target != host;

        // Collect C/CXX flags — merged at the end into CMAKE_C_FLAGS/CMAKE_CXX_FLAGS.
        // pjsua.h guards PJSUA_MAX_CALLS with #ifndef, so -D on the command line wins.
        let mut c_flags: Vec<&str> = vec!["-DPJSUA_MAX_CALLS=128"];

        let mut cmake_args = vec![
            "-G".to_string(),
            "Unix Makefiles".to_string(),
            format!("-DCMAKE_INSTALL_PREFIX={}", pjproject_install.display()),
            "-DCMAKE_BUILD_TYPE=Release".to_string(),
            "-DBUILD_SHARED_LIBS=OFF".to_string(),
            "-DPJ_SKIP_EXPERIMENTAL_NOTICE=ON".to_string(),
            // Disable tests to avoid linking issues with cross-compilation
            "-DPJ_ENABLE_TESTS=OFF".to_string(),
            "-DBUILD_TESTING=OFF".to_string(),
            // Disable video support
            "-DPJMEDIA_WITH_VIDEO=OFF".to_string(),
            "-DPJMEDIA_WITH_FFMPEG=OFF".to_string(),
            "-DPJMEDIA_WITH_LIBYUV=OFF".to_string(),
            // Enable AMR codecs (IMS/MR-NB support)
            "-DPJMEDIA_WITH_OPENCORE_AMRNB_CODEC=ON".to_string(),
            "-DPJMEDIA_WITH_OPENCORE_AMRWB_CODEC=ON".to_string(),
            // Enable Opus codec
            "-DPJMEDIA_WITH_OPUS_CODEC=ON".to_string(),
            // Enable TLS/SSL support with OpenSSL
            "-DPJLIB_WITH_SSL=openssl".to_string(),
        ];

        // Configure cross-compilation toolchain
        if is_cross {
            println!("cargo:warning=Cross-compiling for {} from {}", target, host);

            // Map Rust target to cross-compiler prefix
            let cross_prefix = match target.as_str() {
                "aarch64-unknown-linux-gnu" => "aarch64-linux-gnu",
                "x86_64-unknown-linux-gnu" => "x86_64-linux-gnu",
                _ => "",
            };

            if !cross_prefix.is_empty() {
                let cc = format!("{}-gcc", cross_prefix);
                let cxx = format!("{}-g++", cross_prefix);

                // Check if cross-compiler exists
                if std::process::Command::new("which")
                    .arg(&cc)
                    .output()
                    .map(|o| o.status.success())
                    .unwrap_or(false)
                {
                    cmake_args.push(format!("-DCMAKE_C_COMPILER={}", cc));
                    cmake_args.push(format!("-DCMAKE_CXX_COMPILER={}", cxx));
                    println!("cargo:warning=Using cross-compiler: {}", cc);

                    // ARM64: Fix atomic alignment issues
                    // 1. -mno-outline-atomics: Use inline atomics instead of helper functions
                    // 2. -DPJ_POOL_ALIGNMENT=8: Force pjlib pool to use 8-byte alignment (C define)
                    if target.contains("aarch64") {
                        c_flags.push("-mno-outline-atomics");
                        c_flags.push("-DPJ_POOL_ALIGNMENT=8");
                        println!(
                            "cargo:warning=ARM64: Using inline atomics with 8-byte pool alignment"
                        );
                    }

                    // The cross-compiler (from crossbuild-essential-arm64) has --sysroot=/usr/aarch64-linux-gnu
                    // baked into its specs, but the actual libraries are in /usr/lib/aarch64-linux-gnu/
                    // via Debian's multiarch. We must override the sysroot to "/" so the linker
                    // finds libc at /lib/aarch64-linux-gnu/ instead of the non-existent
                    // /usr/aarch64-linux-gnu/lib/libc.so.6
                    cmake_args.push("-DCMAKE_SYSROOT=/".to_string());
                    println!("cargo:warning=Overriding sysroot to / for multiarch compatibility");

                    // Help CMake find cross-compiled libraries in multiarch paths
                    let multiarch_lib = format!("/usr/lib/{}", cross_prefix);
                    if std::path::Path::new(&multiarch_lib).exists() {
                        cmake_args.push(format!("-DCMAKE_FIND_ROOT_PATH=/usr;{}", multiarch_lib));
                        cmake_args.push("-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=BOTH".to_string());
                        cmake_args.push("-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH".to_string());
                        cmake_args.push("-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER".to_string());
                        println!(
                            "cargo:warning=Using multiarch library path: {}",
                            multiarch_lib
                        );

                        // Explicitly set OpenSSL paths for cross-compilation
                        let openssl_ssl = format!("{}/libssl.so", multiarch_lib);
                        let openssl_crypto = format!("{}/libcrypto.so", multiarch_lib);
                        if std::path::Path::new(&openssl_ssl).exists() {
                            cmake_args.push("-DOPENSSL_ROOT_DIR=/usr".to_string());
                            cmake_args.push("-DOPENSSL_INCLUDE_DIR=/usr/include".to_string());
                            cmake_args.push(format!("-DOPENSSL_SSL_LIBRARY={}", openssl_ssl));
                            cmake_args.push(format!("-DOPENSSL_CRYPTO_LIBRARY={}", openssl_crypto));
                            println!(
                                "cargo:warning=Using cross-compiled OpenSSL from {}",
                                multiarch_lib
                            );
                        }

                        // Explicitly set Opus paths for cross-compilation
                        let opus_lib = format!("{}/libopus.so", multiarch_lib);
                        if std::path::Path::new(&opus_lib).exists() {
                            cmake_args.push("-DOPUS_INCLUDE_DIR=/usr/include".to_string());
                            cmake_args.push(format!("-DOPUS_LIBRARY={}", opus_lib));
                            println!(
                                "cargo:warning=Using cross-compiled Opus from {}",
                                multiarch_lib
                            );
                        }
                    }
                }
            }
        } else {
            // Native build - find OpenSSL in standard locations
            let openssl_prefixes = if cfg!(target_os = "macos") {
                vec![
                    "/opt/homebrew/opt/openssl@3",
                    "/opt/homebrew/opt/openssl",
                    "/usr/local/opt/openssl@3",
                    "/usr/local/opt/openssl",
                ]
            } else {
                vec!["/usr", "/usr/local"]
            };

            for prefix in &openssl_prefixes {
                let include_path = format!("{}/include", prefix);
                if std::path::Path::new(&include_path)
                    .join("openssl/ssl.h")
                    .exists()
                {
                    println!("cargo:warning=Found OpenSSL at: {}", prefix);
                    cmake_args.push(format!("-DOPENSSL_ROOT_DIR={}", prefix));
                    if cfg!(target_os = "macos") {
                        cmake_args.push(format!("-DOPENSSL_INCLUDE_DIR={}", include_path));
                        let lib_path = format!("{}/lib", prefix);
                        let static_crypto = format!("{}/libcrypto.a", lib_path);
                        let static_ssl = format!("{}/libssl.a", lib_path);
                        if std::path::Path::new(&static_crypto).exists() {
                            cmake_args.push(format!("-DOPENSSL_CRYPTO_LIBRARY={}", static_crypto));
                            cmake_args.push(format!("-DOPENSSL_SSL_LIBRARY={}", static_ssl));
                        }
                    }
                    break;
                }
            }

            // Native build - find Opus codec library
            let opus_prefixes = if cfg!(target_os = "macos") {
                vec!["/opt/homebrew/opt/opus", "/usr/local/opt/opus"]
            } else {
                vec!["/usr", "/usr/local"]
            };

            for prefix in &opus_prefixes {
                let include_path = format!("{}/include", prefix);
                if std::path::Path::new(&include_path)
                    .join("opus/opus.h")
                    .exists()
                {
                    println!("cargo:warning=Found Opus at: {}", prefix);
                    cmake_args.push(format!("-DOPUS_INCLUDE_DIR={}", include_path));
                    let lib_path = format!("{}/lib", prefix);
                    let opus_lib = if cfg!(target_os = "macos") {
                        format!("{}/libopus.a", lib_path)
                    } else {
                        format!("{}/libopus.so", lib_path)
                    };
                    if std::path::Path::new(&opus_lib).exists() {
                        cmake_args.push(format!("-DOPUS_LIBRARY={}", opus_lib));
                    }
                    break;
                }
            }
        }

        // Merge all collected C/CXX flags into cmake args
        if !c_flags.is_empty() {
            let flags = c_flags.join(" ");
            println!("cargo:warning=C flags: {}", flags);
            cmake_args.push(format!("-DCMAKE_C_FLAGS={}", flags));
            cmake_args.push(format!("-DCMAKE_CXX_FLAGS={}", flags));
        }

        cmake_args.push(pjproject_src.to_str().unwrap().to_string());

        // Run CMake configure
        let cmake_result = Command::new("cmake")
            .current_dir(pjproject_build)
            .args(&cmake_args)
            .output()
            .expect("Failed to run cmake configure");

        if !cmake_result.status.success() {
            eprintln!(
                "CMake configure stdout: {}",
                String::from_utf8_lossy(&cmake_result.stdout)
            );
            eprintln!(
                "CMake configure stderr: {}",
                String::from_utf8_lossy(&cmake_result.stderr)
            );
            panic!("CMake configure failed");
        }

        // Get number of CPUs for parallel build
        let num_cpus = std::thread::available_parallelism()
            .map(|n| n.get())
            .unwrap_or(4);

        // Run CMake build - only build the libraries we need, not sample apps
        println!(
            "cargo:warning=Compiling pjproject with {} threads...",
            num_cpus
        );
        let mut build_args = vec![
            "--build".to_string(),
            ".".to_string(),
            "--config".to_string(),
            "Release".to_string(),
        ];

        // Specify only the library targets we need
        let targets = [
            "pjlib",
            "pjlib-util",
            "pjnath",
            "pjmedia",
            "pjmedia-audiodev",
            "pjmedia-codec",
            "pjsip",
            "pjsip-simple",
            "pjsip-ua",
            "pjsua-lib",
            "pjsua2",
            "resample",
            "srtp",
            "speex",
            "g7221",
            "gsm",
            "ilbc",
        ];
        for target in &targets {
            build_args.push("--target".to_string());
            build_args.push(target.to_string());
        }
        build_args.push("-j".to_string());
        build_args.push(num_cpus.to_string());

        let build_result = Command::new("cmake")
            .current_dir(pjproject_build)
            .args(&build_args)
            .output()
            .expect("Failed to run cmake build");

        if !build_result.status.success() {
            eprintln!(
                "CMake build stdout: {}",
                String::from_utf8_lossy(&build_result.stdout)
            );
            eprintln!(
                "CMake build stderr: {}",
                String::from_utf8_lossy(&build_result.stderr)
            );
            panic!("CMake build failed");
        }
        println!("cargo:warning=Library builds complete");

        // Run CMake install - may fail for sample apps but that's OK
        let install_result = Command::new("cmake")
            .current_dir(pjproject_build)
            .args(["--install", "."])
            .output()
            .expect("Failed to run cmake install");

        if !install_result.status.success() {
            // Install might fail for sample apps we didn't build, but libraries are installed
            println!("cargo:warning=CMake install had errors (OK if only sample apps failed)");
        }

        println!("cargo:warning=pjproject build complete!");
    } else {
        println!("cargo:warning=Using cached pjproject build");
    }
}
