//! # Kinetics Library API Module
//!
//! ## Purpose
//! This module provides a comprehensive API for accessing and searching chemical reaction databases.
//! It enables users to load kinetic libraries, search for reactions by various criteria, and retrieve
//! detailed reaction data including Arrhenius parameters and reaction mechanisms.
//!
//! ## Main Data Structures
//! - `KineticData`: Core structure that manages reaction libraries and provides search functionality
//!   - `LibKineticData`: HashMap storing reaction ID -> reaction data mappings
//!   - `HashMapOfReactantsAndProducts`: Nested HashMap for substance-based searches
//!   - `AllLibraries`: Vector of available reaction library names
//!   - Search result vectors for found reactions by products/reagents
//!
//! ## Key Logic Implementation
//! 1. **Library Loading**: Reads JSON files containing reaction databases (Reactbase.json, dict_reaction.json)
//! 2. **Multi-criteria Search**: Supports searching by reaction ID, equation, substances, or field values
//! 3. **Substance Matching**: Uses HashSet operations for efficient subset matching of reagents/products
//! 4. **Data Retrieval**: Returns structured reaction data as serde_json::Value objects
//!
//! ## Usage Pattern
//! ```rust
//! use KiThe::Kinetics::kinetics_lib_api::KineticData;
//! let mut kin_instance = KineticData::new();
//! kin_instance.open_json_files("NUIG");  // Load library
//! kin_instance.print_all_reactions();    // Get all reactions
//! kin_instance.search_reaction_by_reagents_and_products(vec!["CO".to_string()]);
//! println!("Found reactions: {:?}", kin_instance.FoundReactionsByProducts);
//! ```
//!
//! ## Interesting Features
//! - **Dual JSON Structure**: Uses separate files for kinetic parameters and substance mappings
//! - **Flexible Search**: Supports both exact matches and subset searches for reaction participants
//! - **Library Agnostic**: Can work with multiple reaction databases (NUIG, Cantera, Aramco, etc.)
//! - **Efficient Indexing**: Pre-builds equation-to-ID mappings for fast lookups

use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::io::Read;
use std::sync::{Arc, Mutex, OnceLock};

use crate::Kinetics::error::{KineticsError, KineticsResult};
use crate::library_manager::with_library_manager;
use serde_json::Value;

type LibraryReactbase = HashMap<String, HashMap<String, Value>>;
type LibraryReactionDb = HashMap<String, HashMap<String, HashMap<String, HashSet<String>>>>;

static REACTBASE_CACHE: OnceLock<Mutex<Option<Arc<LibraryReactbase>>>> = OnceLock::new();
static REACTION_DB_CACHE: OnceLock<Mutex<Option<Arc<LibraryReactionDb>>>> = OnceLock::new();

fn cache_lock_error(cache_name: &str) -> KineticsError {
    KineticsError::InvalidReactionData(format!("failed to lock {} cache", cache_name))
}

/// Load and cache the reaction parameter library once per process.
pub(crate) fn cached_reactbase() -> KineticsResult<Arc<LibraryReactbase>> {
    let cache = REACTBASE_CACHE.get_or_init(|| Mutex::new(None));
    {
        let guard = cache.lock().map_err(|_| cache_lock_error("Reactbase"))?;
        if let Some(existing) = guard.as_ref() {
            return Ok(existing.clone());
        }
    }

    let mut file = with_library_manager(|manager| File::open(manager.reactbase_path()))?;
    let mut file_contents = String::new();
    file.read_to_string(&mut file_contents)?;
    let parsed: LibraryReactbase = serde_json::from_str(&file_contents)?;
    let parsed = Arc::new(parsed);

    let mut guard = cache.lock().map_err(|_| cache_lock_error("Reactbase"))?;
    if let Some(existing) = guard.as_ref() {
        return Ok(existing.clone());
    }
    *guard = Some(parsed.clone());
    Ok(parsed)
}

