use bmux_appearance::RuntimeAppearance;
use bmux_config::{
    StatusAlignActive, StatusBarConfig, StatusBarPreset, StatusDensity, StatusHintPolicy,
    StatusOverflowStyle, StatusSeparatorSet,
};
use bmux_plugin::{RenderColor, RenderStyle, RenderTextSpan};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use uuid::Uuid;

pub struct AttachTab {
    pub label: String,
    pub active: bool,
    pub context_id: Option<Uuid>,
}

#[derive(Clone, Debug)]
pub struct AttachStatusTabHitbox {
    pub start_col: u16,
    pub end_col: u16,
    pub context_id: Uuid,
}

#[derive(Clone, Debug)]
pub struct AttachStatusLine {
    /// ANSI-styled status line retained for text consumers and compatibility.
    pub rendered: String,
    /// Declarative spans for render paths that want display-cell-safe styling.
    pub spans: Vec<RenderTextSpan>,
    pub tab_hitboxes: Vec<AttachStatusTabHitbox>,
    pub drag_marker_col: Option<u16>,
}

#[allow(clippy::too_many_arguments, clippy::cast_possible_truncation)]
pub fn build_attach_status_line(
    width: u16,
    config: &StatusBarConfig,
    runtime_appearance: &RuntimeAppearance,
    session_label: &str,
    session_count: usize,
    current_context_label: &str,
    tabs: &[AttachTab],
    tab_position_label: Option<&str>,
    mode_label: &str,
    role_label: &str,
    follow_label: Option<&str>,
    hint: &str,
) -> AttachStatusLine {
    if !config.enabled {
        return AttachStatusLine {
            rendered: String::new(),
            spans: Vec::new(),
            tab_hitboxes: Vec::new(),
            drag_marker_col: None,
        };
    }

    let style = StatusRenderStyle::from_config(config);
    let resolved_appearance = ResolvedStatusAppearance::resolve(config, runtime_appearance);
    let mut left = String::new();
    let mut tab_hitboxes = Vec::new();
    let mut overflow_ranges = Vec::new();
    left.push_str(&" ".repeat(config.layout.left_padding));

    append_tabs(
        &mut left,
        &mut tab_hitboxes,
        &mut overflow_ranges,
        config,
        tabs,
        &style,
    );

    if config.show_session_name {
        append_segment(
            &mut left,
            &style.module_separator,
            &format!("session:{session_label} ({session_count})"),
        );
    }
    if config.show_context_name {
        append_segment(
            &mut left,
            &style.module_separator,
            &format!("ctx:{current_context_label}"),
        );
    }

    let mut right_segments = Vec::new();
    let mut mode_range = None;
    if config.show_mode {
        append_right_segment(
            &mut right_segments,
            &style,
            style.badge(mode_label),
            Some(&mut mode_range),
        );
    }
    if config.show_role {
        append_right_segment(&mut right_segments, &style, style.badge(role_label), None);
    }
    if let Some(tab_position_label) = tab_position_label {
        append_right_segment(
            &mut right_segments,
            &style,
            style.badge(tab_position_label),
            None,
        );
    }
    if config.show_follow
        && let Some(follow) = follow_label
    {
        append_right_segment(&mut right_segments, &style, style.badge(follow), None);
    }
    if config.show_hint && hint_allowed(config.hint_policy, mode_label) {
        append_right_segment(&mut right_segments, &style, style.badge(hint), None);
    }
    let mut right = right_segments.concat();
    if config.layout.right_padding > 0 {
        right.push_str(&" ".repeat(config.layout.right_padding));
    }

    let composed = compose_status_line(width, &left, &right);
    clamp_hitboxes_to_width(&mut tab_hitboxes, width);

    attach_status_line_from_composed(
        &composed,
        width,
        config,
        &resolved_appearance,
        tabs,
        tab_hitboxes,
        &overflow_ranges,
        mode_range,
    )
}

