//! Progress Bar Config

use std::{fmt::Write, io::Write as _, time::Duration};

use std::io::IsTerminal;
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Instant;

use bytesize::ByteSize;
use indicatif::{HumanDuration, MultiProgress, ProgressBar, ProgressState, ProgressStyle};

use clap::Parser;
use conflate::Merge;
use jiff::SignedDuration;
use log::info;

use serde::{Deserialize, Serialize};
use serde_with::{DisplayFromStr, serde_as};

use rustic_core::{Progress, ProgressBars, ProgressType, RusticProgress};

/// Returns the global `MultiProgress` instance used by all interactive progress bars.
///
/// Must be shared with `indicatif_log_bridge::LogWrapper` so that log output
/// suspends progress bars before printing.
pub fn multi_progress() -> &'static MultiProgress {
    static MP: OnceLock<MultiProgress> = OnceLock::new();
    MP.get_or_init(|| {
        let mp = MultiProgress::new();
        mp.set_move_cursor(true);
        mp
    })
}

mod constants {
    use std::time::Duration;

    pub(super) const DEFAULT_INTERVAL: Duration = Duration::from_millis(100);
    pub(super) const DEFAULT_LOG_INTERVAL: Duration = Duration::from_secs(10);
}

/// Progress Bar Config
#[serde_as]
#[derive(Default, Debug, Parser, Clone, Copy, Deserialize, Serialize, Merge)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct ProgressOptions {
    /// Don't show any progress bar
    #[clap(long, global = true, env = "RUSTIC_NO_PROGRESS")]
    #[merge(strategy=conflate::bool::overwrite_false)]
    pub no_progress: bool,

    /// Write progress as newline-delimited JSON
    #[clap(
        long,
        global = true,
        env = "RUSTIC_JSON_PROGRESS",
        conflicts_with = "no_progress"
    )]
    #[merge(strategy=conflate::bool::overwrite_false)]
    pub json_progress: bool,

    /// Interval to update progress bars (default: 100ms)
    #[clap(
        long,
        global = true,
        env = "RUSTIC_PROGRESS_INTERVAL",
        value_name = "DURATION",
        conflicts_with = "no_progress"
    )]
    #[serde_as(as = "Option<DisplayFromStr>")]
    #[merge(strategy=conflate::option::overwrite_none)]
    pub progress_interval: Option<SignedDuration>,
}

impl ProgressOptions {
    /// Get interval for interactive progress bars
    fn interactive_interval(&self) -> Duration {
        self.progress_interval
            .map_or(constants::DEFAULT_INTERVAL, |i| {
                i.try_into().expect("negative durations are not allowed")
            })
    }

    /// Get interval for non-interactive logging
    fn log_interval(&self) -> Duration {
        self.progress_interval
            .map_or(constants::DEFAULT_LOG_INTERVAL, |i| {
                i.try_into().expect("negative durations are not allowed")
            })
    }

    /// Factory Pattern: Create progress indicator based on terminal capabilities
    ///
    /// * `Hidden`: If --no-progress is set.
    /// * `Interactive`: If running in a TTY.
    /// * `NonInteractive`: If running in a pipe/service (logs to stderr).
    fn create_progress(&self, prefix: &str, kind: ProgressType) -> Progress {
        if self.no_progress {
            return Progress::hidden();
        }

        let interval = self.log_interval();
        if self.json_progress {
            return if interval > Duration::ZERO && matches!(kind, ProgressType::Bytes) {
                Progress::new(JsonProgress::new(prefix, interval, kind))
            } else {
                Progress::hidden()
            };
        }

        if std::io::stderr().is_terminal() {
            Progress::new(InteractiveProgress::new(
                prefix,
                kind,
                self.interactive_interval(),
            ))
        } else {
            if interval > Duration::ZERO {
                Progress::new(NonInteractiveProgress::new(prefix, interval, kind))
            } else {
                Progress::hidden()
            }
        }
    }
}

impl ProgressBars for ProgressOptions {
    fn progress(&self, progress_kind: ProgressType, prefix: &str) -> Progress {
        self.create_progress(prefix, progress_kind)
    }
}

