//! CD1 — Cascade overflow recovery: breadcrumb capture as the pipeline
//! descends, surfaced on the recovery suspension when `MAX_CASCADE_DEPTH`
//! or `MAX_MODIFY_RESTARTS` is hit.
//!
//! See `docs/superpowers/specs/2026-05-09-cd1-cascade-overflow-recovery.md`.

use crate::events::EventKind;

/// One step in a cascade. `depth = 0` is the root propose.
///
/// `emitted_by` and `fanout` describe what THIS depth emitted to the
/// next level — they're filled as the cascade unwinds (a child's resolve
/// returns to the parent, the parent stamps the child's caller name and
/// side-effect count). The deepest crumb (the one that tripped the
/// limit) leaves both fields blank.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CascadeBreadcrumb {
    pub depth: u32,
    pub event_kind: EventKind,
    /// Handler name (the `#[handler(name = ...)]` attribute) that
    /// emitted the side-effect events at depth+1, if any. `None` for
    /// the deepest crumb (limit-tripping cascade level).
    ///
    /// Held as `String` rather than `&'static str` so the type can
    /// derive `Deserialize` without lifetime contortion. Allocations
    /// only happen at suspension-emit time (rare path), not in the
    /// hot pipeline.
    pub emitted_by: Option<String>,
    /// Side-effect fan-out at this level — how many events the named
    /// handler produced. 0 if `emitted_by` is `None`.
    pub fanout: u16,
}

/// Ordered chain of cascade breadcrumbs. Bounded by
/// `MAX_CASCADE_DEPTH + 1` ≈ 17 entries, so the worst-case wire size
/// is ~1 KiB.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CascadeTrace {
    pub crumbs: Vec<CascadeBreadcrumb>,
}

impl CascadeTrace {
    pub fn new() -> Self { Self::default() }

    /// EventKind of the root propose (the entry-point event). None for
    /// an empty trace.
    pub fn root_event_kind(&self) -> Option<EventKind> {
        self.crumbs.first().map(|c| c.event_kind)
    }

    /// Heuristic: same handler appears in three or more consecutive
    /// crumbs as `emitted_by`. Used to label the suspension prompt
    /// "possible reactor recursion" — doesn't change recovery options,
    /// only the human-readable copy.
    pub fn looks_recursive(&self) -> bool {
        let mut run: u32 = 0;
        let mut last: Option<&str> = None;
        for c in &self.crumbs {
            match (c.emitted_by.as_deref(), last) {
                (Some(name), Some(prev)) if name == prev => {
                    run += 1;
                    if run >= 3 { return true; }
                }
                (Some(name), _) => {
                    run = 1;
                    last = Some(name);
                }
                (None, _) => {
                    run = 0;
                    last = None;
                }
            }
        }
        false
    }

    /// Append a breadcrumb at `depth` for an event currently entering
    /// the resolver. `emitted_by` and `fanout` start blank.
    pub fn push(&mut self, depth: u32, event_kind: EventKind) {
        self.crumbs.push(CascadeBreadcrumb {
            depth,
            event_kind,
            emitted_by: None,
            fanout: 0,
        });
    }