#[allow(clippy::too_many_arguments)]
fn attach_status_line_from_composed(
    composed: &ComposedStatusLine,
    width: u16,
    config: &StatusBarConfig,
    resolved_appearance: &ResolvedStatusAppearance,
    tabs: &[AttachTab],
    tab_hitboxes: Vec<AttachStatusTabHitbox>,
    overflow_ranges: &[(usize, usize)],
    mode_range: Option<(usize, usize)>,
) -> AttachStatusLine {
    let resolved_mode_range = mode_range.map(|(start, end)| {
        let right_start = composed.right_start_col.unwrap_or(0);
        (
            right_start.saturating_add(start),
            right_start.saturating_add(end),
        )
    });
    let rendered = stylize_status_line(
        &composed.rendered,
        width,
        config,
        resolved_appearance,
        tabs,
        &tab_hitboxes,
        overflow_ranges,
        resolved_mode_range,
        composed.right_start_col,
    );
    let spans = status_line_spans(
        &composed.rendered,
        width,
        config,
        resolved_appearance,
        tabs,
        &tab_hitboxes,
        overflow_ranges,
        resolved_mode_range,
        composed.right_start_col,
    );

    AttachStatusLine {
        rendered,
        spans,
        tab_hitboxes,
        drag_marker_col: None,
    }
}

fn hint_allowed(policy: StatusHintPolicy, mode_label: &str) -> bool {
    match policy {
        StatusHintPolicy::Always => true,
        StatusHintPolicy::ScrollOnly => mode_label == "SCROLL",
        StatusHintPolicy::Never => false,
    }
}

fn append_tabs(
    out: &mut String,
    hitboxes: &mut Vec<AttachStatusTabHitbox>,
    overflow_ranges: &mut Vec<(usize, usize)>,
    config: &StatusBarConfig,
    tabs: &[AttachTab],
    style: &StatusRenderStyle,
) {
    if tabs.is_empty() {
        out.push_str(&style.empty_tabs_label);
        return;
    }

    let max_tabs = config.max_tabs.max(1);
    let (visible, hidden_left, hidden_right) = visible_tabs_for_layout(tabs, max_tabs, config);
    let mut col = display_width(out);

    if hidden_left > 0 {
        let marker = style.overflow_marker(hidden_left);
        let start = col;
        out.push_str(&marker);
        col = col.saturating_add(display_width(&marker));
        let end = col.saturating_sub(1);
        overflow_ranges.push((start, end));
        out.push_str(&style.tab_separator);
        col = col.saturating_add(display_width(&style.tab_separator));
    }

    for (index, tab) in visible.iter().enumerate() {
        if index > 0 {
            out.push_str(&style.tab_separator);
            col = col.saturating_add(display_width(&style.tab_separator));
        }
        let label = truncate_cells(&tab.label, config.tab_label_max_width.max(1));
        let global_index = tabs
            .iter()
            .position(|entry| entry.context_id == tab.context_id)
            .unwrap_or(index);
        let indexed = if config.show_tab_index {
            format!("{}:{}", global_index + 1, label)
        } else {
            label
        };
        let token = if tab.active {
            style.active_tab(&indexed)
        } else {
            style.inactive_tab(&indexed)
        };
        out.push_str(&token);
        let token_width = display_width(&token);
        if let Some(context_id) = tab.context_id {
            hitboxes.push(AttachStatusTabHitbox {
                start_col: u16::try_from(col).unwrap_or(u16::MAX),
                end_col: u16::try_from(col.saturating_add(token_width.saturating_sub(1)))
                    .unwrap_or(u16::MAX),
                context_id,
            });
        }
        col = col.saturating_add(token_width);
    }

    if hidden_right > 0 {
        out.push_str(&style.tab_separator);
        col = col.saturating_add(display_width(&style.tab_separator));
        let marker = style.overflow_marker(hidden_right);
        let start = col;
        out.push_str(&marker);
        col = col.saturating_add(display_width(&marker));
        let end = col.saturating_sub(1);
        overflow_ranges.push((start, end));
    }
}

