use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use std::io::Write;
use std::sync::LazyLock;
use tracing_subscriber::fmt::MakeWriter;

static MULTI: LazyLock<MultiProgress> = LazyLock::new(MultiProgress::new);

pub fn multi() -> &'static MultiProgress {
    &MULTI
}

/// Create a spinner for a numbered pipeline stage.
///
/// Returns a hidden (no-op) bar in robot mode so callers can use
/// the same code path regardless of output mode.
pub fn stage_spinner(stage: u8, total: u8, msg: &str, robot_mode: bool) -> ProgressBar {
    if robot_mode {
        return ProgressBar::hidden();
    }
    let pb = multi().add(ProgressBar::new_spinner());
    pb.set_style(
        ProgressStyle::default_spinner()
            .template("{spinner:.blue} {prefix} {msg}")
            .expect("valid template"),
    );
    pb.enable_steady_tick(std::time::Duration::from_millis(80));
    pb.set_prefix(format!("[{stage}/{total}]"));
    pb.set_message(msg.to_string());
    pb
}

#[derive(Clone)]
pub struct SuspendingWriter;

pub struct SuspendingWriterInner {
    buf: Vec<u8>,
}

impl Write for SuspendingWriterInner {
    fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
        self.buf.extend_from_slice(data);
        Ok(data.len())
    }

    fn flush(&mut self) -> std::io::Result<()> {
        Ok(())
    }
}

impl Drop for SuspendingWriterInner {
    fn drop(&mut self) {
        if !self.buf.is_empty() {
            let buf = std::mem::take(&mut self.buf);
            MULTI.suspend(|| {
                let _ = std::io::stderr().write_all(&buf);
                let _ = std::io::stderr().flush();
            });
        }
    }
}

impl<'a> MakeWriter<'a> for SuspendingWriter {
    type Writer = SuspendingWriterInner;

    fn make_writer(&'a self) -> Self::Writer {
        SuspendingWriterInner { buf: Vec::new() }
    }
}

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

    #[test]
    fn multi_returns_same_instance() {
        let a = multi() as *const MultiProgress;
        let b = multi() as *const MultiProgress;
        assert!(std::ptr::eq(a, b));
    }

    #[test]
    fn added_bar_is_tracked() {
        let bar = multi().add(ProgressBar::new_spinner());
        bar.set_message("test");
        bar.finish_and_clear();
    }

    #[test]
    fn hidden_bar_works_with_multi() {
        let bar = multi().add(ProgressBar::hidden());
        bar.set_message("hidden");
        bar.finish_and_clear();
    }

    #[test]
    fn suspending_writer_buffers_and_flushes() {
        let writer = SuspendingWriter;
        let mut w = MakeWriter::make_writer(&writer);
        let n = w.write(b"test log line\n").unwrap();
        assert_eq!(n, 14);
        drop(w);
    }

    #[test]
    fn suspending_writer_empty_does_not_flush() {
        let writer = SuspendingWriter;
        let w = MakeWriter::make_writer(&writer);
        drop(w);
    }

    #[test]
    fn stage_spinner_robot_mode_returns_hidden() {
        let pb = stage_spinner(1, 3, "Testing...", true);
        assert!(pb.is_hidden());
    }

    #[test]
    fn stage_spinner_human_mode_sets_properties() {
        // In non-TTY test environments, MultiProgress may report bars as
        // hidden. Verify the human-mode code path by checking that prefix
        // and message are configured (robot-mode returns a bare hidden bar).
        let pb = stage_spinner(1, 3, "Testing...", false);
        assert_eq!(pb.prefix(), "[1/3]");
        assert_eq!(pb.message(), "Testing...");
        pb.finish_and_clear();
    }

    #[test]
    fn stage_spinner_sets_prefix_format() {
        let pb = stage_spinner(2, 5, "Working...", false);
        assert_eq!(pb.prefix(), "[2/5]");
        pb.finish_and_clear();
    }

    #[test]
    fn stage_spinner_sets_message() {
        let pb = stage_spinner(1, 3, "Seeding timeline...", false);
        assert_eq!(pb.message(), "Seeding timeline...");
        pb.finish_and_clear();
    }
}