/// Load and cache the reagent/product relationship database once per process.
pub(crate) fn cached_reaction_db() -> KineticsResult<Arc<LibraryReactionDb>> {
    let cache = REACTION_DB_CACHE.get_or_init(|| Mutex::new(None));
    {
        let guard = cache
            .lock()
            .map_err(|_| cache_lock_error("reaction database"))?;
        if let Some(existing) = guard.as_ref() {
            return Ok(existing.clone());
        }
    }

    let mut file = with_library_manager(|manager| File::open(manager.dict_reaction_path()))?;
    let mut file_contents = String::new();
    file.read_to_string(&mut file_contents)?;
    let parsed: LibraryReactionDb = serde_json::from_str(&file_contents)?;
    let parsed = Arc::new(parsed);

    let mut guard = cache
        .lock()
        .map_err(|_| cache_lock_error("reaction database"))?;
    if let Some(existing) = guard.as_ref() {
        return Ok(existing.clone());
    }
    *guard = Some(parsed.clone());
    Ok(parsed)
}
#[derive(Debug, Clone)]
pub struct KineticData {
    pub HashMapOfReactantsAndProducts: HashMap<String, HashMap<String, HashSet<String>>>, // {'reaction ID':{'reagent'/"product": HashSet[substance]}}
    pub lib_name: String,
    pub LibKineticData: HashMap<String, Value>, // {'reaction ID':{data of reaction}}
    pub AllLibraries: Vec<String>,              // all reaction libraries
    pub AllEquations: Vec<String>,              // all reaction equations in library
    pub EquationReactionMap: HashMap<String, String>, // {'equation':'reaction ID'}
    pub UserEquationReactionMap: HashMap<String, String>,
    pub FoundReactionsByProducts: Vec<String>,
    pub FoundReactionsByReagents: Vec<String>,
    pub FoundReactionDatasByIDs: Vec<Value>,
}
impl KineticData {
    pub fn new() -> Self {
        Self {
            HashMapOfReactantsAndProducts: HashMap::new(),
            lib_name: String::new(),
            LibKineticData: HashMap::new(),
            AllLibraries: Vec::new(),
            AllEquations: Vec::new(),
            EquationReactionMap: HashMap::new(),
            UserEquationReactionMap: HashMap::new(),
            FoundReactionsByProducts: Vec::new(),
            FoundReactionsByReagents: Vec::new(),
            FoundReactionDatasByIDs: Vec::new(),
        }
    }
    /// Load the reaction library from a JSON file by the name of the reaction library
    pub fn open_json_files(&mut self, big_mech: &str) -> KineticsResult<()> {
        self.lib_name = big_mech.to_owned();
        let reactlibrary = cached_reactbase()?;
        self.AllLibraries = reactlibrary.keys().map(|k| k.to_string()).collect();
        let library_of_kinetic_parameters = reactlibrary
            .get(big_mech)
            .ok_or_else(|| KineticsError::MissingLibrary(big_mech.to_string()))?;
        self.LibKineticData = library_of_kinetic_parameters.to_owned();
        let reaction_db = cached_reaction_db()?;
        let library_of_reagents_and_products: &HashMap<String, HashMap<String, HashSet<String>>> =
            reaction_db
                .get(big_mech)
                .ok_or_else(|| KineticsError::MissingLibrary(big_mech.to_string()))?;
        self.HashMapOfReactantsAndProducts = library_of_reagents_and_products.to_owned();
        Ok(())
    }

    /// returns vector of all reaction equations and HashMap {reaction equation : reaction ID}
    pub fn print_all_reactions(&mut self) -> KineticsResult<()> {
        let all_reactions: Vec<String> =
            self.LibKineticData.keys().map(|k| k.to_string()).collect();
        let all_equations: Vec<String> = self
            .LibKineticData
            .keys()
            .map(|k| {
                self.LibKineticData
                    .get(k)
                    .and_then(|reaction| reaction.get("eq"))
                    .and_then(|eq| eq.as_str())
                    .map(|eq| eq.to_string())
                    .ok_or_else(|| KineticsError::MissingReaction(k.clone()))
            })
            .collect::<KineticsResult<Vec<String>>>()?;
        self.AllEquations = all_equations.clone();
        let EquationReactionIDMap: HashMap<String, String> = all_equations
            .iter()
            .zip(all_reactions.iter())
            .map(|(eq, r)| (eq.to_string(), r.to_string()))
            .collect();
        self.EquationReactionMap = EquationReactionIDMap;
        Ok(())
    } //end print_all_reactions
    /// returns reaction ID and reaction data (parsed from json) for given reaction equation
    pub fn search_reaction_by_equation(
        &mut self,
        equation: &str,
    ) -> KineticsResult<(String, Value)> {
        let reaction_id = self
            .EquationReactionMap
            .get(equation)
            .ok_or_else(|| KineticsError::MissingReaction(equation.to_string()))?;
        let reaction = self
            .LibKineticData
            .get(reaction_id)
            .ok_or_else(|| KineticsError::MissingReaction(reaction_id.clone()))?;
        return Ok((reaction_id.clone(), reaction.clone()));
    }

