//! Functionality for creating grouping enhancements and applying them to stacktraces.
//!
//! This module's core type is [`Enhancements`]. It contains a list of [`Rules`](Rule) which can
//! modify certain stack frames and change grouping metadata.
//!
//! `Enhancements` can be parsed from a human-readable string representation with [`parse`](Enhancements::parse)
//! or from a compact msgpack representation with [`from_config_structure`](Enhancements::from_config_structure).
//!
//! They are applied to stacktraces with [`apply_modifications_to_frames`](Enhancements::apply_modifications_to_frames).

use smol_str::SmolStr;

mod actions;
mod cache;
mod config_structure;
mod families;
mod frame;
mod grammar;
mod matchers;
mod rules;

pub use cache::*;
use config_structure::{EncodedAction, EncodedEnhancements, EncodedMatcher};
pub use families::Families;
pub use frame::{Frame, StringField};
pub use rules::Rule;

/// Exception data to match against rules.
#[derive(Debug, Clone, Default)]
pub struct ExceptionData {
    /// The exception's type, i.e. name.
    pub ty: Option<SmolStr>,
    /// The exception's value, i.e. human-readable description.
    pub value: Option<SmolStr>,
    /// The exception's mechanism.
    pub mechanism: Option<SmolStr>,
}

/// A collection of [Rules](Rule) that modify the stacktrace and update grouping information.
#[derive(Debug, Default)]
pub struct Enhancements {
    /// The list of all rules in this collection.
    pub(crate) all_rules: Vec<Rule>,
    /// The list of "modifier rules" in this collection.
    ///
    /// Modifier rules are those rules that may modify a stacktrace.
    modifier_rules: Vec<Rule>,
    /// The list of "updater rules" in this collection.
    ///
    /// Updater rules are those rules that may update grouping metadata.
    updater_rules: Vec<Rule>,
}

impl Enhancements {
    /// Creates a new `Enhancements` from a list of `Rules`.
    pub fn new(all_rules: Vec<Rule>) -> Self {
        let modifier_rules = all_rules
            .iter()
            .filter(|r| r.has_modifier_action())
            .cloned()
            .collect();

        let updater_rules = all_rules
            .iter()
            .filter(|r| r.has_updater_action())
            .cloned()
            .collect();

        Enhancements {
            all_rules,
            modifier_rules,
            updater_rules,
        }
    }

    /// Parses an `Enhancements` structure from a string (in the form of a list of rules).
    pub fn parse(input: &str, cache: &mut Cache) -> anyhow::Result<Self> {
        let mut all_rules = vec![];

        for line in input.lines() {
            let line = line.trim();
            if line.is_empty() || line.starts_with('#') {
                continue;
            }
            let rule = cache.get_or_try_insert_rule(line)?;
            all_rules.push(rule);
        }

        Ok(Enhancements::new(all_rules))
    }

    /// Parses an `Enhancements` structure from the msgpack representation.
    pub fn from_config_structure(input: &[u8], cache: &mut Cache) -> anyhow::Result<Self> {
        let EncodedEnhancements(version, _bases, rules) = rmp_serde::from_slice(input)?;

        anyhow::ensure!(
            version == 2,
            "Rust Enhancements only supports config_structure version `2`"
        );

        let all_rules: Vec<_> = rules
            .into_iter()
            .map(|r| {
                let matchers =
                    r.0.into_iter()
                        .map(|encoded| EncodedMatcher::into_matcher(encoded, cache))
                        .collect::<anyhow::Result<_>>()?;
                let actions =
                    r.1.into_iter()
                        .map(EncodedAction::into_action)
                        .collect::<anyhow::Result<_>>()?;

                Ok(Rule::new(matchers, actions))
            })
            .collect::<anyhow::Result<_>>()?;

        Ok(Enhancements::new(all_rules))
    }

    /// Matches `frames` and `exception_data` against all rules in this collection
    /// and applies the corresponding modifications if a frame matches a rule.
    pub fn apply_modifications_to_frames(
        &self,
        frames: &mut [Frame],
        exception_data: &ExceptionData,
    ) {
        let mut matching_frames = Vec::with_capacity(frames.len());
        for rule in &self.modifier_rules {
            if !rule.matches_exception(exception_data) {
                continue;
            }

            // first, for each frame check if the rule matches
            matching_frames
                .extend((0..frames.len()).filter(|idx| rule.matches_frame(frames, *idx)));

            // then in a second pass, apply the actions to all matching frames
            for idx in matching_frames.drain(..) {
                rule.apply_modifications_to_frame(frames, idx);
            }
        }
    }

