//! TUI rendering helper functions

use arrayvec::ArrayString;
use ratatui::style::Color;

use super::constants::{BACKEND_COLORS, throughput};
use super::dashboard::BackendView;
use super::types::{BackendChartData, ChartDataVec, ChartPoint, ChartX, ChartY, PointVec};
use crate::tui::app::ThroughputPoint;

// ============================================================================
// Sparkline Rendering
// ============================================================================

/// Width of bandwidth sparkline bars in characters
const SPARKLINE_WIDTH: usize = 15;

/// Create a text-based sparkline bar visualization
///
/// Creates a bar using filled (█) and empty (░) characters scaled
/// relative to the maximum value.
///
/// # Arguments
/// * `value` - The value to visualize
/// * `max_value` - The maximum value for scaling (0-100% range)
///
/// # Returns
/// A string of length `SPARKLINE_WIDTH` with filled/empty blocks
// Sparkline fill count is a bounded UI-only conversion from integer percentages.
#[allow(
    clippy::cast_precision_loss,
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss
)]
#[cfg(test)]
#[must_use]
pub fn create_sparkline(value: u64, max_value: u64) -> String {
    create_sparkline_text(value, max_value).to_string()
}

/// Create a text-based sparkline bar without heap allocation.
#[must_use]
pub fn create_sparkline_text(value: u64, max_value: u64) -> ArrayString<64> {
    let filled = if max_value > 0 {
        // Sparkline width is tiny and purely visual, so approximate float math is
        // sufficient and keeping the cast local makes the display tradeoff explicit.
        ((value as f64 / max_value as f64) * SPARKLINE_WIDTH as f64) as usize
    } else {
        0
    };

    let filled = filled.min(SPARKLINE_WIDTH);
    let empty = SPARKLINE_WIDTH.saturating_sub(filled);
    let mut bar = ArrayString::<64>::new();
    for _ in 0..filled {
        bar.push_str("█");
    }
    for _ in 0..empty {
        bar.push_str("░");
    }
    bar
}

// ============================================================================
// Chart Data Building
// ============================================================================

/// Build chart data for all backends using functional pipeline
///
/// Single-pass processing:
/// 1. Maps over servers to extract history
/// 2. Folds over history points to build point vectors and track max
/// 3. Accumulates results with global max tracking
///
/// Returns (`chart_data`, `global_max_throughput`)
pub fn build_chart_data(backend_views: &[BackendView]) -> (ChartDataVec, f64) {
    /// Extract data from a single throughput point
    #[inline]
    fn extract_point_data(idx: usize, point: &ThroughputPoint) -> (ChartPoint, ChartPoint, ChartY) {
        let x = ChartX::from(idx);
        let sent = ChartY::from(point.sent_per_sec().get());
        let recv = ChartY::from(point.received_per_sec().get());
        (
            ChartPoint::new(x, sent),
            ChartPoint::new(x, recv),
            sent.max(recv),
        )
    }

    /// Accumulate points and track maximum (pure function)
    #[inline]
    fn accumulate_points(
        ((mut sent_vec, mut recv_vec), max): ((PointVec, PointVec), ChartY),
        (sent_point, recv_point, point_max): (ChartPoint, ChartPoint, ChartY),
    ) -> ((PointVec, PointVec), ChartY) {
        sent_vec.push(sent_point);
        recv_vec.push(recv_point);
        ((sent_vec, recv_vec), max.max(point_max))
    }

    // Functional pipeline: enumerate → map → fold
    backend_views
        .iter()
        .enumerate()
        .map(|(index, backend)| {
            let ((sent_points, recv_points), backend_max) = backend
                .history
                .iter()
                .enumerate()
                .map(|(idx, point)| extract_point_data(idx, point))
                .fold(
                    ((PointVec::new(), PointVec::new()), ChartY::from(0.0)),
                    accumulate_points,
                );

            (
                BackendChartData::new(
                    backend.server.name.as_str().to_string(),
                    backend_color(index),
                    &sent_points,
                    &recv_points,
                ),
                backend_max.get(),
            )
        })
        .fold(
            (ChartDataVec::new(), 0.0_f64),
            |(mut chart_data, global_max), (backend_data, backend_max)| {
                chart_data.push(backend_data);
                (chart_data, global_max.max(backend_max))
            },
        )
}