    // /returns reaction ID and reaction data (parsed from json) for given reagents/products
    fn search_reaction_by_substances(
        &mut self,
        substances: Vec<String>,
        SubstanceType: &str,
    ) -> KineticsResult<Vec<String>> {
        let substances: HashSet<String> = substances.iter().cloned().collect();
        let mut found_reactions: Vec<String> = Vec::new();
        for (r_id, reaction_parts) in &self.HashMapOfReactantsAndProducts {
            let Some(base_substances) = reaction_parts.get(SubstanceType) else {
                return Err(KineticsError::MissingReaction(r_id.to_string()));
            };
            if base_substances
                .iter()
                .filter(|subs| !matches!(subs.as_str(), "M" | "M)" | "(M" | "(M)"))
                .all(|subs| substances.contains(subs))
            {
                found_reactions.push(r_id.to_string());
            }
        }
        Ok(found_reactions)
    }
    /// search reactions where these substances are reagents/products
    pub fn search_reaction_by_reagents_and_products(
        &mut self,
        reagents: Vec<String>,
    ) -> KineticsResult<()> {
        let found_reactions_by_reagents =
            self.search_reaction_by_substances(reagents.clone(), "reagents")?;
        self.FoundReactionsByReagents = found_reactions_by_reagents;
        let found_reactions_by_products =
            self.search_reaction_by_substances(reagents, "products")?;
        self.FoundReactionsByProducts = found_reactions_by_products;
        Ok(())
    }
    /// get concrete reaction data
    pub fn search_reactdata_by_reaction_id(&mut self, reaction_id: &str) -> KineticsResult<Value> {
        let reaction = self
            .LibKineticData
            .get(reaction_id)
            .ok_or_else(|| KineticsError::MissingReaction(reaction_id.to_string()))?;
        return Ok(reaction.clone());
    }
    /// get concrete reaction data for vector of IDs
    pub fn search_reactdata_for_vector_of_IDs(
        &mut self,
        reaction_ids: Vec<String>,
    ) -> KineticsResult<Vec<Value>> {
        let mut vec_of_reactions: Vec<Value> = Vec::new();
        for r_id in reaction_ids {
            let reaction = self
                .LibKineticData
                .get(&r_id)
                .ok_or_else(|| KineticsError::MissingReaction(r_id.clone()))?;
            vec_of_reactions.push(reaction.clone());
        }
        self.FoundReactionDatasByIDs = vec_of_reactions.clone();
        Ok(vec_of_reactions)
    }
    //TODO! test
    /// get reaction data for a certain value of json field
    pub fn get_reactions_by_field(&self, field: &str, field_value: &str) -> Vec<Value> {
        self.LibKineticData
            .values()
            .filter(|reaction| {
                reaction
                    .get(field)
                    .and_then(|v| v.as_str())
                    .map_or(false, |v| v == field_value)
            })
            .cloned()
            .collect()
    }
    #[allow(dead_code)]
    /// form user chosen data
    pub fn form_user_chosen_data(
        &self,
        what_to_get: SearchVariants,
        mech_name: Option<String>,
    ) -> KineticsResult<HashMap<String, HashMap<String, Value>>> {
        let big_mech = if let Some(mech_name) = { mech_name } {
            mech_name
        } else {
            self.lib_name.clone()
        };
        let reaction_ids = self.get_search_results(what_to_get);
        if reaction_ids.len() == 0 {
            return Ok(HashMap::new());
        }
        let mut hash_map_id_data = HashMap::with_capacity(reaction_ids.len());
        for r_id in reaction_ids {
            let reaction = self
                .LibKineticData
                .get(&r_id)
                .ok_or_else(|| KineticsError::MissingReaction(r_id.clone()))?;
            hash_map_id_data.insert(r_id, reaction.clone());
        }
        let mut hash_map = HashMap::with_capacity(1);
        hash_map.insert(big_mech, hash_map_id_data);
        Ok(hash_map)
    }
    ///
    #[allow(dead_code)]
    pub fn get_search_results(&self, what_to_get: SearchVariants) -> Vec<String> {
        match what_to_get {
            SearchVariants::FoundReactionsByProducts => self.FoundReactionsByProducts.clone(),
            SearchVariants::FoundReactionsByReagents => self.FoundReactionsByReagents.clone(),
        }
    }
}

