//! Stage-transition notification subscriber.
//!
//! Subscribes to the [`TransitionFired`] broadcast channel and dispatches
//! chat notifications for each significant pipeline stage change.
//! This is the **single** emitter of stage-change notifications; all other
//! call sites have been removed (story 995).

use crate::chat::ChatTransport;
use crate::config::ProjectConfig;
use crate::pipeline_state::Stage;
use crate::service::notifications::filter::STAGE_TRANSITION_DEBOUNCE;
use crate::service::notifications::format::format_stage_notification;
use crate::service::notifications::route::rooms_for_notification;
use crate::slog;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::broadcast;

/// Spawn a background task that subscribes to typed [`TransitionFired`] events
/// and posts stage-change notifications to all configured chat rooms.
///
/// Coalesces rapid successive transitions for the same item into a single
/// notification (200 ms debounce window) so only the final stage is announced.
/// Config is read once at startup; `status_push_enabled = false` suppresses all
/// notifications from this subscriber.
pub fn spawn_stage_notification_subscriber(
    transport: Arc<dyn ChatTransport>,
    get_room_ids: impl Fn() -> Vec<String> + Send + Sync + 'static,
    project_root: PathBuf,
) {
    let mut rx = crate::pipeline_state::subscribe_transitions();
    tokio::spawn(async move {
        let config = ProjectConfig::load(&project_root).unwrap_or_default();

        // Pending notifications keyed by story_id: (from_stage, to_stage, story_name).
        // Rapid transitions are coalesced: original from preserved, to updated.
        let mut pending: HashMap<String, (Stage, Stage, String)> = HashMap::new();
        let mut flush_deadline: Option<tokio::time::Instant> = None;

        loop {
            let recv_result = if let Some(deadline) = flush_deadline {
                tokio::time::timeout_at(deadline, rx.recv()).await.ok()
            } else {
                Some(rx.recv().await)
            };

            // Timeout → flush coalesced notifications.
            if recv_result.is_none() {
                if flush_deadline.is_some_and(|d| d <= tokio::time::Instant::now()) {
                    send_pending(&pending, &transport, &get_room_ids).await;
                    pending.clear();
                    flush_deadline = None;
                }
                continue;
            }

            let fired = match recv_result.unwrap() {
                Ok(f) => f,
                Err(broadcast::error::RecvError::Lagged(n)) => {
                    slog!("[bot/transition] Subscriber lagged; skipped {n} event(s)");
                    continue;
                }
                Err(broadcast::error::RecvError::Closed) => {
                    send_pending(&pending, &transport, &get_room_ids).await;
                    break;
                }
            };

            if !config.status_push_enabled {
                continue;
            }

            // Upcoming is the only stage we suppress; every other stage
            // arrival produces a notification.
            if matches!(&fired.after, Stage::Upcoming) {
                continue;
            }

            let story_name =
                super::read_story_name(&project_root, fired.after.dir_name(), &fired.story_id.0);

            // Coalesce: keep original from_stage, update to_stage and name.
            pending
                .entry(fired.story_id.0.clone())
                .and_modify(|e| {
                    e.1 = fired.after.clone();
                    if !story_name.is_empty() {
                        e.2 = story_name.clone();
                    }
                })
                .or_insert_with(|| (fired.before.clone(), fired.after.clone(), story_name));

            // Set the deadline once from the first arriving event so that
            // concurrent test broadcasts on the global channel do not keep
            // pushing the window out and starving the flush.
            if flush_deadline.is_none() {
                flush_deadline = Some(tokio::time::Instant::now() + STAGE_TRANSITION_DEBOUNCE);
            }
        }
    });
}

/// Send all pending stage-transition notifications and log each one.
async fn send_pending(
    pending: &HashMap<String, (Stage, Stage, String)>,
    transport: &Arc<dyn ChatTransport>,
    get_room_ids: &(impl Fn() -> Vec<String> + Sync),
) {
    for (item_id, (from_stage, to_stage, story_name)) in pending {
        let (plain, html) = format_stage_notification(item_id, story_name, from_stage, to_stage);
        slog!("[bot/transition] Sending stage notification: {plain}");
        for room_id in &rooms_for_notification(get_room_ids) {
            if let Err(e) = transport.send_message(room_id, &plain, &html).await {
                slog!("[bot/transition] Failed to send notification to {room_id}: {e}");
            }
        }
    }
}
