//! Panic catcher — wraps a handler invocation so user-code panics
//! surface as `StepError::Failed` instead of crashing the host process.

/// Result of running a (sync-returns-future) closure with panic catching.
pub(crate) enum CaughtResult<T> {
    Ok(T),
    Panicked(String),
}

/// Run a closure that produces a future, awaiting it while catching any
/// panic that happens during the future's polling. Uses
/// `futures::FutureExt::catch_unwind` under the hood. Returns
/// `CaughtResult::Panicked(msg)` when the closure or the future panics;
/// the message is the panic payload formatted as a String.
pub(crate) async fn run_caught<F, Fut, T>(make_fut: F) -> CaughtResult<T>
where
    F: FnOnce() -> Fut,
    Fut: std::future::Future<Output = T>,
{
    use futures::FutureExt;
    // Catch panics from constructing the future, too.
    let fut = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(make_fut)) {
        Ok(f) => f,
        Err(panic) => return CaughtResult::Panicked(format_panic(panic)),
    };
    match std::panic::AssertUnwindSafe(fut).catch_unwind().await {
        Ok(value) => CaughtResult::Ok(value),
        Err(panic) => CaughtResult::Panicked(format_panic(panic)),
    }
}

fn format_panic(payload: Box<dyn std::any::Any + Send>) -> String {
    if let Some(s) = payload.downcast_ref::<&str>() {
        return (*s).to_string();
    }
    if let Some(s) = payload.downcast_ref::<String>() {
        return s.clone();
    }
    "<non-string panic payload>".to_string()
}