    /// Stamp the parent crumb (the one before the most recently pushed)
    /// with `(emitted_by, fanout)` once the child cascade resolves.
    /// No-op if the trace has < 2 crumbs (no parent to stamp).
    #[allow(
        clippy::indexing_slicing,
        reason = "`checked_sub(2)` on the matched arm guarantees \
                  parent_idx < crumbs.len(); the indexed access is \
                  bounds-safe by construction"
    )]
    pub fn record_emit(&mut self, emitted_by: impl Into<String>, fanout: u16) {
        if let Some(parent_idx) = self.crumbs.len().checked_sub(2) {
            let parent = &mut self.crumbs[parent_idx];
            // Only set if not already set — first emitter wins so the
            // breadcrumb names the *triggering* handler, not the last
            // one to fire under it.
            if parent.emitted_by.is_none() {
                parent.emitted_by = Some(emitted_by.into());
                parent.fanout = fanout;
            }
        }
    }

    /// Pop the most recent breadcrumb (called as resolve returns).
    pub fn pop(&mut self) -> Option<CascadeBreadcrumb> {
        self.crumbs.pop()
    }

    pub fn len(&self) -> usize { self.crumbs.len() }
    pub fn is_empty(&self) -> bool { self.crumbs.is_empty() }
}

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

    fn ek() -> EventKind { EventKind::Moved }

    #[test]
    fn new_trace_is_empty() {
        let t = CascadeTrace::new();
        assert!(t.is_empty());
        assert_eq!(t.len(), 0);
        assert_eq!(t.root_event_kind(), None);
        assert!(!t.looks_recursive());
    }

    #[test]
    fn push_pop_roundtrips() {
        let mut t = CascadeTrace::new();
        t.push(0, ek());
        t.push(1, EventKind::Activated);
        assert_eq!(t.len(), 2);
        assert_eq!(t.root_event_kind(), Some(EventKind::Moved));
        let popped = t.pop().unwrap();
        assert_eq!(popped.depth, 1);
        assert_eq!(t.len(), 1);
    }

    #[test]
    fn record_emit_stamps_parent() {
        let mut t = CascadeTrace::new();
        t.push(0, ek());
        t.push(1, ek());
        t.record_emit("plugin:handler", 3);
        assert_eq!(t.crumbs[0].emitted_by.as_deref(), Some("plugin:handler"));
        assert_eq!(t.crumbs[0].fanout, 3);
        assert_eq!(t.crumbs[1].emitted_by, None);
    }

    #[test]
    fn record_emit_no_op_with_one_crumb() {
        let mut t = CascadeTrace::new();
        t.push(0, ek());
        t.record_emit("plugin:handler", 3);
        assert_eq!(t.crumbs[0].emitted_by, None);
    }

    #[test]
    fn record_emit_first_writer_wins() {
        let mut t = CascadeTrace::new();
        t.push(0, ek());
        t.push(1, ek());
        t.record_emit("plugin:first", 2);
        t.record_emit("plugin:second", 5);
        assert_eq!(t.crumbs[0].emitted_by.as_deref(), Some("plugin:first"));
        assert_eq!(t.crumbs[0].fanout, 2);
    }

    #[test]
    fn looks_recursive_false_for_empty() {
        let t = CascadeTrace::new();
        assert!(!t.looks_recursive());
    }

    #[test]
    fn looks_recursive_false_for_two_consecutive() {
        let mut t = CascadeTrace::new();
        // 4 crumbs, only 2 share a handler name — run never reaches 3
        t.push(0, ek()); t.crumbs[0].emitted_by = Some("a".into());
        t.push(1, ek()); t.crumbs[1].emitted_by = Some("a".into());
        t.push(2, ek()); t.crumbs[2].emitted_by = Some("b".into());
        t.push(3, ek());
        assert!(!t.looks_recursive());
    }

    #[test]
    fn looks_recursive_true_for_three_consecutive() {
        let mut t = CascadeTrace::new();
        t.push(0, ek()); t.crumbs[0].emitted_by = Some("a".into());
        t.push(1, ek()); t.crumbs[1].emitted_by = Some("a".into());
        t.push(2, ek()); t.crumbs[2].emitted_by = Some("a".into());
        t.push(3, ek());
        assert!(t.looks_recursive());
    }

    #[test]
    fn looks_recursive_handles_diverse_chain() {
        let mut t = CascadeTrace::new();
        t.push(0, ek()); t.crumbs[0].emitted_by = Some("a".into());
        t.push(1, ek()); t.crumbs[1].emitted_by = Some("b".into());
        t.push(2, ek()); t.crumbs[2].emitted_by = Some("c".into());
        t.push(3, ek()); t.crumbs[3].emitted_by = Some("d".into());
        t.push(4, ek());
        assert!(!t.looks_recursive());
    }

    #[test]
    fn looks_recursive_resets_on_break() {
        let mut t = CascadeTrace::new();
        t.push(0, ek()); t.crumbs[0].emitted_by = Some("a".into());
        t.push(1, ek()); t.crumbs[1].emitted_by = Some("a".into());
        t.push(2, ek()); t.crumbs[2].emitted_by = Some("b".into());
        t.push(3, ek()); t.crumbs[3].emitted_by = Some("a".into());
        t.push(4, ek()); t.crumbs[4].emitted_by = Some("a".into());
        t.push(5, ek());
        // 2 a's, 1 b, 2 a's, 1 None → no run of 3 same handlers
        assert!(!t.looks_recursive());
    }
}