fn visible_tabs_for_layout<'a>(
    tabs: &'a [AttachTab],
    max_tabs: usize,
    config: &StatusBarConfig,
) -> (Vec<&'a AttachTab>, usize, usize) {
    if tabs.len() <= max_tabs {
        return (tabs.iter().collect(), 0, 0);
    }
    let active_index = tabs.iter().position(|tab| tab.active).unwrap_or(0);
    let start = match config.layout.align_active {
        StatusAlignActive::KeepVisible => active_index.saturating_sub(max_tabs.saturating_sub(1)),
        StatusAlignActive::FocusBias => active_index.saturating_sub(max_tabs / 2),
    }
    .min(tabs.len().saturating_sub(max_tabs));
    let end = (start + max_tabs).min(tabs.len());
    (
        tabs[start..end].iter().collect(),
        start,
        tabs.len().saturating_sub(end),
    )
}

struct StatusRenderStyle {
    tab_separator: String,
    module_separator: String,
    active_prefix: &'static str,
    active_suffix: &'static str,
    inactive_prefix: &'static str,
    inactive_suffix: &'static str,
    empty_tabs_label: String,
    overflow_left: &'static str,
    overflow_right: &'static str,
    overflow_count_prefix: &'static str,
    badge_left: &'static str,
    badge_right: &'static str,
    overflow_style: StatusOverflowStyle,
}

impl StatusRenderStyle {
    fn from_config(config: &StatusBarConfig) -> Self {
        let use_ascii = config.style.force_ascii;
        let separators = if use_ascii
            || matches!(
                config.style.separator_set,
                StatusSeparatorSet::Ascii | StatusSeparatorSet::Plain
            ) {
            ("|", "|", "<", ">")
        } else if config.style.prefer_unicode {
            ("", "", "◀", "▶")
        } else {
            ("|", "|", "<", ">")
        };
        let gap = " ".repeat(match config.layout.density {
            StatusDensity::Compact => 0,
            StatusDensity::Cozy => config.layout.tab_gap.max(1),
        });
        let module_gap = " ".repeat(match config.layout.density {
            StatusDensity::Compact => 0,
            StatusDensity::Cozy => config.layout.module_gap.max(1),
        });
        let (
            active_prefix,
            active_suffix,
            inactive_prefix,
            inactive_suffix,
            badge_left,
            badge_right,
        ) = match config.preset {
            StatusBarPreset::TabRail => (" ", " ", " ", " ", " ", " "),
            StatusBarPreset::Minimal => ("", "", "", "", "", ""),
            StatusBarPreset::Classic => ("(", ")", " ", " ", "[", "]"),
        };
        Self {
            tab_separator: if gap.is_empty() {
                separators.0.to_string()
            } else {
                format!("{gap}{}{gap}", separators.0)
            },
            module_separator: if module_gap.is_empty() {
                separators.1.to_string()
            } else {
                format!("{module_gap}{}{module_gap}", separators.1)
            },
            active_prefix,
            active_suffix,
            inactive_prefix,
            inactive_suffix,
            empty_tabs_label: "[no tabs]".to_string(),
            overflow_left: separators.2,
            overflow_right: separators.3,
            overflow_count_prefix: "+",
            badge_left,
            badge_right,
            overflow_style: config.layout.overflow_style,
        }
    }

    fn active_tab(&self, label: &str) -> String {
        format!("{}{}{}", self.active_prefix, label, self.active_suffix)
    }

    fn inactive_tab(&self, label: &str) -> String {
        format!("{}{}{}", self.inactive_prefix, label, self.inactive_suffix)
    }

    fn overflow_marker(&self, hidden: usize) -> String {
        match self.overflow_style {
            StatusOverflowStyle::Count => format!("{}{hidden}", self.overflow_count_prefix),
            StatusOverflowStyle::Arrows => {
                format!("{}{}{}", self.overflow_left, hidden, self.overflow_right)
            }
        }
    }

    fn badge(&self, value: &str) -> String {
        format!("{}{}{}", self.badge_left, value, self.badge_right)
    }
}

fn append_segment(out: &mut String, separator: &str, value: &str) {
    if !out.is_empty() {
        out.push_str(separator);
    }
    out.push_str(value);
}