// ============================================================================
// Helper Functions
// ============================================================================

/// Get color for backend by index (round-robin through palette)
#[inline]
#[must_use]
pub fn backend_color(index: usize) -> Color {
    BACKEND_COLORS[index % BACKEND_COLORS.len()]
}

/// Round throughput value up to next nice number for chart axis
///
/// Uses sensible rounding based on magnitude:
/// - > 100 MiB/s → round to nearest 10 MiB/s
/// - > 10 MiB/s → round to nearest 1 MiB/s
/// - > 1 MiB/s → round to nearest 100 KiB/s
/// - Otherwise → round to nearest 10 KiB/s
#[must_use]
pub fn round_up_throughput(value: f64) -> f64 {
    if value == throughput::HUNDRED_MIB
        || value == throughput::TEN_MIB
        || value == throughput::ONE_MIB
    {
        value
    } else if value > throughput::HUNDRED_MIB {
        (value / throughput::TEN_MIB).ceil() * throughput::TEN_MIB
    } else if value > throughput::TEN_MIB {
        (value / throughput::ONE_MIB).ceil() * throughput::ONE_MIB
    } else if value > throughput::ONE_MIB {
        (value / throughput::HUNDRED_KIB).ceil() * throughput::HUNDRED_KIB
    } else {
        (value / throughput::TEN_KIB).ceil() * throughput::TEN_KIB
    }
}

/// Format throughput value for axis label
///
/// Returns human-readable string like "10 MiB/s", "500 KiB/s", "100 B/s"
#[must_use]
pub fn format_throughput_label(value: f64) -> String {
    if value >= throughput::ONE_MIB {
        format!("{:.0} MiB/s", value / throughput::ONE_MIB)
    } else if value >= throughput::ONE_KIB {
        format!("{:.0} KiB/s", value / throughput::ONE_KIB)
    } else {
        format!("{value:.0} B/s")
    }
}

// ============================================================================
// Summary Helpers
// ============================================================================

/// Format throughput strings for summary display
#[must_use]
pub fn format_summary_throughput(latest_throughput: Option<&ThroughputPoint>) -> (String, String) {
    use super::constants::text;

    latest_throughput.map_or_else(
        || {
            (
                format!("{}{}", text::ARROW_UP, text::DEFAULT_THROUGHPUT),
                format!("{}{}", text::ARROW_DOWN, text::DEFAULT_THROUGHPUT),
            )
        },
        |point| {
            (
                format!("{}{}", text::ARROW_UP, point.sent_per_sec()),
                format!("{}{}", text::ARROW_DOWN, point.received_per_sec()),
            )
        },
    )
}

// ============================================================================
// Backend Display Helpers
// ============================================================================

/// Health status icon and color
#[must_use]
pub const fn health_indicator(
    status: crate::metrics::BackendHealthStatus,
) -> (&'static str, Color) {
    use crate::metrics::BackendHealthStatus;
    match status {
        BackendHealthStatus::Healthy => ("●", Color::Green),
        BackendHealthStatus::Degraded => ("◐", Color::Yellow),
        BackendHealthStatus::Down => ("○", Color::Red),
    }
}

/// Format error rate with warning icon if critical
#[cfg(test)]
#[must_use]
pub fn format_error_rate(rate: f64) -> String {
    match rate {
        r if r > 5.0 => format!(" ⚠ {r:.1}%"),
        r if r > 0.0 => format!(" {r:.1}%"),
        _ => String::new(),
    }
}

/// Color for error rate display
#[must_use]
pub const fn error_rate_color(rate: f64) -> Color {
    if rate > 5.0 {
        Color::Red
    } else {
        Color::Yellow
    }
}

/// Color for error count
#[must_use]
pub const fn error_count_color(has_errors: bool) -> Color {
    use super::constants::styles;
    if has_errors {
        Color::Yellow
    } else {
        styles::VALUE_NEUTRAL
    }
}

