//! Background listener task: receives [`WatcherEvent`]s and dispatches
//! stage-transition and alert notifications to all configured chat rooms.

use crate::chat::ChatTransport;
use crate::config::ProjectConfig;
use crate::io::watcher::WatcherEvent;
use crate::slog;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::broadcast;

use super::super::events::classify;
use super::super::filter::{
    AGENT_EVENT_DEBOUNCE, NEW_ITEM_COALESCE_WINDOW, should_send_rate_limit,
};
use super::super::format::{
    MERGE_FAILURE_TAIL_LINES, NewItemInfo, format_agent_completed_notification,
    format_agent_started_notification, format_blocked_notification,
    format_disk_recovery_notification, format_disk_warning_notification, format_error_notification,
    format_merge_auto_retry_notification, format_new_items_notification,
    format_oauth_account_swapped, format_oauth_accounts_exhausted, format_rate_limit_notification,
    project_display_name, truncate_gate_output,
};
use super::super::route::rooms_for_notification;
use super::{find_story_name_any_stage, read_story_name};

/// Format and send any pending new-item-creation notifications as a single
/// combined message, then clear the pending buffer.
///
/// A no-op if `pending` is empty. The message is prefixed with
/// `[{project_name}] ` (story 1201, AC2).
async fn send_new_item_notifications(
    pending: &mut Vec<NewItemInfo>,
    project_name: &str,
    transport: &Arc<dyn ChatTransport>,
    get_room_ids: &impl Fn() -> Vec<String>,
) {
    if pending.is_empty() {
        return;
    }
    let items = std::mem::take(pending);
    let (plain, html) = format_new_items_notification(&items);
    let plain = format!("[{project_name}] {plain}");
    let html = format!("[{project_name}] {html}");
    slog!("[bot] Sending new-item 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] Failed to send new-item notification to {room_id}: {e}");
        }
    }
}