fn append_right_segment(
    segments: &mut Vec<String>,
    style: &StatusRenderStyle,
    value: String,
    range_out: Option<&mut Option<(usize, usize)>>,
) {
    let start = segments
        .iter()
        .map(String::as_str)
        .map(display_width)
        .sum::<usize>();
    if !segments.is_empty() {
        segments.push(style.module_separator.clone());
    }
    let value_start = start.saturating_add(if segments.len() > 1 {
        display_width(&style.module_separator)
    } else {
        0
    });
    let width = display_width(&value);
    if let Some(range) = range_out
        && width > 0
    {
        *range = Some((value_start, value_start.saturating_add(width - 1)));
    }
    segments.push(value);
}

struct ComposedStatusLine {
    rendered: String,
    right_start_col: Option<usize>,
}

fn compose_status_line(width: u16, left: &str, right: &str) -> ComposedStatusLine {
    let width = usize::from(width);
    if width == 0 {
        return ComposedStatusLine {
            rendered: String::new(),
            right_start_col: None,
        };
    }

    if right.is_empty() {
        return ComposedStatusLine {
            rendered: pad_or_truncate(left, width),
            right_start_col: None,
        };
    }

    let right_width = display_width(right);
    if right_width >= width {
        return ComposedStatusLine {
            rendered: truncate_cells(right, width),
            right_start_col: Some(0),
        };
    }

    let available_left = width.saturating_sub(right_width + 1);
    let left_trimmed = truncate_cells(left, available_left);
    let left_width = display_width(&left_trimmed);
    let spacer = " ".repeat(width.saturating_sub(left_width + right_width));
    ComposedStatusLine {
        rendered: format!("{left_trimmed}{spacer}{right}"),
        right_start_col: Some(width.saturating_sub(right_width)),
    }
}

#[derive(Clone, Copy, PartialEq, Eq)]
enum SegmentKind {
    Base,
    ActiveTab,
    InactiveTab,
    Mode,
    Module,
    Overflow,
}

#[derive(Clone, Copy)]
struct RgbColor {
    r: u8,
    g: u8,
    b: u8,
}

#[derive(Clone, Copy)]
struct SegmentStyle {
    fg: RgbColor,
    bg: RgbColor,
    bold: bool,
    dim: bool,
    underline: bool,
}

struct ResolvedStatusAppearance {
    base: SegmentStyle,
    active_tab: SegmentStyle,
    inactive_tab: SegmentStyle,
    mode: SegmentStyle,
    module: SegmentStyle,
    overflow: SegmentStyle,
}

