/*
 * SPDX-License-Identifier: MIT
 * Copyright (c) 2023 - 2026. The DeepCausality Authors and Contributors. All Rights Reserved.
 */

use crate::{AssumptionError, DescriptionValue, Identifiable, NumericalValue, PropagatingEffect};

/// The Assumable trait defines the interface for objects that represent
/// assumptions that can be tested and verified. Assumable types must also
/// implement Identifiable.
///
/// # Trait Methods
///
/// * `description` - Returns a description of the assumption as a
///   DescriptionValue
/// * `assumption_fn` - Returns the function that will evaluate the assumption
///   as an EvalFn
/// * `assumption_tested` - Returns whether this assumption has been tested
/// * `assumption_valid` - Returns whether this assumption is valid
/// * `verify_assumption` - Tests the assumption against the provided data and
///   returns whether it is valid
///
/// The AssumableReasoning trait provides default implementations for common
/// operations over collections of Assumable types.
///
pub trait Assumable: Identifiable {
    fn description(&self) -> DescriptionValue;
    fn assumption_tested(&self) -> bool;
    fn assumption_valid(&self) -> bool;
    fn verify_assumption(&self, data: &[PropagatingEffect<f64>]) -> Result<bool, AssumptionError>;
}

/// The AssumableReasoning trait provides default implementations for common
/// operations over collections of Assumable types.
///
/// It requires the associated type T to implement Assumable.
///
/// # Trait Methods
///
/// * `len` - Returns the number of items in the collection.
/// * `is_empty` - Returns true if the collection is empty.
/// * `get_all_items` - Returns a vector containing references to all items.
///
/// It also provides default implementations for:
///
/// * `all_assumptions_tested` - Checks if all assumptions have been tested.
/// * `all_assumptions_valid` - Checks if all assumptions are valid.
/// * `number_assumption_valid` - Returns the number of valid assumptions.
/// * `percent_assumption_valid` - Returns the percentage of valid assumptions.
/// * `verify_all_assumptions` - Verifies all assumptions against provided data.
/// * `get_all_invalid_assumptions` - Filters for invalid assumptions.
/// * `get_all_valid_assumptions` - Filters for valid assumptions.
/// * `get_all_tested_assumptions` - Filters for tested assumptions.
/// * `get_all_untested_assumptions` - Filters for untested assumptions.
///
pub trait AssumableReasoning<T>
where
    T: Assumable,
{
    // Method can be generated by the compiler using macros.
    fn len(&self) -> usize;
    fn is_empty(&self) -> bool;
    fn get_all_items(&self) -> Vec<&T>;

    //
    // Default implementations for all other trait methods.
    //

    /// Checks if all assumptions in the collection have been tested.
    ///
    /// Iterates through all items returned by `get_all_items()` and checks if
    /// `assumption_tested()` returns true for each one.
    ///
    /// Returns false if any assumption has not been tested, otherwise returns
    /// true.
    ///
    fn all_assumptions_tested(&self) -> bool {
        for elem in self.get_all_items() {
            if !elem.assumption_tested() {
                return false;
            }
        }
        true
    }

    /// Checks if all assumptions in the collection are valid.
    ///
    /// Iterates through all items returned by `get_all_items()` and checks if
    /// `assumption_valid()` returns true for each one.
    ///
    /// Returns false if any assumption is invalid, otherwise returns true.
    ///
    fn all_assumptions_valid(&self) -> bool {
        for a in self.get_all_items() {
            if !a.assumption_valid() {
                return false;
            }
        }
        true
    }

    /// Returns the number of valid assumptions in the collection.
    ///
    /// Gets all items via `get_all_items()`, filters to keep only those where
    /// `assumption_valid()` returns true, and returns the count as a
    /// NumericalValue.
    ///
    fn number_assumption_valid(&self) -> NumericalValue {
        self.get_all_items()
            .iter()
            .filter(|a| a.assumption_valid())
            .count() as NumericalValue
    }

    /// Returns the percentage of valid assumptions in the collection.
    ///
    /// Calculates the percentage by dividing the number of valid assumptions
    /// (from `number_assumption_valid()`) by the total number of assumptions
    /// (from `len()`) and multiplying by 100.
    ///
    /// # Errors
    ///
    /// Returns `AssumptionError::EvaluationFailed` if the number of assumptions is zero,
    /// as percentage calculation would lead to a division by zero.
    ///
    fn percent_assumption_valid(&self) -> Result<NumericalValue, AssumptionError> {
        if self.is_empty() {
            return Err(AssumptionError::EvaluationFailed(
                "Cannot calculate percentage with zero assumptions".to_string(),
            ));
        }
        let percentage = (self.number_assumption_valid() / self.len() as NumericalValue) * 100.0;
        Ok(percentage)
    }

    /// Verifies all assumptions in the collection against the provided data.
    ///
    /// Iterates through all items returned by `get_all_items()` and calls
    /// `verify_assumption()` on each one, passing the `data`.
    ///
    /// This will test each assumption against the data and update the
    /// `assumption_valid` and `assumption_tested` flags accordingly.
    ///
    /// # Errors
    ///
    /// Returns an `AssumptionError` if any of the assumption functions fail during execution.
    ///
    fn verify_all_assumptions(
        &self,
        data: &[PropagatingEffect<f64>],
    ) -> Result<(), AssumptionError> {
        for a in self.get_all_items() {
            // We are interested in the side effect of updating the assumption state,
            // but we must handle the potential error.
            let _ = a.verify_assumption(data)?;
        }
        Ok(())
    }

    /// Returns a vector containing references to all invalid assumptions.
    ///
    /// Gets all items via `get_all_items()`, filters to keep only those where
    /// `assumption_valid()` returns false, and collects into a vector.
    ///
    fn get_all_invalid_assumptions(&self) -> Vec<&T> {
        self.get_all_items()
            .into_iter()
            .filter(|a| !a.assumption_valid())
            .collect()
    }

    /// Returns a vector containing references to all valid assumptions.
    ///
    /// Gets all items via `get_all_items()`, filters to keep only those where
    /// `assumption_valid()` returns true, and collects into a vector.
    ///
    fn get_all_valid_assumptions(&self) -> Vec<&T> {
        self.get_all_items()
            .into_iter()
            .filter(|a| a.assumption_valid())
            .collect()
    }

    /// Returns a vector containing references to all tested assumptions.
    ///
    /// Gets all items via `get_all_items()`, filters to keep only those where
    /// `assumption_tested()` returns true, and collects into a vector.
    ///
    fn get_all_tested_assumptions(&self) -> Vec<&T> {
        self.get_all_items()
            .into_iter()
            .filter(|a| a.assumption_tested())
            .collect()
    }

    /// Returns a vector containing references to all untested assumptions.
    ///
    /// Gets all items via `get_all_items()`, filters to keep only those where
    /// `assumption_tested()` returns false, and collects into a vector.
    ///
    fn get_all_untested_assumptions(&self) -> Vec<&T> {
        self.get_all_items()
            .into_iter()
            .filter(|a| !a.assumption_tested())
            .collect()
    }
}
