use std::{
    env,
    ffi::{OsStr, OsString},
    process::Command,
};

use crate::{
    binary_file, collect_audit_data,
    platform_detection::{is_32bit_x86, is_apple, is_msvc, is_wasm},
    rustc_arguments::{self, should_embed_audit_data},
    target_info,
};

use std::io::BufRead;

pub fn main(rustc_path: &OsStr) {
    let mut command = match rustc_command_with_audit_data(rustc_path) {
        Some(cmd) => cmd,
        None => rustc_command(rustc_path), // could not construct command that injects audit data, skip it
    };

    // Invoke rustc
    let results = command.status().unwrap_or_else(|err| {
        let mut command_with_args: Vec<&OsStr> = vec![command.get_program()];
        command_with_args.extend(command.get_args());
        eprintln!(
            "Failed to invoke rustc! Make sure it's in your $PATH\n\
                The error was: {err}\n\
                The attempted call was: {command_with_args:?}",
        );
        std::process::exit(1);
    });
    let code = results
        .code()
        .expect("rustc was terminated by a deadly signal");
    std::process::exit(code);
}

/// Creates a rustc command line and populates arguments from arguments passed to us.
fn rustc_command(rustc_path: &OsStr) -> Command {
    let mut command = Command::new(rustc_path);
    // Pass along all the arguments that Cargo meant to pass to rustc
    // We skip the path to our binary as well as the first argument passed by Cargo,
    // which is the path to rustc to use (or just "rustc")
    command.args(env::args_os().skip(2));
    command
}

/// Returns the default target triple for the rustc we're running
fn rustc_host_target_triple(rustc_path: &OsStr) -> String {
    Command::new(rustc_path)
        .arg("-vV")
        .output()
        .expect("Failed to invoke rustc! Is it in your $PATH?")
        .stdout
        .lines()
        .map(|l| l.unwrap())
        .find(|l| l.starts_with("host: "))
        .map(|l| l[6..].to_string())
        .expect("Failed to parse rustc output to determine the current platform. Please report this bug!")
}

fn rustc_command_with_audit_data(rustc_path: &OsStr) -> Option<Command> {
    let mut command = rustc_command(rustc_path);

    // Only inject audit data if CARGO_PRIMARY_PACKAGE is set.
    // This allows linking audit data only in toplevel binaries, not intermediate artifacts.
    //
    // Binaries and C dynamic libraries are not built as non-primary packages,
    // so this should not cause issues with Cargo caches.
    #[allow(clippy::question_mark)]
    if env::var_os("CARGO_PRIMARY_PACKAGE").is_none() {
        return None;
    }
    let args = rustc_arguments::parse_args().unwrap(); // descriptive enough message
    if !should_embed_audit_data(&args) {
        return None;
    }
    // Get the audit data to embed
    let target_triple = args
        .target
        .clone()
        .unwrap_or_else(|| rustc_host_target_triple(rustc_path));
    let contents: Vec<u8> = collect_audit_data::compressed_dependency_list(&args, &target_triple);

    // write the audit info to an object file
    let target_info = target_info::rustc_target_info(rustc_path, &target_triple);
    let binfile = binary_file::create_binary_file(
        &target_info,
        &target_triple,
        &contents,
        "AUDITABLE_VERSION_INFO",
    );
    if let Some(file) = binfile {
        // Place the audit data in the output dir.
        // We can place it anywhere really, the only concern is clutter and name collisions,
        // and the target dir is locked so we're probably good
        let crate_name = match args.crate_name.as_deref() {
            Some(name) => name,
            None => {
                eprintln!(
                    "WARNING: cargo-auditable: rustc command is missing --crate-name\n\
                    Please double-check that the audit data was injected into the binary.\n\
                    If it wasn't, please report a bug."
                );
                return None;
            }
        };
        let out_dir = match args.out_dir.as_deref() {
            Some(name) => name,
            None => {
                eprintln!(
                    "WARNING: cargo-auditable: rustc command is missing --out-dir\n\
                    Please double-check that the audit data was injected into the binary.\n\
                    If it wasn't, please report a bug."
                );
                return None;
            }
        };
        let filename = format!("{crate_name}_audit_data.o");
        let path = out_dir.join(filename);
        std::fs::write(&path, file).expect("Unable to write output file");

        // Modify the rustc command to link the object file with audit data
        let mut linker_command = OsString::from("-Clink-arg=");
        linker_command.push(&path);
        command.arg(linker_command);
        // Prevent the symbol from being removed as unused by the linker
        if is_apple(&target_info) {
            if args.bare_linker() {
                command.arg("-Clink-arg=-u");
                command.arg("-Clink-arg=_AUDITABLE_VERSION_INFO");
            } else {
                command.arg("-Clink-arg=-Wl,-u,_AUDITABLE_VERSION_INFO");
            }
        } else if is_msvc(&target_info) {
            // On x86 MSVC, the `object` crate's CoffI386 mangling adds a `_`
            // prefix to global symbols, so the linker must reference the
            // decorated name.
            if is_32bit_x86(&target_info) {
                command.arg("-Clink-arg=/INCLUDE:_AUDITABLE_VERSION_INFO");
            } else {
                command.arg("-Clink-arg=/INCLUDE:AUDITABLE_VERSION_INFO");
            }
        } else if is_wasm(&target_info) {
            // We don't emit the symbol name in WASM, so nothing to do
        } else {
            // Unrecognized platform, assume it to be unix-like.
            // Use POSIX `-u` instead of GNU `--undefined=` for broad compatibility
            // (e.g. zig rejects the GNU form).
            if args.bare_linker() {
                command.arg("-Clink-arg=-u");
                command.arg("-Clink-arg=AUDITABLE_VERSION_INFO");
            } else {
                command.arg("-Clink-arg=-Wl,-u,AUDITABLE_VERSION_INFO");
            }
        }
        Some(command)
    } else {
        // create_binary_file() returned None, indicating an unsupported architecture
        eprintln!(
            "WARNING: cargo-auditable: target '{target_triple}' is not supported!\n\
            The build will continue, but no audit data will be injected into the binary."
        );
        None
    }
}