// ================ Interactive ================
/// Wrapper around `indicatif::ProgressBar` for interactive terminal usage
#[derive(Debug, Clone)]
pub struct InteractiveProgress {
    bar: ProgressBar,
    kind: ProgressType,
}

impl InteractiveProgress {
    fn new(prefix: &str, kind: ProgressType, tick_interval: Duration) -> Self {
        let style = Self::initial_style(kind);
        let bar = multi_progress().add(ProgressBar::new(0).with_style(style));
        bar.set_prefix(prefix.to_string());
        bar.enable_steady_tick(tick_interval);
        Self { bar, kind }
    }

    #[allow(clippy::literal_string_with_formatting_args)]
    fn initial_style(kind: ProgressType) -> ProgressStyle {
        let template = match kind {
            ProgressType::Spinner => "[{elapsed_precise}] {prefix:30} {spinner}",
            ProgressType::Counter => "[{elapsed_precise}] {prefix:30} {bar:40.cyan/blue} {pos:>10}",
            ProgressType::Bytes => {
                "[{elapsed_precise}] {prefix:30} {bar:40.cyan/blue} {bytes:>10}            {bytes_per_sec:12}"
            }
        };
        ProgressStyle::default_bar().template(template).unwrap()
    }

    #[allow(clippy::literal_string_with_formatting_args)]
    fn style_with_length(kind: ProgressType) -> ProgressStyle {
        match kind {
            ProgressType::Spinner => Self::initial_style(kind),
            ProgressType::Counter => ProgressStyle::default_bar()
                .template("[{elapsed_precise}] {prefix:30} {bar:40.cyan/blue} {pos:>10}/{len:10}")
                .unwrap(),
            ProgressType::Bytes => ProgressStyle::default_bar()
                .with_key("my_eta", |s: &ProgressState, w: &mut dyn Write| {
                    let _ = match (s.pos(), s.len()) {
                        (pos, Some(len)) if pos != 0 && len > pos => {
                            let eta_secs = s.elapsed().as_secs() * (len - pos) / pos;
                            write!(w, "{:#}", HumanDuration(Duration::from_secs(eta_secs)))
                        }
                        _ => write!(w, "-"),
                    };
                })
                .template("[{elapsed_precise}] {prefix:30} {bar:40.cyan/blue} {bytes:>10}/{total_bytes:10} {bytes_per_sec:12} (ETA {my_eta})")
                .unwrap(),
        }
    }
}

impl RusticProgress for InteractiveProgress {
    fn is_hidden(&self) -> bool {
        false
    }

    fn set_length(&self, len: u64) {
        if matches!(self.kind, ProgressType::Bytes | ProgressType::Counter) {
            self.bar.set_style(Self::style_with_length(self.kind));
        }
        self.bar.set_length(len);
    }

    fn set_title(&self, title: &str) {
        self.bar.set_prefix(title.to_string());
    }

    fn inc(&self, inc: u64) {
        self.bar.inc(inc);
    }

    fn finish(&self) {
        self.bar.finish_with_message("done");
    }
}

// ================ Non-Interactive ================

/// Store state for non-interactive progress
#[derive(Debug)]
struct NonInteractiveState {
    prefix: String,
    position: u64,
    length: Option<u64>,
    last_log: Instant,
}

impl NonInteractiveState {
    fn progress_text(&self, kind: ProgressType) -> String {
        let format_value = |value| match kind {
            ProgressType::Bytes => ByteSize(value).to_string(),
            ProgressType::Counter | ProgressType::Spinner => value.to_string(),
        };

        self.length.map_or_else(
            || format_value(self.position),
            |len| format!("{} / {}", format_value(self.position), format_value(len)),
        )
    }

    fn should_log(&self, interval: Duration) -> bool {
        self.last_log.elapsed() >= interval
    }

    fn mark_logged(&mut self) {
        self.last_log = Instant::now();
    }
}

/// Periodic logger for non-interactive environments (i.e. systemd)
/// Implemented thread-safe and decouples logging logic from indicatif
#[derive(Clone, Debug)]
pub struct NonInteractiveProgress {
    state: Arc<Mutex<NonInteractiveState>>,
    start: Instant,
    interval: Duration,
    kind: ProgressType,
}