/// Color for connection failures
#[must_use]
pub const fn connection_failure_color(failures: u64) -> Color {
    use super::constants::styles;
    if failures > 0 {
        Color::Red
    } else {
        styles::VALUE_NEUTRAL
    }
}

// ============================================================================
// Chart Helpers
// ============================================================================

/// Calculate chart bounds (clamped and rounded for nice axis labels)
#[must_use]
pub fn calculate_chart_bounds(max_throughput: f64) -> f64 {
    use super::constants::chart;

    let clamped = max_throughput.max(chart::MIN_THROUGHPUT);
    round_up_throughput(clamped)
}

#[cfg(test)]
#[allow(clippy::float_cmp)] // These helper tests use exact expected fixture values for chart math.
mod tests {
    use super::*;

    #[test]
    fn test_backend_color_cycles() {
        let color0 = backend_color(0);
        let color_wrap = backend_color(BACKEND_COLORS.len());
        assert_eq!(color0, color_wrap, "Should wrap around");
    }

    #[test]
    fn test_backend_color_distinct() {
        // First few backends should have distinct colors
        let color0 = backend_color(0);
        let color1 = backend_color(1);
        let color2 = backend_color(2);

        assert_ne!(color0, color1);
        assert_ne!(color1, color2);
        assert_ne!(color0, color2);
    }

    #[test]
    fn test_round_up_throughput() {
        // > 100 MiB/s → round to 10 MiB/s
        assert_eq!(round_up_throughput(110_000_000.0), 115_343_360.0);

        // > 10 MiB/s → round to 1 MiB/s
        assert_eq!(round_up_throughput(15_500_000.0), 15_728_640.0);

        // > 1 MiB/s → round to 100 KiB/s
        assert_eq!(round_up_throughput(1_250_000.0), 1_331_200.0);

        // < 1 MiB/s → round to 10 KiB/s
        assert_eq!(round_up_throughput(55_000.0), 61_440.0);
    }

    #[test]
    fn test_round_up_throughput_exact_boundaries() {
        // Test exact boundary values
        assert_eq!(round_up_throughput(104_857_600.0), 104_857_600.0);
        assert_eq!(round_up_throughput(10_485_760.0), 10_485_760.0);
        assert_eq!(round_up_throughput(1_048_576.0), 1_048_576.0);
    }

    #[test]
    fn test_format_throughput_label() {
        assert_eq!(format_throughput_label(10_485_760.0), "10 MiB/s");
        assert_eq!(format_throughput_label(500_000.0), "488 KiB/s");
        assert_eq!(format_throughput_label(100.0), "100 B/s");
    }

    #[test]
    fn test_format_throughput_label_boundaries() {
        // Test boundary values
        assert_eq!(format_throughput_label(1_048_576.0), "1 MiB/s");
        assert_eq!(format_throughput_label(1_024.0), "1 KiB/s");
        assert_eq!(format_throughput_label(0.0), "0 B/s");
    }

    #[test]
    fn test_point_vec_type() {
        // Verify PointVec can hold typical history size on stack
        let mut points = PointVec::new();

        // Add 60 points (typical 60-second history)
        for i in 0..60 {
            points.push(ChartPoint::new(
                ChartX::from(i),
                ChartY::from(f64::from(
                    u32::try_from(i * 1000).expect("test value fits into u32"),
                )),
            ));
        }

        assert_eq!(points.len(), 60);
        assert_eq!(points[0].x.get(), 0.0);
        assert_eq!(points[59].x.get(), 59.0);
    }

    #[test]
    fn test_chart_data_vec_type() {
        // Verify ChartDataVec can hold typical backend count on stack
        let mut chart_data = ChartDataVec::new();

        // Add 8 backends (at SmallVec capacity)
        for i in 0..8 {
            chart_data.push(BackendChartData::new(
                format!("Server {i}"),
                backend_color(i),
                &PointVec::new(),
                &PointVec::new(),
            ));
        }

        assert_eq!(chart_data.len(), 8);
        assert_eq!(chart_data[0].name, "Server 0");
        assert_eq!(chart_data[7].name, "Server 7");
    }