impl ResolvedStatusAppearance {
    #[allow(clippy::similar_names, clippy::too_many_lines)] // bg/fg pairs are intentionally parallel names
    fn resolve(config: &StatusBarConfig, runtime_appearance: &RuntimeAppearance) -> Self {
        let fallback_bar_bg =
            parse_hex_color(&runtime_appearance.status.background).unwrap_or(RgbColor {
                r: 30,
                g: 30,
                b: 30,
            });
        let fallback_bar_fg =
            parse_hex_color(&runtime_appearance.status.foreground).unwrap_or(RgbColor {
                r: 220,
                g: 220,
                b: 220,
            });
        let fallback_active_bg = parse_hex_color(&runtime_appearance.status.active_window)
            .unwrap_or(RgbColor {
                r: 110,
                g: 170,
                b: 240,
            });
        let fallback_active_fg =
            parse_hex_color(&runtime_appearance.background).unwrap_or(RgbColor {
                r: 20,
                g: 20,
                b: 20,
            });
        let fallback_mode_bg = parse_hex_color(&runtime_appearance.status.mode_indicator)
            .unwrap_or(fallback_active_bg);

        let bar_bg = config
            .colors
            .bar_bg
            .as_deref()
            .and_then(parse_hex_color)
            .unwrap_or(fallback_bar_bg);
        let bar_fg = config
            .colors
            .bar_fg
            .as_deref()
            .and_then(parse_hex_color)
            .unwrap_or(fallback_bar_fg);
        let active_bg = config
            .colors
            .tab_active_bg
            .as_deref()
            .and_then(parse_hex_color)
            .unwrap_or(fallback_active_bg);
        let active_fg = config
            .colors
            .tab_active_fg
            .as_deref()
            .and_then(parse_hex_color)
            .unwrap_or(fallback_active_fg);
        let inactive_bg = config
            .colors
            .tab_inactive_bg
            .as_deref()
            .and_then(parse_hex_color)
            .unwrap_or_else(|| adjust_rgb(bar_bg, 18));
        let inactive_fg = config
            .colors
            .tab_inactive_fg
            .as_deref()
            .and_then(parse_hex_color)
            .unwrap_or(bar_fg);
        let module_bg = config
            .colors
            .module_bg
            .as_deref()
            .and_then(parse_hex_color)
            .unwrap_or_else(|| adjust_rgb(bar_bg, 10));
        let module_fg = config
            .colors
            .module_fg
            .as_deref()
            .and_then(parse_hex_color)
            .unwrap_or(bar_fg);
        let overflow_bg = config
            .colors
            .overflow_bg
            .as_deref()
            .and_then(parse_hex_color)
            .unwrap_or_else(|| adjust_rgb(bar_bg, 26));
        let overflow_fg = config
            .colors
            .overflow_fg
            .as_deref()
            .and_then(parse_hex_color)
            .unwrap_or(bar_fg);

        Self {
            base: SegmentStyle {
                fg: bar_fg,
                bg: bar_bg,
                bold: false,
                dim: false,
                underline: false,
            },
            active_tab: SegmentStyle {
                fg: active_fg,
                bg: active_bg,
                bold: config.style.bold_active,
                dim: false,
                underline: config.style.underline_active,
            },
            inactive_tab: SegmentStyle {
                fg: inactive_fg,
                bg: inactive_bg,
                bold: false,
                dim: config.style.dim_inactive,
                underline: false,
            },
            mode: SegmentStyle {
                fg: fallback_active_fg,
                bg: fallback_mode_bg,
                bold: true,
                dim: false,
                underline: false,
            },
            module: SegmentStyle {
                fg: module_fg,
                bg: module_bg,
                bold: false,
                dim: false,
                underline: false,
            },
            overflow: SegmentStyle {
                fg: overflow_fg,
                bg: overflow_bg,
                bold: false,
                dim: false,
                underline: false,
            },
        }
    }
}

#[allow(clippy::too_many_arguments)]
fn stylize_status_line(
    rendered_plain: &str,
    width: u16,
    config: &StatusBarConfig,
    appearance: &ResolvedStatusAppearance,
    tabs: &[AttachTab],
    hitboxes: &[AttachStatusTabHitbox],
    overflow_ranges: &[(usize, usize)],
    mode_range: Option<(usize, usize)>,
    right_start_col: Option<usize>,
) -> String {
    let width = usize::from(width);
    if width == 0 {
        return String::new();
    }
    let segments = status_segments(
        width,
        tabs,
        hitboxes,
        overflow_ranges,
        mode_range,
        right_start_col,
    );

    let mut rendered = String::new();
    let mut current_style = None;
    let mut col = 0usize;
    for ch in rendered_plain.chars() {
        let char_width = UnicodeWidthChar::width(ch).unwrap_or(0);
        if char_width == 0 {
            rendered.push(ch);
            continue;
        }
        if col >= width {
            break;
        }
        let style = style_for_segment(segments[col], config, appearance);
        if current_style != Some(segments[col]) {
            rendered.push_str(&style_sgr(style));
            current_style = Some(segments[col]);
        }
        rendered.push(ch);
        col = col.saturating_add(char_width);
    }
    rendered.push_str("\x1b[0m");
    rendered
}

