use std::{collections::HashMap, path::Path};

use object::{read::archive::ArchiveFile, Object as _, ObjectSection as _, ObjectSymbol as _};
use thiserror::Error;

use super::{cargo_directive, warn};

pub fn fetch_archive_metadata(
    static_lib_path: &Path,
    mod_hint: &str,
    symbol_hint: &str,
) -> Option<HashMap<String, String>> {
    let lib_data = std::fs::read(static_lib_path).expect("Unable to read static lib file.");

    if ::std::env::var("DITTO_LOCAL_BUILD").as_deref() == Ok("1") && lib_data.is_empty() {
        // Dummy `libdittoffi.a` case; do not emit a warning in this case:
        eprintln!("Dummy (empty) `libdittoffi.a` found, skipping metadata-fetching.");
        return None;
    }

    let mut archive_fetcher =
        match ArchiveMetadataFetcher::new(&lib_data, mod_hint.to_string(), symbol_hint.to_string())
        {
            Ok(x) => Some(x),
            Err(err) => {
                warn!("Error creating archive metadata fetcher: {err}");
                None
            }
        }?;
    if let Err(err) = archive_fetcher.find_metadata() {
        warn!("Error fetching metadata from archive: {err}");
        return None;
    };
    Some(archive_fetcher.metadata)
}

/// A fetcher for retrieving archive metadata from an object archive.
///
/// "Metadata" can be any data the library user finds useful to fetch from a static library before
/// linking time. The format that this fetcher is expecting is for the metadata to be static C
/// formated strings. This means that the strings must end in "\0"
///
/// This only works for archive files that contain object files with 32 and 64 bit addresses.
struct ArchiveMetadataFetcher<'a> {
    data: &'a [u8],
    /// Fetcher will look in archive members that contain this hint to find archive metadata.
    mod_hint: String,
    /// Fetcher will look for symbols that contain this hint to find archive metadata.
    ///
    /// Capitalization matters here.
    symbol_hint: String,
    /// Metadata retrived by fetcher.
    pub metadata: HashMap<String, String>,
}

impl<'a> ArchiveMetadataFetcher<'a> {
    pub fn new(
        data: &'a [u8],
        mod_hint: String,
        symbol_hint: String,
    ) -> Result<Self, ArchiveMetadataError> {
        Ok(Self {
            data,
            mod_hint,
            symbol_hint,
            metadata: HashMap::new(),
        })
    }
    /// Conducts a best effort search of the archive file data for symbols that contain the
    /// symbol_hint in archive members that contain the mod_hint. This search is best effort
    /// because it can miss symbols that are not exactly what the struct is expecting (strings that
    /// end with \0)
    pub fn find_metadata(&mut self) -> Result<(), ArchiveMetadataError> {
        let archive = ArchiveFile::parse(self.data)?;

        // Iterate only over members that have readable names that contain the mod hint.
        for member in archive
            .members()
            .filter_map(|mem_res| mem_res.ok())
            .filter(|mem| String::from_utf8_lossy(mem.name()).contains(&self.mod_hint))
        {
            let name = String::from_utf8_lossy(member.name());
            // Parse the object file within the archive
            let Ok(data) = member.data(self.data) else {
                eprintln!("Failed to retrieve member data for {name}");
                continue;
            };

            let Ok(obj) = object::File::parse(data) else {
                eprintln!("Failed to parse object file for {name}");
                continue;
            };

            let Some(addr_size) = obj.architecture().address_size() else {
                eprintln!("{name} has an unknown architecture.");
                continue;
            };

            if (addr_size as u8) < 4 {
                eprintln!("{name} has an address size smaller than 32 bits.");
                continue;
            }

            // Filter out symbols that contain the symbol hint.
            for symbol in obj
                .symbols()
                .filter(|s| s.name().ok().is_some_and(|n| n.contains(&self.symbol_hint)))
            {
                let name = symbol.name().expect("BUG: Symbol name was just checked");

                let Some(section_idx) = symbol.section_index() else {
                    eprintln!("{name} does not have a section index.");
                    continue;
                };

                let Some(section) = obj.section_by_index(section_idx).ok() else {
                    eprintln!("Could not find section for {name}");
                    continue;
                };

                let Ok(section_data) = section.data() else {
                    eprintln!("Failed to retrieve data from {:?}", section.name());
                    continue;
                };

                // The symbol address is the absolute address, need to get offset within const
                // section
                let data_offset = (symbol.address() - section.address()) as usize;

                // Read from const_offset until null terminator
                let string_bytes = &section_data[data_offset..];

                // Try to parse as a C-style null-terminated string
                let cstr = match std::ffi::CStr::from_bytes_until_nul(string_bytes) {
                    Ok(c) => c,
                    Err(err) => {
                        eprintln!("failed to parse bytes into C string - {err}");
                        continue;
                    }
                };

                let metadata_str = match cstr.to_str() {
                    Ok(s) => s,
                    Err(err) => {
                        eprintln!("Failed to parse cstr to &str - {err}");
                        continue;
                    }
                };

                let name_trimmed = name.trim_matches([' ', '_']).to_string();

                eprintln!("Found archive metadata - {name_trimmed} : {metadata_str}");

                self.metadata.insert(
                    // Trim off spaces and underscores to normalize across platforms.
                    name_trimmed,
                    metadata_str.to_string(),
                );
            }
        }
        eprintln!("Found archive metadata: {:#?}", self.metadata);
        Ok(())
    }
}

#[derive(Error, Debug)]
enum ArchiveMetadataError {
    #[error(transparent)]
    ObjectReadError(#[from] object::read::Error),
}