    #[test]
    fn test_backend_chart_data_structure() {
        let data = BackendChartData::new(
            "Test Server".to_string(),
            backend_color(0),
            &PointVec::new(),
            &PointVec::new(),
        );

        assert_eq!(data.name, "Test Server");
        // Verify pre-computed tuples are empty
        assert_eq!(data.sent_points_as_tuples().len(), 0);
        assert_eq!(data.recv_points_as_tuples().len(), 0);
    }

    // ========================================================================
    // Summary Tests
    // ========================================================================

    #[test]
    fn test_format_summary_throughput_none() {
        use super::super::constants::text;

        let (up, down) = format_summary_throughput(None);

        assert!(up.starts_with(text::ARROW_UP));
        assert!(down.starts_with(text::ARROW_DOWN));
        assert!(up.contains(text::DEFAULT_THROUGHPUT));
        assert!(down.contains(text::DEFAULT_THROUGHPUT));
    }

    #[test]
    fn test_format_summary_throughput_with_data() {
        use super::super::constants::text;
        use crate::types::tui::{CommandsPerSecond, Throughput, Timestamp};

        let point = ThroughputPoint::new_backend(
            Timestamp::now(),
            Throughput::new(1_000_000.0),
            Throughput::new(2_000_000.0),
            CommandsPerSecond::new(10.0),
        );

        let (up, down) = format_summary_throughput(Some(&point));

        assert!(up.starts_with(text::ARROW_UP));
        assert!(down.starts_with(text::ARROW_DOWN));
        // Should contain formatted throughput values
        assert!(up.contains("MiB/s") || up.contains("KiB/s") || up.contains("B/s"));
        assert!(down.contains("MiB/s") || down.contains("KiB/s") || down.contains("B/s"));
    }

    // ========================================================================
    // Sparkline Tests
    // ========================================================================

    #[test]
    fn test_create_sparkline_full() {
        let bar = create_sparkline(100, 100);
        assert_eq!(bar.len(), SPARKLINE_WIDTH * "█".len());
        assert_eq!(bar, "█".repeat(SPARKLINE_WIDTH));
    }

    #[test]
    fn test_create_sparkline_empty() {
        let bar = create_sparkline(0, 100);
        assert_eq!(bar.len(), SPARKLINE_WIDTH * "░".len());
        assert_eq!(bar, "░".repeat(SPARKLINE_WIDTH));
    }

    #[test]
    fn test_create_sparkline_half() {
        let bar = create_sparkline(50, 100);
        let expected_filled = SPARKLINE_WIDTH / 2;
        assert!(bar.starts_with(&"█".repeat(expected_filled)));
        assert!(bar.ends_with(&"░".repeat(SPARKLINE_WIDTH - expected_filled)));
    }

    #[test]
    fn test_create_sparkline_zero_max() {
        let bar = create_sparkline(50, 0);
        // Should be all empty when max is 0
        assert_eq!(bar, "░".repeat(SPARKLINE_WIDTH));
    }

    #[test]
    fn create_sparkline_text_matches_owned_sparkline() {
        for (value, max_value) in [(0, 100), (50, 100), (100, 100), (50, 0)] {
            assert_eq!(
                create_sparkline_text(value, max_value).as_str(),
                create_sparkline(value, max_value)
            );
        }
    }

    // ========================================================================
    // Chart Bounds Tests
    // ========================================================================

    #[test]
    fn test_calculate_chart_bounds_below_min() {
        use super::super::constants::chart;

        // Should clamp to MIN_THROUGHPUT and round
        let bounds = calculate_chart_bounds(500_000.0);
        assert!(bounds >= chart::MIN_THROUGHPUT);
    }

    #[test]
    fn test_calculate_chart_bounds_above_min() {
        // Should round up nicely
        let bounds = calculate_chart_bounds(15_500_000.0);
        assert_eq!(bounds, 15_728_640.0); // Rounded to 15 MiB/s
    }

    #[test]
    fn test_calculate_chart_bounds_zero() {
        use super::super::constants::chart;

        // Zero should clamp to minimum
        let bounds = calculate_chart_bounds(0.0);
        assert_eq!(bounds, round_up_throughput(chart::MIN_THROUGHPUT));
    }
}