#[allow(clippy::too_many_arguments)]
fn status_line_spans(
    rendered_plain: &str,
    width: u16,
    config: &StatusBarConfig,
    appearance: &ResolvedStatusAppearance,
    tabs: &[AttachTab],
    hitboxes: &[AttachStatusTabHitbox],
    overflow_ranges: &[(usize, usize)],
    mode_range: Option<(usize, usize)>,
    right_start_col: Option<usize>,
) -> Vec<RenderTextSpan> {
    let width = usize::from(width);
    if width == 0 {
        return Vec::new();
    }
    let segments = status_segments(
        width,
        tabs,
        hitboxes,
        overflow_ranges,
        mode_range,
        right_start_col,
    );
    let mut spans = Vec::new();
    let mut current_kind = None;
    let mut current_text = String::new();
    let mut col = 0usize;
    for ch in rendered_plain.chars() {
        let char_width = UnicodeWidthChar::width(ch).unwrap_or(0);
        if char_width == 0 {
            current_text.push(ch);
            continue;
        }
        if col >= width {
            break;
        }
        let kind = segments[col];
        if let Some(previous_kind) = current_kind
            && previous_kind != kind
            && !current_text.is_empty()
        {
            let style = style_for_segment(previous_kind, config, appearance);
            spans.push(RenderTextSpan::new(
                std::mem::take(&mut current_text),
                render_style_from_status_segment(style),
            ));
        }
        current_kind = Some(kind);
        current_text.push(ch);
        col = col.saturating_add(char_width);
    }
    if let Some(kind) = current_kind
        && !current_text.is_empty()
    {
        let style = style_for_segment(kind, config, appearance);
        spans.push(RenderTextSpan::new(
            current_text,
            render_style_from_status_segment(style),
        ));
    }
    spans
}

fn status_segments(
    width: usize,
    tabs: &[AttachTab],
    hitboxes: &[AttachStatusTabHitbox],
    overflow_ranges: &[(usize, usize)],
    mode_range: Option<(usize, usize)>,
    right_start_col: Option<usize>,
) -> Vec<SegmentKind> {
    let mut segments = vec![SegmentKind::Base; width];
    if let Some(start) = right_start_col {
        for segment in &mut segments[start.min(width)..width] {
            *segment = SegmentKind::Module;
        }
    }

    for (start, end) in overflow_ranges {
        if *start >= width {
            continue;
        }
        for segment in &mut segments[*start..=(*end).min(width.saturating_sub(1))] {
            *segment = SegmentKind::Overflow;
        }
    }

    if let Some((start, end)) = mode_range
        && start < width
    {
        for segment in &mut segments[start..=end.min(width.saturating_sub(1))] {
            *segment = SegmentKind::Mode;
        }
    }

    for hitbox in hitboxes {
        let kind = tabs
            .iter()
            .find(|tab| tab.context_id == Some(hitbox.context_id))
            .map_or(SegmentKind::InactiveTab, |tab| {
                if tab.active {
                    SegmentKind::ActiveTab
                } else {
                    SegmentKind::InactiveTab
                }
            });
        let start = usize::from(hitbox.start_col).min(width.saturating_sub(1));
        let end = usize::from(hitbox.end_col).min(width.saturating_sub(1));
        for segment in &mut segments[start..=end] {
            *segment = kind;
        }
    }
    segments
}

const fn render_style_from_status_segment(style: SegmentStyle) -> RenderStyle {
    RenderStyle {
        fg: Some(RenderColor::Rgb {
            r: style.fg.r,
            g: style.fg.g,
            b: style.fg.b,
        }),
        bg: Some(RenderColor::Rgb {
            r: style.bg.r,
            g: style.bg.g,
            b: style.bg.b,
        }),
        bold: style.bold,
        underline: style.underline,
        italic: false,
        reverse: false,
        dim: style.dim,
        blink: false,
        strikethrough: false,
    }
}

const fn style_for_segment(
    segment: SegmentKind,
    _config: &StatusBarConfig,
    appearance: &ResolvedStatusAppearance,
) -> SegmentStyle {
    match segment {
        SegmentKind::Base => appearance.base,
        SegmentKind::ActiveTab => appearance.active_tab,
        SegmentKind::InactiveTab => appearance.inactive_tab,
        SegmentKind::Mode => appearance.mode,
        SegmentKind::Module => appearance.module,
        SegmentKind::Overflow => appearance.overflow,
    }
}