/// Spawn a background task that listens for watcher events and posts
/// stage-transition notifications to all configured rooms via the
/// [`ChatTransport`] abstraction.
///
/// `get_room_ids` is called on each notification to obtain the current list of
/// destination room IDs.  Pass a closure that returns a static list for Matrix
/// and Slack, or one that reads from a runtime `Arc<Mutex<HashSet<String>>>`
/// for WhatsApp ambient senders.
pub fn spawn_notification_listener(
    transport: Arc<dyn ChatTransport>,
    get_room_ids: impl Fn() -> Vec<String> + Send + Sync + 'static,
    watcher_rx: broadcast::Receiver<WatcherEvent>,
    project_root: PathBuf,
) {
    tokio::spawn(async move {
        let mut rx = watcher_rx;
        // Load initial config; re-loaded on ConfigChanged events.
        let mut config = ProjectConfig::load(&project_root).unwrap_or_default();
        let mut project_name =
            project_display_name(config.gateway_project.as_deref(), &project_root);
        // Tracks when a rate-limit notification was last sent for each
        // "story_id:agent_name" key, to debounce repeated warnings.
        let mut rate_limit_last_notified: HashMap<String, Instant> = HashMap::new();

        // Pending agent-status notifications, keyed by "{story_id}:{event_kind}".
        // Value: (plain, html).  Rapid successive events for the same story and
        // event kind are coalesced: only the latest is sent after the debounce
        // window expires (AGENT_EVENT_DEBOUNCE = 5 s).
        let mut pending_agent_events: HashMap<String, (String, String)> = HashMap::new();
        let mut agent_flush_deadline: Option<tokio::time::Instant> = None;

        // Pending new-item-creation notifications. Unlike agent events, every
        // item created within the window is kept (not latest-wins) so a burst
        // of creations produces one combined message listing all of them
        // (story 1201, AC3).
        let mut pending_new_items: Vec<NewItemInfo> = Vec::new();
        let mut new_item_flush_deadline: Option<tokio::time::Instant> = None;

        loop {
            // Wait for the next event, or flush pending notifications when the
            // earliest debounce window expires.
            let next_deadline = [agent_flush_deadline, new_item_flush_deadline]
                .into_iter()
                .flatten()
                .min();
            let recv_result = if let Some(deadline) = next_deadline {
                tokio::time::timeout_at(deadline, rx.recv()).await.ok()
            } else {
                Some(rx.recv().await)
            };

            if recv_result.is_none() {
                let now = tokio::time::Instant::now();
                // Flush agent events if their deadline has passed.
                if agent_flush_deadline.is_some_and(|d| d <= now) {
                    for (_key, (plain, html)) in pending_agent_events.drain() {
                        slog!("[bot] Sending agent notification: {plain}");
                        if config.status_push_enabled {
                            for room_id in &rooms_for_notification(&get_room_ids) {
                                if let Err(e) = transport.send_message(room_id, &plain, &html).await
                                {
                                    slog!(
                                        "[bot] Failed to send agent notification to {room_id}: {e}"
                                    );
                                }
                            }
                        }
                    }
                    agent_flush_deadline = None;
                }
                // Flush new-item events if their deadline has passed.
                if new_item_flush_deadline.is_some_and(|d| d <= now) {
                    send_new_item_notifications(
                        &mut pending_new_items,
                        &project_name,
                        &transport,
                        &get_room_ids,
                    )
                    .await;
                    new_item_flush_deadline = None;
                }
                continue;
            }

            let event = match recv_result.unwrap() {
                Ok(ev) => ev,
                Err(broadcast::error::RecvError::Lagged(n)) => {
                    slog!("[bot] Notification listener lagged, skipped {n} events");
                    continue;
                }
                Err(broadcast::error::RecvError::Closed) => {
                    slog!("[bot] Watcher channel closed, stopping notification listener");
                    if config.status_push_enabled {
                        for (_key, (plain, html)) in pending_agent_events.drain() {
                            slog!("[bot] Sending agent 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] Failed to send agent notification to {room_id}: {e}"
                                    );
                                }
                            }
                        }
                    }
                    send_new_item_notifications(
                        &mut pending_new_items,
                        &project_name,
                        &transport,
                        &get_room_ids,
                    )
                    .await;
                    break;
                }
            };

            use super::super::events::EventAction;
            match classify(&event) {
                EventAction::MergeFailure => {
                    if !config.status_push_enabled {
                        continue;
                    }
                    let WatcherEvent::MergeFailure {
                        ref story_id,
                        ref reason,
                    } = event
                    else {
                        continue;
                    };
                    let story_name = read_story_name(&project_root, "4_merge", story_id);
                    let snippet = truncate_gate_output(reason, MERGE_FAILURE_TAIL_LINES);
                    let (plain, html) = format_error_notification(story_id, &story_name, &snippet);
                    slog!("[bot] Sending error 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] Failed to send error notification to {room_id}: {e}");
                        }
                    }
                }
                EventAction::RateLimitWarning => {
                    if !config.status_push_enabled {
                        continue;
                    }
                    let WatcherEvent::RateLimitWarning {
                        ref story_id,
                        ref agent_name,
                    } = event
                    else {
                        continue;
                    };
                    if !config.rate_limit_notifications {
                        slog!(
                            "[bot] RateLimitWarning suppressed by config for \
                             {story_id}:{agent_name}"
                        );
                        continue;
                    }
                    let debounce_key = format!("{story_id}:{agent_name}");
                    let now = Instant::now();
                    if !should_send_rate_limit(
                        rate_limit_last_notified.get(&debounce_key).copied(),
                        now,
                    ) {
                        slog!(
                            "[bot] Rate-limit notification debounced for \
                             {story_id}:{agent_name}"
                        );
                        continue;
                    }
                    rate_limit_last_notified.insert(debounce_key, now);
                    let story_name = find_story_name_any_stage(&project_root, story_id);
                    let (plain, html) =
                        format_rate_limit_notification(story_id, &story_name, agent_name);
                    slog!("[bot] Sending rate-limit 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] Failed to send rate-limit notification \
                                 to {room_id}: {e}"
                            );
                        }
                    }
                }
                EventAction::StoryBlocked => {
                    if !config.status_push_enabled {
                        continue;
                    }
                    let WatcherEvent::StoryBlocked {
                        ref story_id,
                        ref reason,
                    } = event
                    else {
                        continue;
                    };
                    let story_name = find_story_name_any_stage(&project_root, story_id);
                    let (plain, html) = format_blocked_notification(story_id, &story_name, reason);
                    slog!("[bot] Sending blocked 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] Failed to send blocked notification to {room_id}: {e}");
                        }
                    }
                }
                EventAction::OAuthAccountSwapped => {
                    if !config.status_push_enabled {
                        continue;
                    }
                    let WatcherEvent::OAuthAccountSwapped { ref new_email } = event else {
                        continue;
                    };
                    let (plain, html) = format_oauth_account_swapped(new_email);
                    slog!("[bot] Sending OAuth account-swap 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] Failed to send OAuth account-swap notification \
                                 to {room_id}: {e}"
                            );
                        }
                    }
                }
                EventAction::OAuthAccountsExhausted => {
                    if !config.status_push_enabled {
                        continue;
                    }
                    let WatcherEvent::OAuthAccountsExhausted {
                        ref earliest_reset_msg,
                    } = event
                    else {
                        continue;
                    };
                    let (plain, html) = format_oauth_accounts_exhausted(earliest_reset_msg);
                    slog!("[bot] Sending OAuth accounts-exhausted 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] Failed to send OAuth accounts-exhausted notification \
                                 to {room_id}: {e}"
                            );
                        }
                    }
                }
                EventAction::AgentStarted => {
                    if !config.status_push_enabled {
                        continue;
                    }
                    let WatcherEvent::AgentStarted {
                        ref story_id,
                        ref agent_name,
                    } = event
                    else {
                        continue;
                    };
                    let story_name = find_story_name_any_stage(&project_root, story_id);
                    let (plain, html) =
                        format_agent_started_notification(story_id, &story_name, agent_name);
                    // Buffer with 5s debounce; later arrivals overwrite earlier ones.
                    let key = format!("{story_id}:started");
                    pending_agent_events.insert(key, (plain, html));
                    agent_flush_deadline = Some(tokio::time::Instant::now() + AGENT_EVENT_DEBOUNCE);
                }
                EventAction::AgentCompleted { success } => {
                    if !config.status_push_enabled {
                        continue;
                    }
                    let WatcherEvent::AgentCompleted {
                        ref story_id,
                        ref agent_name,
                        ..
                    } = event
                    else {
                        continue;
                    };
                    let story_name = find_story_name_any_stage(&project_root, story_id);
                    let (plain, html) = format_agent_completed_notification(
                        story_id,
                        &story_name,
                        agent_name,
                        success,
                    );
                    // Buffer with 5s debounce; later arrivals overwrite earlier ones.
                    let key = format!("{story_id}:completed");
                    pending_agent_events.insert(key, (plain, html));
                    agent_flush_deadline = Some(tokio::time::Instant::now() + AGENT_EVENT_DEBOUNCE);
                }
                EventAction::NewItemCreated => {
                    if !config.status_push_enabled {
                        continue;
                    }
                    let WatcherEvent::NewItemCreated {
                        ref item_id,
                        ref item_type,
                        ref name,
                        ref origin,
                    } = event
                    else {
                        continue;
                    };
                    pending_new_items.push(NewItemInfo {
                        item_id: item_id.clone(),
                        item_type: item_type.clone(),
                        name: name.clone(),
                        origin_label: origin.clone(),
                    });
                    // Set the deadline once from the first arriving event so
                    // concurrent bursts don't keep pushing the window out and
                    // starving the flush.
                    if new_item_flush_deadline.is_none() {
                        new_item_flush_deadline =
                            Some(tokio::time::Instant::now() + NEW_ITEM_COALESCE_WINDOW);
                    }
                }
                EventAction::MergeAutoRetry => {
                    if !config.status_push_enabled {
                        continue;
                    }
                    let WatcherEvent::MergeAutoRetry {
                        ref story_id,
                        attempt,
                        budget,
                    } = event
                    else {
                        continue;
                    };
                    let story_name = find_story_name_any_stage(&project_root, story_id);
                    let (plain, html) = format_merge_auto_retry_notification(
                        story_id,
                        &story_name,
                        attempt,
                        budget,
                    );
                    slog!("[bot] Sending merge-auto-retry 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] Failed to send merge-auto-retry notification \
                                 to {room_id}: {e}"
                            );
                        }
                    }
                }
                EventAction::DiskWarning { .. } => {
                    if !config.status_push_enabled {
                        continue;
                    }
                    let WatcherEvent::DiskSpaceWarning {
                        ref level,
                        free_bytes,
                        target_bytes,
                        worktrees_bytes,
                        ref host_id,
                    } = event
                    else {
                        continue;
                    };
                    let (plain, html) = format_disk_warning_notification(
                        level,
                        host_id,
                        free_bytes,
                        target_bytes,
                        worktrees_bytes,
                    );
                    slog!("[bot] Sending disk-space warning 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] Failed to send disk-space warning notification \
                                 to {room_id}: {e}"
                            );
                        }
                    }
                }
                EventAction::DiskRecovery => {
                    if !config.status_push_enabled {
                        continue;
                    }
                    let WatcherEvent::DiskSpaceRecovered {
                        free_bytes,
                        ref host_id,
                    } = event
                    else {
                        continue;
                    };
                    let (plain, html) = format_disk_recovery_notification(host_id, free_bytes);
                    slog!("[bot] Sending disk-space recovery 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] Failed to send disk-space recovery notification \
                                 to {room_id}: {e}"
                            );
                        }
                    }
                }
                EventAction::LogOnly => {
                    // Hard-block: log server-side for debugging; do NOT post to chat.
                    // Hard-block auto-resume is normal operation — the status command
                    // already surfaces rate-limit state via emoji.
                    if let WatcherEvent::RateLimitHardBlock {
                        ref story_id,
                        ref agent_name,
                        reset_at,
                    } = event
                    {
                        slog!(
                            "[bot] Rate-limit hard block for {story_id}/{agent_name}, \
                             auto-resume at {reset_at}"
                        );
                    }
                }
                EventAction::ReloadConfig => {
                    if let Ok(new_cfg) = ProjectConfig::load(&project_root) {
                        config = new_cfg;
                        project_name =
                            project_display_name(config.gateway_project.as_deref(), &project_root);
                    }
                }
                EventAction::Skip => {}
            }
        }
    });
}
