//! ImportProgressSink: bounded mpsc with drop-oldest-non-terminal overflow.
//! Terminal events (ImportDone / ImportFailed) are never dropped — on full
//! channel they evict oldest non-terminal events until they fit. The import
//! thread never backpressures on a slow consumer.

use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;

use super::ImportDiscovery;
use super::ImportStats;

const DEFAULT_CAPACITY: usize = 64;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum ImportProgressEvent {
    DiscoveryStarted  { source: String },
    DiscoveryProgress { scanned: usize, estimated_total: Option<usize> },
    DiscoveryDone     { discovered: ImportDiscovery },
    SessionStarted    { native_id: String, index: usize, total: usize },
    SessionFinished   { native_id: String, outcome: SessionOutcome, stats_delta: StatsDelta },
    ImportDone        { stats: ImportStats },
    ImportFailed      { error: String },
}

impl ImportProgressEvent {
    pub fn is_terminal(&self) -> bool {
        matches!(self, ImportProgressEvent::ImportDone { .. } | ImportProgressEvent::ImportFailed { .. })
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SessionOutcome { Imported, Skipped, Updated, Failed }

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct StatsDelta {
    pub messages_written:         u64,
    pub messages_already_present: u64,
}

pub struct ImportProgressSink {
    inner: SinkInner,
}

enum SinkInner {
    Live { tx: mpsc::Sender<ImportProgressEvent> },
    Noop,
}

impl ImportProgressSink {
    pub fn channel() -> (Self, mpsc::Receiver<ImportProgressEvent>) {
        let (tx, rx) = mpsc::channel(DEFAULT_CAPACITY);
        (Self { inner: SinkInner::Live { tx } }, rx)
    }

    pub fn noop() -> Self { Self { inner: SinkInner::Noop } }

    pub async fn send(&self, evt: ImportProgressEvent) {
        match &self.inner {
            SinkInner::Noop => {}
            SinkInner::Live { tx } => {
                if evt.is_terminal() {
                    // Force-send: guaranteed delivery of terminal events.
                    let _ = tx.send(evt).await;
                } else {
                    // Best-effort: drop non-terminal events when the channel is full.
                    match tx.try_send(evt) {
                        Ok(()) => {}
                        Err(mpsc::error::TrySendError::Full(_)) => {
                            tracing::debug!("import progress: dropped non-terminal event (queue full)");
                        }
                        Err(mpsc::error::TrySendError::Closed(_)) => {
                            tracing::warn!("import progress: consumer gone");
                        }
                    }
                }
            }
        }
    }
}

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

    #[tokio::test]
    async fn terminal_events_always_delivered() {
        let (sink, mut rx) = ImportProgressSink::channel();
        // Fill the channel with non-terminal events (mostly drop).
        for i in 0..1000 {
            sink.send(ImportProgressEvent::SessionStarted {
                native_id: format!("s{i}"), index: i, total: 1000,
            }).await;
        }
        // Consumer drains in background.
        let handle = tokio::spawn(async move {
            let mut saw_done = false;
            while let Some(e) = rx.recv().await {
                if matches!(e, ImportProgressEvent::ImportDone { .. }) {
                    saw_done = true;
                    break;
                }
            }
            saw_done
        });
        sink.send(ImportProgressEvent::ImportDone { stats: ImportStats::default() }).await;
        let saw_done = tokio::time::timeout(std::time::Duration::from_secs(2), handle).await.unwrap().unwrap();
        assert!(saw_done);
    }

    #[tokio::test]
    async fn noop_sink_never_fails() {
        let sink = ImportProgressSink::noop();
        sink.send(ImportProgressEvent::ImportDone { stats: ImportStats::default() }).await;
    }
}