fn style_sgr(style: SegmentStyle) -> String {
    let mut parts = vec!["0".to_string()];
    if style.bold {
        parts.push("1".to_string());
    }
    if style.dim {
        parts.push("2".to_string());
    }
    if style.underline {
        parts.push("4".to_string());
    }
    parts.push(format!("38;2;{};{};{}", style.fg.r, style.fg.g, style.fg.b));
    parts.push(format!("48;2;{};{};{}", style.bg.r, style.bg.g, style.bg.b));
    format!("\x1b[{}m", parts.join(";"))
}

fn parse_hex_color(value: &str) -> Option<RgbColor> {
    let hex = value.strip_prefix('#').unwrap_or(value);
    if hex.len() != 6 {
        return None;
    }
    let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
    let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
    let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
    Some(RgbColor { r, g, b })
}

fn adjust_rgb(value: RgbColor, delta: i16) -> RgbColor {
    let adjust = |channel: u8| -> u8 {
        u8::try_from((i16::from(channel) + delta).clamp(0, 255)).unwrap_or(0)
    };
    RgbColor {
        r: adjust(value.r),
        g: adjust(value.g),
        b: adjust(value.b),
    }
}

fn pad_or_truncate(value: &str, width: usize) -> String {
    let mut rendered = truncate_cells(value, width);
    let current = display_width(&rendered);
    if current < width {
        rendered.push_str(&" ".repeat(width - current));
    }
    rendered
}

fn display_width(value: &str) -> usize {
    UnicodeWidthStr::width(value)
}

fn truncate_cells(value: &str, max_width: usize) -> String {
    if max_width == 0 {
        return String::new();
    }
    let mut width = 0usize;
    let mut out = String::new();
    for ch in value.chars() {
        let char_width = UnicodeWidthChar::width(ch).unwrap_or(0);
        if width.saturating_add(char_width) > max_width {
            break;
        }
        out.push(ch);
        width = width.saturating_add(char_width);
    }
    out
}

fn clamp_hitboxes_to_width(hitboxes: &mut Vec<AttachStatusTabHitbox>, width: u16) {
    if width == 0 {
        hitboxes.clear();
        return;
    }
    let max = width - 1;
    hitboxes.retain_mut(|entry| {
        if entry.start_col > max {
            return false;
        }
        entry.end_col = entry.end_col.min(max);
        entry.start_col <= entry.end_col
    });
}

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

    #[test]
    fn mode_badge_uses_mode_indicator_color() {
        let appearance = RuntimeAppearance {
            background: "#010203".to_string(),
            status: bmux_appearance::RuntimeStatusAppearance {
                mode_indicator: "#112233".to_string(),
                ..Default::default()
            },
            ..Default::default()
        };

        let status = build_attach_status_line(
            80,
            &StatusBarConfig::default(),
            &appearance,
            "session",
            1,
            "context",
            &[],
            None,
            "NORMAL",
            "write",
            None,
            "",
        );

        assert!(status.rendered.contains("NORMAL"));
        assert!(
            status
                .rendered
                .contains("\x1b[0;1;38;2;1;2;3;48;2;17;34;51m")
        );
        let mode_span = status
            .spans
            .iter()
            .find(|span| span.text.contains("NORMAL"))
            .expect("mode span should be declarative");
        assert!(mode_span.style.bold);
        assert_eq!(
            mode_span.style.bg,
            Some(RenderColor::Rgb {
                r: 17,
                g: 34,
                b: 51,
            })
        );
    }

    #[test]
    fn disabled_status_line_has_no_declarative_spans() {
        let status = build_attach_status_line(
            80,
            &StatusBarConfig {
                enabled: false,
                ..StatusBarConfig::default()
            },
            &RuntimeAppearance::default(),
            "session",
            1,
            "context",
            &[],
            None,
            "NORMAL",
            "write",
            None,
            "",
        );

        assert!(status.rendered.is_empty());
        assert!(status.spans.is_empty());
    }
}