impl NonInteractiveProgress {
    fn new(prefix: &str, interval: Duration, kind: ProgressType) -> Self {
        let now = Instant::now();
        Self {
            state: Arc::new(Mutex::new(NonInteractiveState {
                prefix: prefix.to_string(),
                position: 0,
                length: None,
                last_log: now,
            })),
            start: now,
            interval,
            kind,
        }
    }

    fn format_value(&self, value: u64) -> String {
        match self.kind {
            ProgressType::Bytes => ByteSize(value).to_string(), // delegate bytesize handling
            ProgressType::Counter | ProgressType::Spinner => value.to_string(),
        }
    }

    fn log_progress(&self, state: &NonInteractiveState) {
        info!("{}: {}", state.prefix, state.progress_text(self.kind));
    }
}

impl RusticProgress for NonInteractiveProgress {
    fn is_hidden(&self) -> bool {
        false
    }

    fn set_length(&self, len: u64) {
        if let Ok(mut state) = self.state.lock() {
            state.length = Some(len);
        }
    }

    fn set_title(&self, title: &str) {
        if let Ok(mut state) = self.state.lock() {
            state.prefix = title.to_string();
        }
    }

    fn inc(&self, inc: u64) {
        if let Ok(mut state) = self.state.lock() {
            state.position += inc;

            if state.should_log(self.interval) {
                self.log_progress(&state);
                state.mark_logged();
            }
        }
    }

    fn finish(&self) {
        let Ok(state) = self.state.lock() else {
            return;
        };

        info!(
            "{}: {} done in {:.2?}",
            state.prefix,
            self.format_value(state.position),
            self.start.elapsed()
        );
    }
}

// ================ JSON ================

/// Periodic JSON lines progress for machine-readable consumers
#[derive(Clone, Debug)]
pub struct JsonProgress {
    state: Arc<Mutex<NonInteractiveState>>,
    start: Instant,
    interval: Duration,
    kind: ProgressType,
}

#[derive(Serialize)]
struct JsonProgressStatus {
    message_type: &'static str,
    seconds_elapsed: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    seconds_remaining: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    percent_done: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    total_bytes: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    bytes_done: Option<u64>,
}

impl JsonProgress {
    fn new(prefix: &str, interval: Duration, kind: ProgressType) -> Self {
        let now = Instant::now();
        Self {
            state: Arc::new(Mutex::new(NonInteractiveState {
                prefix: prefix.to_string(),
                position: 0,
                length: None,
                last_log: now,
            })),
            start: now,
            interval,
            kind,
        }
    }

    fn log_progress(&self, state: &NonInteractiveState) {
        let is_bytes = matches!(self.kind, ProgressType::Bytes);
        let elapsed = self.start.elapsed().as_secs();
        let percent_done = state
            .length
            .filter(|len| *len > 0)
            .map(|len| (state.position as f64 / len as f64).min(1.0));
        let seconds_remaining = match (state.position, state.length) {
            (position, Some(len)) if position > 0 && len > position => {
                Some(elapsed.saturating_mul(len - position) / position)
            }
            _ => None,
        };

        let status = JsonProgressStatus {
            message_type: "status",
            seconds_elapsed: elapsed,
            seconds_remaining,
            percent_done,
            total_bytes: is_bytes.then_some(state.length).flatten(),
            bytes_done: is_bytes.then_some(state.position),
        };

        let mut stdout = std::io::stdout().lock();
        _ = serde_json::to_writer(&mut stdout, &status);
        _ = writeln!(stdout);
    }
}

impl RusticProgress for JsonProgress {
    fn is_hidden(&self) -> bool {
        false
    }

    fn set_length(&self, len: u64) {
        if let Ok(mut state) = self.state.lock() {
            state.length = Some(len);
            self.log_progress(&state);
            state.mark_logged();
        }
    }

    fn set_title(&self, title: &str) {
        if let Ok(mut state) = self.state.lock() {
            state.prefix = title.to_string();
        }
    }

    fn inc(&self, inc: u64) {
        if let Ok(mut state) = self.state.lock() {
            state.position += inc;

            if state.should_log(self.interval) {
                self.log_progress(&state);
                state.mark_logged();
            }
        }
    }

    fn finish(&self) {
        let Ok(state) = self.state.lock() else {
            return;
        };

        self.log_progress(&state);
    }
}
