//! Live terminal progress: per-repo spinners for multi-repo ops.
//!
//! Animated only when stderr is a TTY (and NO_COLOR / RV_NO_PROGRESS are
//! unset). Otherwise every finish prints the exact plain line the op printed
//! before this module existed, so piped output, tests, and agents see stable
//! text.

use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use std::time::{Duration, Instant};

const TICK_MS: u64 = 70;
const TICKS: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];

fn animated() -> bool {
    use std::io::IsTerminal;
    std::env::var_os("NO_COLOR").is_none()
        && std::env::var_os("RV_NO_PROGRESS").is_none()
        && std::env::var_os("CI").is_none()
        && std::env::var_os("GITHUB_ACTIONS").is_none()
        && std::io::stderr().is_terminal()
}

pub struct Multi {
    mp: Option<MultiProgress>,
}

impl Multi {
    pub fn new() -> Self {
        Self {
            mp: animated().then(MultiProgress::new),
        }
    }

    /// Spinner labeled `<label> <verb>…` until finished.
    pub fn task(&self, label: &str, verb: &str) -> Task {
        let bar = self.mp.as_ref().map(|mp| {
            let bar = mp.add(ProgressBar::new_spinner());
            bar.set_style(
                ProgressStyle::with_template("  {spinner:.cyan.bold} {msg}")
                    .expect("static template")
                    .tick_strings(TICKS),
            );
            bar.set_message(format!("\x1b[1m{label}\x1b[0m \x1b[2m{verb}…\x1b[0m"));
            bar.enable_steady_tick(Duration::from_millis(TICK_MS));
            bar
        });
        Task {
            bar,
            started: Instant::now(),
        }
    }

    /// Print a line above any active spinners (plain println otherwise).
    pub fn println(&self, line: &str) {
        match &self.mp {
            Some(mp) => {
                let _ = mp.println(line);
            }
            None => println!("{line}"),
        }
    }

    /// Like [`Multi::println`] but stderr in plain mode.
    pub fn eprintln(&self, line: &str) {
        match &self.mp {
            Some(mp) => {
                let _ = mp.println(line);
            }
            None => eprintln!("{line}"),
        }
    }
}

impl Default for Multi {
    fn default() -> Self {
        Self::new()
    }
}

pub struct Task {
    bar: Option<ProgressBar>,
    started: Instant,
}

impl Task {
    fn finish(self, glyph: &str, styled: String, msg: &str, to_stderr: bool) {
        match self.bar {
            Some(bar) => {
                let secs = self.started.elapsed().as_secs_f32();
                bar.set_style(ProgressStyle::with_template("  {msg}").expect("static template"));
                bar.finish_with_message(format!("{glyph} {styled} \x1b[2m{secs:.1}s\x1b[0m"));
            }
            None if to_stderr => eprintln!("  {msg}"),
            None => println!("  {msg}"),
        }
    }

    /// Success: animated `✔ <msg>`, or plain `  <msg>` on stdout.
    pub fn done(self, msg: &str) {
        self.finish("\x1b[32m✔\x1b[0m", msg.to_string(), msg, false);
    }

    /// Failure: animated red `✘ <msg>`, or plain `  <msg>` on stderr.
    pub fn fail(self, msg: &str) {
        self.finish(
            "\x1b[31m✘\x1b[0m",
            format!("\x1b[31m{msg}\x1b[0m"),
            msg,
            true,
        );
    }

    /// Skip/no-op: animated dim `– <msg>`, or plain `  <msg>` on stderr.
    pub fn skip(self, msg: &str) {
        self.finish("\x1b[2m–\x1b[0m", format!("\x1b[2m{msg}\x1b[0m"), msg, true);
    }
}