    /// Updates contribution metadata in `components` based on the rules in this collection.
    pub fn update_frame_components_contributions(
        &self,
        components: &mut [Component],
        frames: &[Frame],
    ) -> StacktraceState {
        let mut stacktrace_state = StacktraceState::default();

        // First, update the `in_app` hints. We have kept track of which rule last set the
        // `in_app` field of each frame, so we don't need to iterate over the rules again for this.
        for (component, frame) in components.iter_mut().zip(frames.iter()) {
            if let Some(rule) = frame.in_app_last_changed.as_ref() {
                // in_app is definitely set, otherwise `in_app_last_changed` would be `None`
                let state = if frame.in_app.unwrap() {
                    "in-app"
                } else {
                    "out of app"
                };
                component.hint = Some(format!("marked {state} by stack trace rule ({rule})"));
            }
        }

        // Apply direct frame actions and update the stack state alongside
        for rule in &self.updater_rules {
            for idx in 0..frames.len() {
                if rule.matches_frame(frames, idx) {
                    rule.update_frame_components_contributions(components, idx);
                    rule.modify_stacktrace_state(&mut stacktrace_state);
                }
            }
        }

        // Use the stack state to update frame contributions again to trim
        // down to max-frames.  min-frames is handled on the other hand for
        // the entire stacktrace later.
        let max_frames = stacktrace_state.max_frames.value;

        if max_frames > 0 {
            let mut ignored = 0;

            for component in components.iter_mut().rev() {
                if !component.contributes {
                    continue;
                }

                ignored += 1;

                if ignored <= max_frames {
                    continue;
                }

                let hint = format!(
                    "ignored because only {} {} considered",
                    max_frames,
                    if max_frames != 1 {
                        "frames are"
                    } else {
                        "frame is"
                    },
                );

                let hint = stacktrace_state
                    .max_frames
                    .setter
                    .as_ref()
                    .map(|r| format!("{hint} by stack trace rule ({r})"))
                    .unwrap_or(hint);

                component.contributes = false;
                component.hint = Some(hint);
            }
        }

        stacktrace_state
    }

    /// Returns an iterator over all rules in this collection.
    pub fn rules(&self) -> impl Iterator<Item = &Rule> {
        self.all_rules.iter()
    }

    /// Adds all rules contained in `other` to `self`.
    pub fn extend_from(&mut self, other: &Enhancements) {
        self.extend(other.rules().cloned())
    }
}

impl Extend<Rule> for Enhancements {
    fn extend<T: IntoIterator<Item = Rule>>(&mut self, iter: T) {
        for rule in iter.into_iter() {
            if rule.has_modifier_action() {
                self.modifier_rules.push(rule.clone());
            }

            if rule.has_updater_action() {
                self.updater_rules.push(rule.clone());
            }

            self.all_rules.push(rule);
        }
    }
}

#[derive(Debug, Clone, Default)]
pub struct Component {
    pub contributes: bool,
    pub is_prefix_frame: bool,
    pub is_sentinel_frame: bool,
    pub hint: Option<String>,
}

#[derive(Debug, Clone, Default)]
pub struct StacktraceVariable<T> {
    pub value: T,
    pub setter: Option<Rule>,
}

#[derive(Debug, Clone, Default)]
pub struct StacktraceState {
    pub max_frames: StacktraceVariable<usize>,
    pub min_frames: StacktraceVariable<usize>,
    pub invert_stacktrace: StacktraceVariable<bool>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_default_enhancers() {
        let enhancers =
            std::fs::read_to_string("../tests/fixtures/newstyle@2023-01-11.txt").unwrap();
        let enhancements = Enhancements::parse(&enhancers, &mut Cache::default()).unwrap();
        dbg!(enhancements.all_rules.len());
        dbg!(enhancements.modifier_rules.len());
        dbg!(enhancements.updater_rules.len());
    }

    #[test]
    fn parses_encoded_default_enhancers() {
        let enhancers = std::fs::read("../tests/fixtures/newstyle@2023-01-11.bin").unwrap();
        let _enhancements =
            Enhancements::from_config_structure(&enhancers, &mut Cache::default()).unwrap();
    }
}