pub enum SearchVariants {
    FoundReactionsByProducts,
    FoundReactionsByReagents,
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// TESTS
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Arc;

    #[test]
    fn test_kinetic_data_new() {
        let mut kin_instance = KineticData::new();
        let lib = "NUIG";
        let LibVec: HashSet<String> = vec![
            "Beckstead".to_string(),
            "NUIG".to_string(),
            "Cantera".to_string(),
            "Beckstead_c".to_string(),
            "Aramco".to_string(),
        ]
        .into_iter()
        .collect();

        // collecting reaction data for library name lib
        kin_instance.open_json_files(lib);
        // veiew all reactions in library
        kin_instance.print_all_reactions();
        assert_eq!(kin_instance.HashMapOfReactantsAndProducts.is_empty(), false);
        assert_eq!(kin_instance.LibKineticData.is_empty(), false);
        let all_lib: HashSet<String> = kin_instance.AllLibraries.clone().into_iter().collect();
        assert_eq!(all_lib, LibVec);
        assert_eq!(kin_instance.AllEquations.is_empty(), false);

        // search reaction by equation
        let equation = kin_instance
            .LibKineticData
            .get("1")
            .unwrap()
            .get("eq")
            .unwrap()
            .as_str()
            .unwrap()
            .to_string();
        // returns reaction ID and reaction data (parsed from json) for given reaction equation
        let (reaction_id, reaction) = kin_instance.search_reaction_by_equation(&equation).unwrap();
        assert_eq!(reaction_id, "1".to_string());
        assert_eq!(reaction.get("eq").unwrap().as_str().unwrap(), equation);
        let substances: Vec<String> = vec!["CO".to_string(), "O".to_string()];
        // search reactions by substances
        kin_instance
            .search_reaction_by_reagents_and_products(substances)
            .unwrap();
        assert_eq!(kin_instance.FoundReactionsByProducts.is_empty(), false);
        assert_eq!(kin_instance.FoundReactionsByReagents.is_empty(), false);
        kin_instance
            .search_reactdata_for_vector_of_IDs(vec!["1".to_string(), "2".to_string()])
            .unwrap();
        assert_eq!(kin_instance.FoundReactionDatasByIDs.is_empty(), false);
    }

    #[test]
    fn test_open_json_files_rejects_unknown_library() {
        let mut kin_instance = KineticData::new();
        let err = kin_instance.open_json_files("NotARealLibrary").unwrap_err();

        assert!(matches!(
            err,
            KineticsError::MissingLibrary(library) if library == "NotARealLibrary"
        ));
    }

    #[test]
    fn test_search_reactdata_by_reaction_id_rejects_missing_reaction() {
        let mut kin_instance = KineticData::new();
        kin_instance.open_json_files("NUIG").unwrap();

        let err = kin_instance
            .search_reactdata_by_reaction_id("definitely_missing_id")
            .unwrap_err();

        assert!(matches!(
            err,
            KineticsError::MissingReaction(reaction_id)
                if reaction_id == "definitely_missing_id"
        ));
    }

    #[test]
    fn test_search_reaction_by_equation_rejects_missing_equation() {
        let mut kin_instance = KineticData::new();
        kin_instance.open_json_files("NUIG").unwrap();
        kin_instance.print_all_reactions().unwrap();

        let err = kin_instance
            .search_reaction_by_equation("A + B => C")
            .unwrap_err();

        assert!(matches!(
            err,
            KineticsError::MissingReaction(equation) if equation == "A + B => C"
        ));
    }

    #[test]
    fn test_search_reactdata_for_vector_of_ids_rejects_missing_reaction() {
        let mut kin_instance = KineticData::new();
        kin_instance.open_json_files("NUIG").unwrap();

        let err = kin_instance
            .search_reactdata_for_vector_of_IDs(vec!["definitely_missing_id".to_string()])
            .unwrap_err();

        assert!(matches!(
            err,
            KineticsError::MissingReaction(reaction_id)
                if reaction_id == "definitely_missing_id"
        ));
    }

    #[test]
    fn test_search_value() {
        let mut kin_instance = KineticData::new();
        let lib = "NUIG";

        // collecting reaction data for library name lib
        kin_instance.open_json_files(lib);
        // veiew all reactions in library
        kin_instance.print_all_reactions();

        // search reaction by equation
        let data_of_reacion = kin_instance.LibKineticData.get("1").unwrap();
        println!(" data_of_reacion {:?}", data_of_reacion);
    }

    #[test]
    fn test_search_data_by_field() {
        let mut kin_instance = KineticData::new();
        kin_instance.open_json_files("NUIG");

        let reactions_by_field = kin_instance.get_reactions_by_field("type", "falloff");
        println!(" reactions_by_field {:?}", reactions_by_field);
        assert_eq!(reactions_by_field.len(), 109);
    }

    #[test]
    fn test_form_user_chosen_data() {
        let mut kin_instance = KineticData::new();
        kin_instance.open_json_files("NUIG");

        // Prepare some test data
        kin_instance.FoundReactionsByProducts = vec!["1".to_string(), "2".to_string()];
        kin_instance.FoundReactionsByReagents = vec!["3".to_string(), "4".to_string()];

        // Test with FoundReactionsByProducts and default mechanism name
        let result = kin_instance
            .form_user_chosen_data(SearchVariants::FoundReactionsByProducts, None)
            .unwrap();
        assert_eq!(result.len(), 1);
        assert!(result.contains_key("NUIG"));
        let nuig_data = result.get("NUIG").unwrap();
        assert_eq!(nuig_data.len(), 2);
        assert!(nuig_data.contains_key("1"));
        assert!(nuig_data.contains_key("2"));

        // Test with FoundReactionsByReagents and specified mechanism name
        let result = kin_instance
            .form_user_chosen_data(
                SearchVariants::FoundReactionsByReagents,
                Some("CustomMech".to_string()),
            )
            .unwrap();
        assert_eq!(result.len(), 1);
        assert!(result.contains_key("CustomMech"));
        let custom_mech_data = result.get("CustomMech").unwrap();
        assert_eq!(custom_mech_data.len(), 2);
        assert!(custom_mech_data.contains_key("3"));
        assert!(custom_mech_data.contains_key("4"));

        // Test with empty results
        kin_instance.FoundReactionsByProducts = vec![];
        let result = kin_instance
            .form_user_chosen_data(SearchVariants::FoundReactionsByProducts, None)
            .unwrap();
        assert_eq!(result.len(), 0);
    }

    #[test]
    fn test_form_user_chosen_data_integration() {
        let mut kin_instance = KineticData::new();
        kin_instance.open_json_files("NUIG");

        // Perform a search to populate FoundReactionsByProducts
        kin_instance
            .search_reaction_by_reagents_and_products(vec!["O".to_string(), "H".to_string()])
            .unwrap();

        // Test with actual search results
        let result = kin_instance
            .form_user_chosen_data(SearchVariants::FoundReactionsByProducts, None)
            .unwrap();
        assert!(!result.is_empty());
        assert!(result.contains_key("NUIG"));
        let nuig_data = result.get("NUIG").unwrap();
        assert!(!nuig_data.is_empty());

        // Verify that the returned data matches the LibKineticData
        for (reaction_id, reaction_data) in nuig_data {
            assert_eq!(
                reaction_data,
                kin_instance.LibKineticData.get(reaction_id).unwrap()
            );
        }
    }

    #[test]
    fn test_cached_reactbase_reuses_same_allocation() {
        let first = cached_reactbase().unwrap();
        let second = cached_reactbase().unwrap();

        assert!(Arc::ptr_eq(&first, &second));
    }

    #[test]
    fn test_cached_reaction_db_reuses_same_allocation() {
        let first = cached_reaction_db().unwrap();
        let second = cached_reaction_db().unwrap();

        assert!(Arc::ptr_eq(&first, &second));
    }
    /*
    #[test]
    fn test_kinetic_data_open_json_files() {
        let mut kinetic_data = KineticData::new();

        // Replace "big_mech" with the actual file name
        kinetic_data.open_json_files("big_mech");

        // Add assertions to verify the content of kinetic_data.HashMapOfReactantsAndProducts,
        // kinetic_data.LibKineticData, kinetic_data.AllLibraries, etc.
    }



    */
    // Add more tests for other methods as needed
}
