use crate::app::{App, ViewMode};
use crate::app_theme::AppThemeColors;
use crate::config::{IconMode, StatuslineConfig};
use crate::storage::NoteSummary;
use crate::ui::{PreviewHeaderInfo, format_date, format_relative_time, format_size};
use ratatui::prelude::*;
use std::borrow::Cow;
use std::path::Path;
use unicode_width::UnicodeWidthChar;

#[derive(Debug, Clone)]
pub enum Segment<'a> {
    Text(String),
    Composite(Vec<Span<'a>>),
    CompositeSplittable(Vec<Span<'a>>),
}

enum FlatSegment<'a> {
    Cell(String),
    Composite(Vec<Span<'a>>),
    Splittable(Vec<Span<'a>>),
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ListHeaderField {
    Tags,
    Sort,
    Age,
    Size,
    Count,
}

#[derive(Clone, Default)]
pub(crate) struct ListHeaderDetail {
    pub(crate) groups: Vec<(ListHeaderField, Vec<Span<'static>>)>,
}

impl ListHeaderDetail {
    pub(crate) fn new(groups: Vec<(ListHeaderField, Vec<Span<'static>>)>) -> Self {
        Self { groups }
    }

    pub(crate) fn spans_without(&self, hidden: &[ListHeaderField]) -> Vec<Span<'static>> {
        let mut spans = Vec::new();
        for (_, group) in self
            .groups
            .iter()
            .filter(|(field, _)| !hidden.contains(field))
        {
            if !spans.is_empty() {
                spans.push(Span::raw(" | "));
            }
            spans.extend(group.iter().cloned());
        }
        spans
    }
}

pub(crate) fn list_relative_age_at(updated_at: u64, now: u64) -> String {
    let elapsed = now.saturating_sub(updated_at);
    if elapsed < 60 {
        "just now".into()
    } else if elapsed < 60 * 60 {
        format!("{}m ago", elapsed / 60)
    } else if elapsed < 24 * 60 * 60 {
        format!("{}h ago", elapsed / (60 * 60))
    } else if elapsed < 7 * 24 * 60 * 60 {
        format!("{}d ago", elapsed / (24 * 60 * 60))
    } else if elapsed < 52 * 7 * 24 * 60 * 60 {
        format!("{}w ago", elapsed / (7 * 24 * 60 * 60))
    } else {
        format!("{}y ago", elapsed / (52 * 7 * 24 * 60 * 60))
    }
}

pub(crate) fn list_relative_age(updated_at: u64) -> String {
    list_relative_age_at(updated_at, crate::ui::now_unix_secs())
}

pub(crate) fn compact_list_tags(tags: &[String]) -> String {
    fn truncate(tag: &str) -> String {
        if tag.chars().map(|ch| ch.width().unwrap_or(0)).sum::<usize>() <= 12 {
            return tag.into();
        }
        let mut out = String::new();
        let mut width = 0;
        for ch in tag.chars() {
            let char_width = ch.width().unwrap_or(0);
            if width + char_width > 11 {
                break;
            }
            out.push(ch);
            width += char_width;
        }
        out.push('…');
        out
    }

    let mut out = tags
        .iter()
        .take(2)
        .map(|tag| truncate(tag))
        .collect::<Vec<_>>()
        .join(", ");
    if tags.len() > 2 {
        out.push_str(&format!(" +{}", tags.len() - 2));
    }
    out
}

#[derive(Default)]
pub(crate) struct EditMemo {
    pub(crate) counts: Option<(usize, usize)>,
    pub(crate) content: Option<String>,
}
impl EditMemo {
    pub(crate) fn counts(&mut self, app: &crate::app::App) -> (usize, usize) {
        *self.counts.get_or_insert_with(|| {
            let lines = app.editor.body.lines();
            let words: usize = lines.iter().map(|l| crate::goals::count_words(l)).sum();
            let chars: usize = lines.iter().map(|l| l.chars().count()).sum::<usize>()
                + lines.len().saturating_sub(1);
            (words, chars)
        })
    }
    pub(crate) fn content(&mut self, app: &crate::app::App) -> &str {
        self.content
            .get_or_insert_with(|| app.editor.body.lines().join("\n"))
    }
}
pub struct StatuslineContext<'a> {
    pub config: &'a crate::config::ClinConfig,
    pub view: ViewMode,
    pub area: Option<Rect>,
    pub app_status: Option<&'a str>,
    pub vault_path: Option<&'a Path>,
    pub date_format: Option<&'a str>,
    pub app: Option<&'a App>,

    edit_memo: std::cell::RefCell<EditMemo>,

    // active overlay states:
    pub graph: Option<&'a crate::graf::graph::GraphState>,
    pub draw: Option<&'a crate::draw::app::DrawAppState>,
    pub backup: Option<&'a crate::backup::state::BackupState>,
    pub outline: Option<&'a crate::outline::state::OutlineState>,
    pub canvas: Option<&'a crate::pinstar::state::PinstarState>,
    pub setup: Option<&'a crate::setup::SetupState>,

    // selected/edited note summary:
    pub note: Option<&'a NoteSummary>,
    pub preview_info: Option<&'a PreviewHeaderInfo>,

    // pre-built composite spans:
    pub hints: Option<Vec<Span<'a>>>,
    pub badge: Option<Vec<Span<'a>>>,
    pub pending: Option<Vec<Span<'a>>>,
    pub preview: Option<Vec<Span<'a>>>,
    pub detail: Option<Vec<Span<'a>>>,
    pub(crate) list_detail: Option<ListHeaderDetail>,

    pub graph_fps: Option<f64>,
    pub graph_grid_visible: bool,
}

impl<'a> StatuslineContext<'a> {
    pub fn for_view(app: &'a App, view: ViewMode) -> Self {
        Self {
            config: &app.config,
            view,
            area: None,
            app_status: Some(app.status.as_ref()),
            vault_path: Some(&app.storage.data_dir),
            date_format: Some(&app.date_format),
            app: Some(app),
            graph: None,
            draw: None,
            backup: None,
            outline: None,
            canvas: None,
            setup: None,
            note: None,
            preview_info: None,
            hints: None,
            badge: None,
            pending: None,
            preview: None,
            detail: None,
            list_detail: None,
            graph_fps: None,
            graph_grid_visible: true,
            edit_memo: std::cell::RefCell::new(EditMemo::default()),
        }
    }

    pub fn for_overlay(config: &'a crate::config::ClinConfig, view: ViewMode) -> Self {
        Self {
            config,
            view,
            area: None,
            app_status: None,
            vault_path: None,
            date_format: None,
            app: None,
            graph: None,
            draw: None,
            backup: None,
            outline: None,
            canvas: None,
            setup: None,
            note: None,
            preview_info: None,
            hints: None,
            badge: None,
            pending: None,
            preview: None,
            detail: None,
            list_detail: None,
            graph_fps: None,
            graph_grid_visible: true,
            edit_memo: std::cell::RefCell::new(EditMemo::default()),
        }
    }
}

pub fn active_note(app: &App, view: ViewMode) -> Option<&NoteSummary> {
    match view {
        ViewMode::List => {
            if let Some(crate::list_view::VisualItem::Note { summary_idx, .. }) =
                app.list.visual_list.get(app.list.visual_index)
            {
                app.notes.get(*summary_idx)
            } else {
                None
            }
        }
        ViewMode::Edit => {
            if let Some(id) = &app.editor.editing_id {
                app.notes.iter().find(|n| &n.id == id)
            } else {
                None
            }
        }
        _ => None,
    }
}

impl StatuslineContext<'_> {
    pub fn resolve(&self, name: &str) -> Option<Cow<'static, str>> {
        match name {
            // Global / App
            "view" => {
                let s = match self.view {
                    ViewMode::List => "Notes",
                    ViewMode::Edit => "Editor",
                    ViewMode::Help => "Help",
                    ViewMode::Graph => "Graph",
                    ViewMode::Draw => "Draw",
                    ViewMode::Canvas => "Canvas",
                    ViewMode::Backup => "Backup",
                    ViewMode::Outline => "Outline",
                    ViewMode::Setup => "Setup",
                };
                Some(s.into())
            }
            "status" => {
                let st = if let Some(app_st) = self.app_status {
                    if app_st == "Ready" || app_st.is_empty() {
                        "".into()
                    } else {
                        app_st.to_string().into()
                    }
                } else if let Some(app) = self.app {
                    if app.status == "Ready" || app.status.is_empty() {
                        "".into()
                    } else {
                        app.status.clone()
                    }
                } else {
                    "".into()
                };
                Some(st)
            }
            "vault" => {
                let name = if let Some(p) = self.vault_path {
                    p.file_name()
                        .and_then(|n| n.to_str())
                        .unwrap_or("")
                        .to_string()
                } else if let Some(app) = self.app {
                    app.storage
                        .data_dir
                        .file_name()
                        .and_then(|n| n.to_str())
                        .unwrap_or("")
                        .to_string()
                } else {
                    "".to_string()
                };
                Some(name.into())
            }
            "vault_path" => {
                let path = if let Some(p) = self.vault_path {
                    p.to_string_lossy().into_owned()
                } else if let Some(app) = self.app {
                    app.storage.data_dir.to_string_lossy().into_owned()
                } else {
                    "".to_string()
                };
                Some(path.into())
            }
            "version" => Some(format!("clin v{}", env!("CARGO_PKG_VERSION")).into()),

            // Date / Time
            "time" | "date" | "datetime" | "weekday" | "year" | "month" | "day" | "hour"
            | "minute" | "second" => {
                let now = chrono::Local::now();
                let fmt = self
                    .date_format
                    .or_else(|| self.app.map(|a| a.date_format.as_str()))
                    .unwrap_or("%Y-%m-%d");
                let val = match name {
                    "time" => now.format("%H:%M").to_string(),
                    "date" => now.format(fmt).to_string(),
                    "datetime" => format!("{} {}", now.format(fmt), now.format("%H:%M")),
                    "weekday" => now.format("%A").to_string(),
                    "year" => now.format("%Y").to_string(),
                    "month" => now.format("%m").to_string(),
                    "day" => now.format("%d").to_string(),
                    "hour" => now.format("%H").to_string(),
                    "minute" => now.format("%M").to_string(),
                    "second" => now.format("%S").to_string(),
                    _ => unreachable!(),
                };
                Some(val.into())
            }

            // Config Echo
            "theme" => Some(self.config.ui.theme.clone().into()),
            "preset" => {
                let p = match self.config.core.keybind_preset {
                    crate::config::KeybindPreset::Default => "default",
                    crate::config::KeybindPreset::Helix => "helix",
                    crate::config::KeybindPreset::Vim => "vim",
                    crate::config::KeybindPreset::Emacs => "emacs",
                };
                Some(p.into())
            }
            "icon_mode" => {
                let im = match self.config.ui.icon_mode {
                    crate::config::IconMode::Nerd => "nerd",
                    crate::config::IconMode::Unicode => "unicode",
                    crate::config::IconMode::None => "none",
                };
                Some(im.into())
            }
            "hint_bar_style" => Some(self.config.ui.hint_bar_style.as_config_str().into()),
            "background" => {
                let bg = match self.config.ui.background {
                    crate::config::Background::Transparent => "transparent",
                    crate::config::Background::Solid => "solid",
                };
                Some(bg.into())
            }

            "help_page" => Some(
                self.app
                    .map_or_else(|| "?".into(), |a| format!("{}", a.help_page + 1).into()),
            ),
            "help_total_pages" => Some(self.app.map_or_else(
                || "?".into(),
                |a| {
                    let rows = a.list.help_text_cache.as_ref().map_or(0, |r| r.len());
                    let ps = a.help_page_size.max(1) as usize;
                    format!("{}", rows.div_ceil(ps)).into()
                },
            )),

            // Goals
            "goal_words" => Some(
                self.app
                    .map(|a| a.goals_progress.words_written.to_string())
                    .unwrap_or_else(|| "0".to_string())
                    .into(),
            ),
            "goal_target" => Some(self.config.goals.word_goal.to_string().into()),
            "goal_notes" => Some(
                self.app
                    .map(|a| a.goals_progress.notes_modified.len().to_string())
                    .unwrap_or_else(|| "0".to_string())
                    .into(),
            ),
            "goal_note_target" => Some(self.config.goals.note_goal.to_string().into()),
            "goal_date" => Some(
                self.app
                    .map(|a| a.goals_progress.date.clone())
                    .unwrap_or_default()
                    .into(),
            ),

            // List View
            "title" => {
                let title = match self.view {
                    ViewMode::List => self.app.map_or_else(
                        || "Notes".to_string(),
                        |app| {
                            if app.layout_edit {
                                "Notes - Editing Layout".to_string()
                            } else {
                                "Notes".to_string()
                            }
                        },
                    ),
                    ViewMode::Edit => "EDITOR".to_string(),
                    ViewMode::Help => "Help".to_string(),
                    ViewMode::Graph => "Graph".to_string(),
                    ViewMode::Draw => "Draw".to_string(),
                    ViewMode::Canvas => "Canvas".to_string(),
                    ViewMode::Backup => "Backup".to_string(),
                    ViewMode::Outline => self.outline.map_or_else(
                        || "Outline".to_string(),
                        |tree| format!("OUTLINE — {}", tree.note_title),
                    ),
                    ViewMode::Setup => "Setup".to_string(),
                };
                Some(title.into())
            }
            "sort" => {
                let sort = self
                    .app
                    .map(|app| {
                        let ascending =
                            matches!(app.list.sort_order, crate::list_view::SortOrder::Ascending);
                        if self.view == ViewMode::List {
                            match app.list.sort_field {
                                crate::list_view::SortField::Title => {
                                    if ascending {
                                        "A-z".into()
                                    } else {
                                        "Z-a".into()
                                    }
                                }
                                crate::list_view::SortField::Modified => {
                                    let prefix = if app.config.ui.icon_mode == IconMode::None {
                                        "M"
                                    } else {
                                        crate::ui::get_icon(
                                            "\u{f03eb}",
                                            "\u{270e}",
                                            app.config.ui.icon_mode,
                                        )
                                    };
                                    let arrow = if ascending { "\u{25b2}" } else { "\u{25bc}" };
                                    format!("{prefix}{arrow}")
                                }
                            }
                        } else {
                            let field = match app.list.sort_field {
                                crate::list_view::SortField::Title => "Title",
                                crate::list_view::SortField::Modified => "Modified",
                            };
                            let arrow = if ascending { "\u{25b2}" } else { "\u{25bc}" };
                            format!("{field} {arrow}")
                        }
                    })
                    .unwrap_or_default();
                Some(sort.into())
            }
            "layout" => {
                let layout = if let Some(app) = self.app {
                    &app.list.notes_layout
                } else {
                    &self.config.list.default_view
                };
                let l = match layout {
                    crate::config::NotesLayout::Tree => "tree",
                    crate::config::NotesLayout::Grid => "grid",
                };
                Some(l.into())
            }
            "density" => {
                let density = if let Some(app) = self.app {
                    &app.list.list_density
                } else {
                    &self.config.list.density
                };
                let d = match density {
                    crate::config::ListDensity::Compact => "compact",
                    crate::config::ListDensity::Comfortable => "comfortable",
                };
                Some(d.into())
            }
            "section" => {
                let folder = self.app.map(|a| a.list.grid_folder.as_str()).unwrap_or("");
                let s = if folder == crate::app::VIRTUAL_PINNED_PATH {
                    "pinned"
                } else if folder.starts_with(crate::app::VIRTUAL_SMART_PATH) {
                    "smart"
                } else {
                    "vault"
                };
                Some(s.into())
            }
            "folder" => Some(
                self.app
                    .map(|a| a.list.grid_folder.clone())
                    .unwrap_or_default()
                    .into(),
            ),
            "folder_count" => {
                let count = self.app.map(|a| a.catalog_folders.len()).unwrap_or(0);
                Some(count.to_string().into())
            }
            "tag_count" => {
                let count = self.app.map(|a| a.collect_live_tags().len()).unwrap_or(0);
                Some(count.to_string().into())
            }
            "note_count" => Some(
                self.app
                    .map(|a| a.notes.len())
                    .unwrap_or(0)
                    .to_string()
                    .into(),
            ),
            "visual_index" => Some(
                self.app
                    .map(|a| a.list.visual_index + 1)
                    .unwrap_or(0)
                    .to_string()
                    .into(),
            ),
            "visual_total" => Some(
                self.app
                    .map(|a| a.list.visual_list.len())
                    .unwrap_or(0)
                    .to_string()
                    .into(),
            ),
            "selected_count" => Some(
                self.app
                    .map(|a| a.list.selected_indices.len())
                    .unwrap_or(0)
                    .to_string()
                    .into(),
            ),
            "select_mode" => {
                let on = self
                    .app
                    .map(|a| a.list.list_mode != crate::list_view::ListMode::Normal)
                    .unwrap_or(false);
                Some((if on { "on" } else { "off" }).into())
            }
            "tag_to_assign" => Some("".into()),
            "search" => {
                let q = self
                    .app
                    .map(|a| {
                        if let Some(crate::popups::ActivePopup::Search(popup)) = &a.popups.active {
                            popup.input.lines().join("")
                        } else {
                            "".to_string()
                        }
                    })
                    .unwrap_or_default();
                Some(q.into())
            }
            "grep" => {
                let q = self
                    .app
                    .map(|a| {
                        if let Some(crate::popups::ActivePopup::Search(popup)) = &a.popups.active {
                            let parsed =
                                crate::app::parse_search_query(&popup.input.lines().join(""));
                            if parsed.grep_mode { "on" } else { "off" }
                        } else {
                            "off"
                        }
                    })
                    .unwrap_or("off");
                Some(q.into())
            }
            "tag_filter" => {
                let t = self
                    .app
                    .map(|a| {
                        if let Some(crate::popups::ActivePopup::Search(popup)) = &a.popups.active {
                            let parsed =
                                crate::app::parse_search_query(&popup.input.lines().join(""));
                            parsed
                                .tag_filter
                                .as_ref()
                                .map(|tags| tags.join(", "))
                                .unwrap_or_default()
                        } else {
                            "".to_string()
                        }
                    })
                    .unwrap_or_default();
                Some(t.into())
            }
            "folder_filter" => {
                let f = self
                    .app
                    .map(|a| {
                        if let Some(crate::popups::ActivePopup::Search(popup)) = &a.popups.active {
                            let parsed =
                                crate::app::parse_search_query(&popup.input.lines().join(""));
                            parsed.folder_filter.clone().unwrap_or_default()
                        } else {
                            "".to_string()
                        }
                    })
                    .unwrap_or_default();
                Some(f.into())
            }
            "pinned_count" => {
                let count = self
                    .app
                    .map(|a| a.notes.iter().filter(|n| n.pinned).count())
                    .unwrap_or(0);
                Some(count.to_string().into())
            }
            "pinned_on_top" => Some(
                (if self.app.map(|a| a.pinned_on_top).unwrap_or(false) {
                    "on"
                } else {
                    "off"
                })
                .into(),
            ),
            "folders_first" => Some(
                (if self.app.map(|a| a.list.folders_first).unwrap_or(false) {
                    "on"
                } else {
                    "off"
                })
                .into(),
            ),
            "list_preview" => Some(
                (if self.app.map(|a| a.list.preview_enabled).unwrap_or(false) {
                    "on"
                } else {
                    "off"
                })
                .into(),
            ),
            "calendar" => Some(
                (if self.app.map(|a| a.list.calendar_enabled).unwrap_or(false) {
                    "on"
                } else {
                    "off"
                })
                .into(),
            ),
            "layout_edit" => Some(
                (if self.app.map(|a| a.layout_edit).unwrap_or(false) {
                    "on"
                } else {
                    "off"
                })
                .into(),
            ),

            // Note (List + Edit)
            "note_title" => Some(
                self.note
                    .map(|n| n.title.as_str())
                    .unwrap_or("")
                    .to_string()
                    .into(),
            ),
            "note_id" => Some(
                self.note
                    .map(|n| n.id.as_str())
                    .unwrap_or("")
                    .to_string()
                    .into(),
            ),
            "note_folder" => Some(
                self.note
                    .map(|n| n.folder.as_str())
                    .unwrap_or("")
                    .to_string()
                    .into(),
            ),
            "note_format" => {
                let ext = self
                    .note
                    .and_then(|n| {
                        Path::new(&n.id)
                            .extension()
                            .and_then(|e| e.to_str())
                            .map(|s| s.to_string())
                    })
                    .unwrap_or_default();
                Some(ext.into())
            }
            "note_size" => {
                let sz = self
                    .note
                    .map(|n| format_size(n.size_bytes))
                    .unwrap_or_default();
                Some(sz.into())
            }
            "note_links" => {
                let count = self.note.map(|note| note.links.len()).unwrap_or(0);
                Some(count.to_string().into())
            }
            "tags" => Some(
                self.note
                    .map(|note| {
                        if self.view == ViewMode::List {
                            compact_list_tags(&note.tags)
                        } else {
                            note.tags.join(", ")
                        }
                    })
                    .unwrap_or_default()
                    .into(),
            ),
            "has_tags" => {
                let has = self.note.map(|n| !n.tags.is_empty()).unwrap_or(false);
                Some((if has { "on" } else { "off" }).into())
            }
            "note_pinned" => {
                let pinned = self.note.map(|n| n.pinned).unwrap_or(false);
                Some((if pinned { "on" } else { "off" }).into())
            }
            "note_updated" => {
                let formatted = self
                    .note
                    .map(|note| {
                        let format = self
                            .date_format
                            .or_else(|| self.app.map(|app| app.date_format.as_str()))
                            .unwrap_or("%Y-%m-%d");
                        format_date(note.updated_at, format)
                    })
                    .unwrap_or_default();
                Some(formatted.into())
            }
            "note_updated_rel" => {
                let relative = self
                    .note
                    .map(|note| {
                        if self.view == ViewMode::List {
                            list_relative_age(note.updated_at)
                        } else {
                            crate::ui::format_relative_time(note.updated_at).into_owned()
                        }
                    })
                    .unwrap_or_default();
                Some(relative.into())
            }
            "prev_note" => Some(
                self.preview_info
                    .and_then(|i| i.prev_name.clone())
                    .unwrap_or_default()
                    .into(),
            ),
            "next_note" => Some(
                self.preview_info
                    .and_then(|i| i.next_name.clone())
                    .unwrap_or_default()
                    .into(),
            ),

            // Edit view
            "word_count" | "line_count" | "char_count" | "cursor_line" | "cursor_col"
            | "modified" | "reading_time" | "header_count" | "task_count" | "has_tasks"
            | "has_frontmatter" | "words_added" | "editing_id" | "editing_template"
            | "line_numbers" | "editor_preview" | "ext_editor" | "ext_editor_enabled" => {
                let app = match self.app {
                    Some(a) => a,
                    None => return Some("".into()),
                };
                if self.view != ViewMode::Edit {
                    return Some("".into());
                }

                let title = self
                    .note
                    .map(|n| n.title.as_str())
                    .unwrap_or("Untitled note");

                let val = match name {
                    "word_count" => self.edit_memo.borrow_mut().counts(app).0.to_string(),
                    "line_count" => app.editor.body.lines().len().to_string(),
                    "char_count" => self.edit_memo.borrow_mut().counts(app).1.to_string(),
                    "cursor_line" => (app.editor.body.cursor().row + 1).to_string(),
                    "cursor_col" => (app.editor.body.cursor().col + 1).to_string(),
                    "modified" => {
                        let now = std::time::Instant::now();
                        let mut cache = app.editor.modified_status_cache.borrow_mut();
                        if cache
                            .map(|(t, _)| {
                                now.duration_since(t) >= std::time::Duration::from_millis(500)
                            })
                            .unwrap_or(true)
                        {
                            let is_mod = if let Some(id) = &app.editor.editing_id {
                                if let Ok(note) = app.storage.load_note(id) {
                                    let current_title =
                                        crate::events::get_title_text(&app.editor.title_editor);
                                    let mut memo = self.edit_memo.borrow_mut();
                                    let content = memo.content(app);
                                    current_title != note.title || content != note.content
                                } else {
                                    false
                                }
                            } else if let Some(path) = &app.editor.template_edit_path {
                                if let Ok(orig_content) = std::fs::read_to_string(path) {
                                    let mut memo = self.edit_memo.borrow_mut();
                                    let content = memo.content(app);
                                    content != orig_content
                                } else {
                                    false
                                }
                            } else {
                                false
                            };
                            *cache = Some((now, is_mod));
                        }
                        (if cache.unwrap().1 { "on" } else { "off" }).to_string()
                    }
                    "reading_time" => {
                        let wc = self.edit_memo.borrow_mut().counts(app).0;
                        ((wc as f64 / 200.0).ceil() as usize).to_string()
                    }
                    "header_count" => {
                        let mut memo = self.edit_memo.borrow_mut();
                        let content = memo.content(app);
                        crate::outline::parse::parse_outline(title, content)
                            .len()
                            .saturating_sub(1)
                            .to_string()
                    }
                    "task_count" => {
                        let mut memo = self.edit_memo.borrow_mut();
                        let content = memo.content(app);
                        let count = content
                            .lines()
                            .filter(|l| {
                                let t = l.trim_start();
                                t.starts_with("- [ ] ")
                                    || t.starts_with("- [x] ")
                                    || t.starts_with("* [ ] ")
                                    || t.starts_with("* [x] ")
                            })
                            .count();
                        count.to_string()
                    }
                    "has_tasks" => {
                        let mut memo = self.edit_memo.borrow_mut();
                        let content = memo.content(app);
                        let count = content
                            .lines()
                            .filter(|l| {
                                let t = l.trim_start();
                                t.starts_with("- [ ] ")
                                    || t.starts_with("- [x] ")
                                    || t.starts_with("* [ ] ")
                                    || t.starts_with("* [x] ")
                            })
                            .count();
                        (if count > 0 { "on" } else { "off" }).to_string()
                    }
                    "has_frontmatter" => {
                        let mut memo = self.edit_memo.borrow_mut();
                        let content = memo.content(app);
                        let has = content.starts_with("---\n");
                        (if has { "on" } else { "off" }).to_string()
                    }
                    "words_added" => {
                        let wc = self.edit_memo.borrow_mut().counts(app).0;
                        let added = wc as isize - app.editor.initial_word_count as isize;
                        added.to_string()
                    }
                    "editing_id" => app.editor.editing_id.clone().unwrap_or_default(),
                    "editing_template" => (if app.editor.template_edit_path.is_some() {
                        "on"
                    } else {
                        "off"
                    })
                    .to_string(),
                    "line_numbers" => (if app.editor.show_line_numbers {
                        "on"
                    } else {
                        "off"
                    })
                    .to_string(),
                    "editor_preview" => (if app.editor.editor_preview_enabled {
                        "on"
                    } else {
                        "off"
                    })
                    .to_string(),
                    "ext_editor" => app.editor.external_editor.clone().unwrap_or_default(),
                    "ext_editor_enabled" => (if app.editor.external_editor_enabled {
                        "on"
                    } else {
                        "off"
                    })
                    .to_string(),
                    _ => unreachable!(),
                };
                Some(val.into())
            }

            "fps" => {
                if self.view == ViewMode::Graph {
                    if let Some(fps_val) = self.graph_fps {
                        Some(format!("{:.1}", fps_val).into())
                    } else {
                        Some("--".into())
                    }
                } else if let Some(app) = self.app {
                    Some(format!("{:.0}", app.fps).into())
                } else {
                    Some("".into())
                }
            }

            // Graph view
            "node_count" | "edge_count" | "selected_node" | "viewport_size" | "scale"
            | "graph_settled" | "label_mode" | "node_color_mode" | "edge_color_mode"
            | "node_size_mode" | "zoom" | "show_grid" | "show_legend" | "show_minimap" => {
                let graph = match self.graph {
                    Some(g) => g,
                    None => return Some("".into()),
                };

                let graph_inner = graph.simulation.get_graph();

                let val = match name {
                    "node_count" => graph_inner.node_count().to_string(),
                    "edge_count" => graph_inner.edge_count().to_string(),
                    "selected_node" => graph
                        .selection
                        .primary
                        .and_then(|idx| graph_inner.node_weight(idx))
                        .map(|n| n.data.title.clone())
                        .unwrap_or_else(|| "none".to_string()),
                    "viewport_size" => {
                        let aspect = self
                            .area
                            .map(|a| a.width as f64 / a.height as f64)
                            .unwrap_or(1.0);
                        let x_bounds = graph.viewport.x_bounds(aspect);
                        let y_bounds = graph.viewport.y_bounds(aspect);
                        let (gx_min, gx_max, gy_min, gy_max) = graph.graph_bounds;
                        let graph_w = gx_max - gx_min;
                        let graph_h = gy_max - gy_min;
                        let vp_w = x_bounds[1] - x_bounds[0];
                        let vp_h = y_bounds[1] - y_bounds[0];
                        let graph_area = graph_w * graph_h;
                        let vp_area = vp_w * vp_h;
                        let size_pct = if graph_area > 0.0 {
                            (vp_area / graph_area * 100.0).clamp(0.0, 100.0)
                        } else {
                            100.0
                        };
                        format!("{:.0}", size_pct)
                    }
                    "scale" => {
                        format!("{:.2}", graph.viewport.scale())
                    }
                    "graph_settled" => (if graph.is_settled { "on" } else { "off" }).to_string(),
                    "label_mode" => {
                        let lm = match self.config.graf.visual.label_mode {
                            crate::config::LabelMode::Selected => "selected",
                            crate::config::LabelMode::Neighbors => "neighbors",
                            crate::config::LabelMode::All => "all",
                            crate::config::LabelMode::None => "none",
                        };
                        lm.to_string()
                    }
                    "node_color_mode" => {
                        let ncm = match self.config.graf.visual.node_color_mode {
                            crate::config::NodeColorMode::Tag => "tag",
                            crate::config::NodeColorMode::Folder => "folder",
                            crate::config::NodeColorMode::LinkCount => "link_count",
                            crate::config::NodeColorMode::Uniform => "uniform",
                        };
                        ncm.to_string()
                    }
                    "edge_color_mode" => {
                        let ecm = match self.config.graf.visual.edge_color_mode {
                            crate::config::EdgeColorMode::Source => "source",
                            crate::config::EdgeColorMode::Target => "target",
                            crate::config::EdgeColorMode::Uniform => "uniform",
                        };
                        ecm.to_string()
                    }
                    "node_size_mode" => {
                        let nsm = match self.config.graf.visual.node_size_mode {
                            crate::config::NodeSizeMode::Fixed => "fixed",
                            crate::config::NodeSizeMode::LinkCount => "link_count",
                        };
                        nsm.to_string()
                    }
                    "zoom" => graph.viewport.zoom.to_string(),
                    "show_grid" => (if self.graph_grid_visible { "on" } else { "off" }).to_string(),
                    "show_legend" => (if self.config.graf.visual.show_legend {
                        "on"
                    } else {
                        "off"
                    })
                    .to_string(),
                    "show_minimap" => (if self.config.graf.visual.show_minimap {
                        "on"
                    } else {
                        "off"
                    })
                    .to_string(),
                    _ => unreachable!(),
                };
                Some(val.into())
            }

            // Draw view
            "tool" | "shape" | "element_count" | "draw_width" | "draw_height" | "draw_grid"
            | "draw_zoom" | "text_editing" => {
                let draw = match self.draw {
                    Some(d) => d,
                    None => return Some("".into()),
                };

                let val = match name {
                    "tool" => {
                        let t = match draw.active_tool {
                            crate::draw::state::DrawTool::Cursor => "cursor",
                            crate::draw::state::DrawTool::Draw => "draw",
                            crate::draw::state::DrawTool::Erase => "erase",
                            crate::draw::state::DrawTool::Text => "text",
                            crate::draw::state::DrawTool::Shape => "shape",
                        };
                        t.to_string()
                    }
                    "shape" => {
                        let s = match draw.active_shape_type {
                            crate::draw::state::DrawShapeType::Rect => "rect",
                            crate::draw::state::DrawShapeType::Ellipse => "ellipse",
                            crate::draw::state::DrawShapeType::Diamond => "diamond",
                            crate::draw::state::DrawShapeType::Line => "line",
                            crate::draw::state::DrawShapeType::Arrow => "arrow",
                        };
                        s.to_string()
                    }
                    "element_count" => draw.data.elements.len().to_string(),
                    "draw_width" => draw.data.width.to_string(),
                    "draw_height" => draw.data.height.to_string(),
                    "draw_grid" => (if draw.grid.visible { "on" } else { "off" }).to_string(),
                    "draw_zoom" => format!("{:.1}", draw.viewport.zoom),
                    "text_editing" => (if draw.text_editor.is_some() {
                        "on"
                    } else {
                        "off"
                    })
                    .to_string(),
                    _ => unreachable!(),
                };
                Some(val.into())
            }

            // Canvas / Pinstar view
            "canvas_nodes" | "canvas_edges" | "canvas_zoom" | "canvas_pan_x" | "canvas_pan_y"
            | "canvas_selected" | "canvas_grid" | "canvas_editor" => {
                let canvas = match self.canvas {
                    Some(c) => c,
                    None => return Some("".into()),
                };

                let val = match name {
                    "canvas_nodes" => canvas.data.nodes.len().to_string(),
                    "canvas_edges" => canvas.data.edges.len().to_string(),
                    "canvas_zoom" => format!("{:.1}", canvas.zoom),
                    "canvas_pan_x" => canvas.viewport_x.to_string(),
                    "canvas_pan_y" => canvas.viewport_y.to_string(),
                    "canvas_selected" => canvas.selection.primary.clone().unwrap_or_default(),
                    "canvas_grid" => (if canvas.grid.visible { "on" } else { "off" }).to_string(),
                    "canvas_editor" => {
                        (if canvas.show_editor_pane { "on" } else { "off" }).to_string()
                    }
                    _ => unreachable!(),
                };
                Some(val.into())
            }

            // Outline view
            "outline_nodes" | "outline_headers" | "outline_visible" | "outline_cursor"
            | "outline_depth" | "outline_max_depth" | "outline_expanded" | "outline_heading"
            | "outline_note" | "outline_error" => {
                let ct = match self.outline {
                    Some(c) => c,
                    None => return Some("".into()),
                };

                let val = match name {
                    "outline_nodes" => ct.nodes.len().to_string(),
                    "outline_headers" => ct
                        .nodes
                        .iter()
                        .filter(|n| {
                            matches!(n.kind, crate::outline::parse::NodeKind::Header { .. })
                        })
                        .count()
                        .to_string(),
                    "outline_visible" => ct.visible_indices().len().to_string(),
                    "outline_cursor" => (ct.selected + 1).to_string(),
                    "outline_depth" => ct
                        .nodes
                        .get(ct.selected)
                        .map(|n| n.depth)
                        .unwrap_or(0)
                        .to_string(),
                    "outline_max_depth" => ct
                        .nodes
                        .iter()
                        .map(|n| n.depth)
                        .max()
                        .unwrap_or(0)
                        .to_string(),
                    "outline_expanded" => ct.expanded.len().to_string(),
                    "outline_heading" => ct
                        .nodes
                        .get(ct.selected)
                        .and_then(|n| {
                            if let crate::outline::parse::NodeKind::Header { title, .. } = &n.kind {
                                Some(title.clone())
                            } else {
                                None
                            }
                        })
                        .unwrap_or_default(),
                    "outline_note" => ct.note_title.clone(),
                    "outline_error" => if ct.load_error { "error" } else { "" }.to_string(),
                    _ => unreachable!(),
                };
                Some(val.into())
            }

            // Backup view
            "branch" | "ahead" | "behind" | "staged" | "unstaged" | "untracked"
            | "commit_count" | "last_commit" | "last_commit_msg" | "last_commit_author"
            | "last_commit_time" | "remote" | "remote_url" | "backup_section" | "input_mode"
            | "auto_push" | "repo_dirty" | "modified_text" => {
                let backup = match self.backup {
                    Some(b) => b,
                    None => return Some("".into()),
                };

                let val = match name {
                    "branch" => backup
                        .status
                        .as_ref()
                        .map(|s| s.branch.clone())
                        .unwrap_or_default(),
                    "ahead" => backup
                        .status
                        .as_ref()
                        .map(|s| s.ahead.to_string())
                        .unwrap_or_else(|| "0".to_string()),
                    "behind" => backup
                        .status
                        .as_ref()
                        .map(|s| s.behind.to_string())
                        .unwrap_or_else(|| "0".to_string()),
                    "staged" => backup
                        .status
                        .as_ref()
                        .map(|s| s.staged.len().to_string())
                        .unwrap_or_else(|| "0".to_string()),
                    "unstaged" => backup
                        .status
                        .as_ref()
                        .map(|s| s.unstaged.len().to_string())
                        .unwrap_or_else(|| "0".to_string()),
                    "untracked" => backup
                        .status
                        .as_ref()
                        .map(|s| s.untracked.len().to_string())
                        .unwrap_or_else(|| "0".to_string()),
                    "commit_count" => backup.commits.len().to_string(),
                    "last_commit" => backup
                        .commits
                        .first()
                        .map(|c| c.id[..std::cmp::min(7, c.id.len())].to_string())
                        .unwrap_or_default(),
                    "last_commit_msg" => backup
                        .commits
                        .first()
                        .map(|c| c.message.clone())
                        .unwrap_or_default(),
                    "last_commit_author" => backup
                        .commits
                        .first()
                        .map(|c| c.author.clone())
                        .unwrap_or_default(),
                    "last_commit_time" => backup
                        .commits
                        .first()
                        .map(|c| format_relative_time(c.time).into_owned())
                        .unwrap_or_default(),
                    "remote" => backup.settings.remote_name.lines().join(""),
                    "remote_url" => backup.settings.remote_url.lines().join(""),
                    "backup_section" => match backup.selected_section {
                        crate::backup::state::BackupSection::Status => "status",
                        crate::backup::state::BackupSection::History => "history",
                    }
                    .to_string(),
                    "input_mode" => match backup.input_mode {
                        crate::backup::state::BackupInputMode::Normal => "normal",
                        crate::backup::state::BackupInputMode::EditCommitMessage => "edit_commit",
                        crate::backup::state::BackupInputMode::EditSettings => "edit_settings",
                        crate::backup::state::BackupInputMode::EditSettingsField => {
                            "edit_settings_field"
                        }
                    }
                    .to_string(),
                    "auto_push" => (if backup.settings.auto_push {
                        "on"
                    } else {
                        "off"
                    })
                    .to_string(),
                    "repo_dirty" => {
                        let dirty = backup
                            .status
                            .as_ref()
                            .map(|s| {
                                !s.staged.is_empty()
                                    || !s.unstaged.is_empty()
                                    || !s.untracked.is_empty()
                            })
                            .unwrap_or(false);
                        (if dirty { "on" } else { "off" }).to_string()
                    }
                    "modified_text" => if let Some(status) = &backup.status {
                        if !status.staged.is_empty()
                            || !status.unstaged.is_empty()
                            || !status.untracked.is_empty()
                        {
                            "modified"
                        } else {
                            "clean"
                        }
                    } else {
                        ""
                    }
                    .to_string(),
                    _ => unreachable!(),
                };
                Some(val.into())
            }

            _ => None,
        }
    }
}

pub fn render_segments<'a>(
    template: &str,
    ctx: &StatuslineContext<'a>,
    theme: &AppThemeColors,
) -> Vec<Segment<'a>> {
    render_segments_with_detail(template, ctx, theme, None)
}

#[allow(unused_assignments)]
fn render_segments_with_detail<'a>(
    template: &str,
    ctx: &StatuslineContext<'a>,
    _theme: &AppThemeColors,
    detail_override: Option<Vec<Span<'a>>>,
) -> Vec<Segment<'a>> {
    let mut segments = Vec::new();

    // Cell assembler state.
    let mut cur = String::new();
    let mut cur_has_var = false;

    macro_rules! flush_cell {
        () => {{
            let trimmed = cur.trim();
            if !trimmed.is_empty() {
                segments.push(Segment::Text(trimmed.to_string()));
            }
            cur.clear();
            cur_has_var = false;
        }};
    }

    let mut chars = template.chars().peekable();
    while let Some(c) = chars.next() {
        if c == '{' {
            if chars.peek() == Some(&'{') {
                chars.next();
                cur.push('{');
            } else {
                let mut name = String::new();
                let mut found_close = false;
                while let Some(&nc) = chars.peek() {
                    if nc == '}' {
                        chars.next();
                        found_close = true;
                        break;
                    } else if nc == '{' {
                        break;
                    } else {
                        name.push(nc);
                        chars.next();
                    }
                }

                if found_close {
                    match name.as_str() {
                        "preview" | "detail" | "hints" | "badge" | "pending" => {
                            // Composites are their own segments: flush any pending
                            // text cell first, then push the composite directly.
                            flush_cell!();
                            let spans = match name.as_str() {
                                "preview" => ctx.preview.clone(),
                                "detail" => detail_override.clone().or_else(|| ctx.detail.clone()),
                                "hints" => ctx.hints.clone(),
                                "badge" => ctx.badge.clone(),
                                "pending" => ctx.pending.clone(),
                                _ => None,
                            };
                            if let Some(spans) = spans {
                                if name == "detail" {
                                    segments.push(Segment::CompositeSplittable(spans));
                                } else {
                                    segments.push(Segment::Composite(spans));
                                }
                            }
                        }
                        _ => {
                            // Boundary: a var cell is open and a whitespace run
                            // separates it from this new var → close the old cell.
                            if cur_has_var
                                && cur.chars().last().is_some_and(|ch| ch.is_whitespace())
                            {
                                flush_cell!();
                            }
                            if let Some(val) = ctx.resolve(&name) {
                                cur.push_str(&val);
                                cur_has_var = true;
                            } else {
                                // Unresolved name → literal text (not a var anchor).
                                cur.push('{');
                                cur.push_str(&name);
                                cur.push('}');
                            }
                        }
                    }
                } else {
                    // Unclosed '{' → literal.
                    cur.push('{');
                    cur.push_str(&name);
                }
            }
        } else if c == '}' {
            if chars.peek() == Some(&'}') {
                chars.next();
                cur.push('}');
            } else {
                cur.push('}');
            }
        } else {
            // Detect explicit " | " separator when a var cell is open: it forces a
            // cell boundary and is consumed (not rendered). Backtrack-safe via clone
            // (std::str::Chars: Clone; Peekable<I>: Clone when I: Clone).
            if c == ' ' && cur_has_var && chars.peek() == Some(&'|') {
                let mut probe = chars.clone();
                probe.next(); // consume '|'
                if probe.peek() == Some(&' ') {
                    chars.next(); // consume '|'
                    chars.next(); // consume ' '
                    flush_cell!();
                    continue;
                }
            }
            cur.push(c);
        }
    }

    flush_cell!();
    segments
}

pub fn line_from_segments<'a>(
    segs: &[Segment<'a>],
    theme: &AppThemeColors,
    is_header: bool,
    is_right: bool,
) -> Line<'a> {
    let mut flat = Vec::new();
    for seg in segs {
        match seg {
            Segment::Text(text) => {
                let trimmed = text.trim();
                if !trimmed.is_empty() {
                    flat.push(FlatSegment::Cell(trimmed.to_string()));
                }
            }
            Segment::Composite(spans) => {
                if !spans.is_empty() {
                    flat.push(FlatSegment::Composite(spans.clone()));
                }
            }
            Segment::CompositeSplittable(spans) => {
                if !spans.is_empty() {
                    flat.push(FlatSegment::Splittable(spans.clone()));
                }
            }
        }
    }

    if flat.is_empty() {
        return Line::default();
    }

    // Every non-Classic style splits composite segments into per-term cells.
    let split_cells = !matches!(
        theme.hint_bar_style,
        crate::config::HintBarStyle::Classic | crate::config::HintBarStyle::Compact
    );

    let flat: Vec<FlatSegment> = if split_cells {
        let mut out = Vec::new();
        for seg in flat {
            match seg {
                FlatSegment::Splittable(spans) => {
                    let text: String = spans.iter().map(|s| s.content.as_ref()).collect();
                    for part in text.split(" | ") {
                        let t = part.trim();
                        if !t.is_empty() {
                            out.push(FlatSegment::Cell(t.to_string()));
                        }
                    }
                }
                other => out.push(other),
            }
        }
        out
    } else {
        flat.into_iter()
            .map(|s| match s {
                FlatSegment::Splittable(spans) => FlatSegment::Composite(spans),
                other => other,
            })
            .collect()
    };

    let mut spans = Vec::new();

    let bar_bg = if is_header {
        theme.title_bar_bg()
    } else {
        theme.hint_line_bg()
    };

    let bg_colors = [
        theme.accent,
        theme.folder,
        theme.tag,
        theme.warning,
        theme.success,
    ];

    let is_header_left = is_header && !is_right;

    match theme.hint_bar_style {
        crate::config::HintBarStyle::Classic => {
            // Classic: muted dots + no bg fills.
            let palette = bg_colors;
            let mut cell_idx = 0;
            let mut prev_was_cell = false;
            for seg in flat {
                match seg {
                    FlatSegment::Cell(text) => {
                        let style = if is_header_left && cell_idx == 0 {
                            Style::default()
                                .fg(theme.highlight_fg)
                                .bg(theme.heading)
                                .add_modifier(Modifier::BOLD)
                        } else if is_header_left {
                            Style::default().fg(theme.fg)
                        } else {
                            Style::default()
                                .fg(palette[cell_idx % palette.len()])
                                .add_modifier(Modifier::BOLD)
                        };
                        if !is_header_left && prev_was_cell {
                            spans.push(Span::styled(" · ", Style::default().fg(theme.muted)));
                        }
                        let text_to_render = if is_header_left && cell_idx == 0 {
                            format!(" {} ", text.trim())
                        } else {
                            text
                        };
                        spans.push(Span::styled(text_to_render, style));
                        cell_idx += 1;
                        prev_was_cell = true;
                    }
                    FlatSegment::Composite(comp_spans) => {
                        if !is_header_left && prev_was_cell {
                            spans.push(Span::styled(" · ", Style::default().fg(theme.muted)));
                        }
                        spans.extend(comp_spans);
                        prev_was_cell = false;
                    }
                    FlatSegment::Splittable(_) => unreachable!(),
                }
            }
        }
        crate::config::HintBarStyle::Compact => {
            // Flat text like Classic but no · dots, single space separation.
            let palette = bg_colors;
            let mut cell_idx = 0;
            let mut prev_was_cell = false;
            for seg in flat {
                match seg {
                    FlatSegment::Cell(text) => {
                        let style = if is_header_left && cell_idx == 0 {
                            Style::default()
                                .fg(theme.highlight_fg)
                                .bg(theme.heading)
                                .add_modifier(Modifier::BOLD)
                        } else if is_header_left {
                            Style::default().fg(theme.fg)
                        } else {
                            Style::default()
                                .fg(palette[cell_idx % palette.len()])
                                .add_modifier(Modifier::BOLD)
                        };
                        if prev_was_cell {
                            spans.push(Span::raw(" "));
                        }
                        let text_to_render = if is_header_left && cell_idx == 0 {
                            format!(" {} ", text.trim())
                        } else {
                            text
                        };
                        spans.push(Span::styled(text_to_render, style));
                        cell_idx += 1;
                        prev_was_cell = true;
                    }
                    FlatSegment::Composite(comp_spans) => {
                        if prev_was_cell {
                            spans.push(Span::raw(" "));
                        }
                        spans.extend(comp_spans);
                        prev_was_cell = false;
                    }
                    FlatSegment::Splittable(_) => unreachable!(),
                }
            }
        }
        style if style.is_chained() => {
            // Chained powerline family: Sharp, Rounded, Slanted.
            let sep_char = match style {
                crate::config::HintBarStyle::Sharp | crate::config::HintBarStyle::SharpGradient => {
                    if is_right {
                        "\u{e0b2}"
                    } else {
                        "\u{e0b0}"
                    }
                }
                crate::config::HintBarStyle::Rounded
                | crate::config::HintBarStyle::RoundedGradient => {
                    if is_right {
                        "\u{e0b6}"
                    } else {
                        "\u{e0b4}"
                    }
                }
                crate::config::HintBarStyle::Slanted
                | crate::config::HintBarStyle::SlantedGradient => {
                    if is_right {
                        "\u{e0be}"
                    } else {
                        "\u{e0bc}"
                    }
                }
                _ => unreachable!(),
            };

            let mut cell_idx = 0;

            if is_right {
                for (idx, seg) in flat.iter().enumerate() {
                    match seg {
                        FlatSegment::Cell(text) => {
                            let bg = bg_colors[cell_idx % bg_colors.len()];
                            let prev_bg = if idx > 0 {
                                let prev_cell_idx = cell_idx.saturating_sub(1);
                                get_segment_bg(
                                    &flat[idx - 1],
                                    prev_cell_idx,
                                    is_header,
                                    true,
                                    theme,
                                    &bg_colors,
                                )
                            } else {
                                None
                            };
                            let prev_bg_val = prev_bg.or(bar_bg).unwrap_or(Color::Reset);

                            if style.is_gradient() {
                                let start_bg = prev_bg.or(bar_bg).unwrap_or(Color::Black);
                                let step1 = crate::app_theme::mix_colors(start_bg, bg, 0.33);
                                let step2 = crate::app_theme::mix_colors(start_bg, bg, 0.67);

                                let mut style1 = Style::default().fg(step1);
                                if prev_bg.or(bar_bg).is_some() {
                                    style1 = style1.bg(start_bg);
                                }
                                spans.push(Span::styled(sep_char, style1));
                                spans.push(Span::styled(
                                    sep_char,
                                    Style::default().fg(step2).bg(step1),
                                ));
                                spans.push(Span::styled(
                                    sep_char,
                                    Style::default().fg(bg).bg(step2),
                                ));
                            } else {
                                let mut sep_style = Style::default().fg(bg);
                                if prev_bg.or(bar_bg).is_some() {
                                    sep_style = sep_style.bg(prev_bg_val);
                                }
                                spans.push(Span::styled(sep_char, sep_style));
                            }

                            spans.push(Span::styled(
                                format!(" {} ", text),
                                Style::default()
                                    .bg(bg)
                                    .fg(theme.highlight_fg)
                                    .add_modifier(Modifier::BOLD),
                            ));

                            cell_idx += 1;
                        }
                        FlatSegment::Composite(comp_spans) => {
                            spans.extend(comp_spans.clone());
                        }
                        FlatSegment::Splittable(_) => unreachable!(),
                    }
                }
            } else {
                for (idx, seg) in flat.iter().enumerate() {
                    match seg {
                        FlatSegment::Cell(text) => {
                            let bg = if is_header && cell_idx == 0 {
                                theme.heading
                            } else {
                                bg_colors[cell_idx % bg_colors.len()]
                            };

                            spans.push(Span::styled(
                                format!(" {} ", text),
                                Style::default()
                                    .bg(bg)
                                    .fg(theme.highlight_fg)
                                    .add_modifier(Modifier::BOLD),
                            ));

                            let next_bg = if idx + 1 < flat.len() {
                                let next_cell_idx = cell_idx + 1;
                                get_segment_bg(
                                    &flat[idx + 1],
                                    next_cell_idx,
                                    is_header,
                                    false,
                                    theme,
                                    &bg_colors,
                                )
                            } else {
                                None
                            };
                            let next_bg_val = next_bg.or(bar_bg).unwrap_or(Color::Reset);

                            if style.is_gradient() {
                                let end_bg = next_bg.or(bar_bg).unwrap_or(Color::Black);
                                let step1 = crate::app_theme::mix_colors(bg, end_bg, 0.33);
                                let step2 = crate::app_theme::mix_colors(bg, end_bg, 0.67);

                                spans.push(Span::styled(
                                    sep_char,
                                    Style::default().fg(bg).bg(step1),
                                ));
                                spans.push(Span::styled(
                                    sep_char,
                                    Style::default().fg(step1).bg(step2),
                                ));
                                let mut style3 = Style::default().fg(step2);
                                if next_bg.or(bar_bg).is_some() {
                                    style3 = style3.bg(end_bg);
                                }
                                spans.push(Span::styled(sep_char, style3));
                            } else {
                                let mut sep_style = Style::default().fg(bg);
                                if next_bg.or(bar_bg).is_some() {
                                    sep_style = sep_style.bg(next_bg_val);
                                }
                                spans.push(Span::styled(sep_char, sep_style));
                            }

                            cell_idx += 1;
                        }
                        FlatSegment::Composite(comp_spans) => {
                            spans.extend(comp_spans.clone());
                        }
                        FlatSegment::Splittable(_) => unreachable!(),
                    }
                }
            }
        }
        style if style.cell_caps().is_some() => {
            // Detached family: Bubbles, Blurred, Chips. Symmetrical — same render for left/right.
            let (cap_l, cap_r) = style.cell_caps().unwrap_or(("", ""));

            let mut cell_idx = 0;
            for (idx, seg) in flat.iter().enumerate() {
                match seg {
                    FlatSegment::Cell(text) => {
                        let bg = if is_header && !is_right && cell_idx == 0 {
                            theme.heading
                        } else {
                            bg_colors[cell_idx % bg_colors.len()]
                        };

                        if let Some(bbg) = bar_bg {
                            spans.push(Span::styled(cap_l, Style::default().fg(bg).bg(bbg)));
                        } else {
                            spans.push(Span::styled(cap_l, Style::default().fg(bg)));
                        }

                        spans.push(Span::styled(
                            format!(" {} ", text),
                            Style::default()
                                .bg(bg)
                                .fg(theme.highlight_fg)
                                .add_modifier(Modifier::BOLD),
                        ));

                        if let Some(bbg) = bar_bg {
                            spans.push(Span::styled(cap_r, Style::default().fg(bg).bg(bbg)));
                        } else {
                            spans.push(Span::styled(cap_r, Style::default().fg(bg)));
                        }

                        if idx < flat.len() - 1 {
                            if let Some(bbg) = bar_bg {
                                spans.push(Span::styled(" ", Style::default().bg(bbg)));
                            } else {
                                spans.push(Span::raw(" "));
                            }
                        }

                        cell_idx += 1;
                    }
                    FlatSegment::Composite(comp_spans) => {
                        spans.extend(comp_spans.clone());
                    }
                    FlatSegment::Splittable(_) => unreachable!(),
                }
            }
        }
        crate::config::HintBarStyle::Brackets => {
            // Brackets: [key] action, no bg fills.
            let mut cell_idx = 0;
            let mut prev_was_cell = false;
            for seg in flat {
                match seg {
                    FlatSegment::Cell(text) => {
                        let fg = if is_header_left && cell_idx == 0 {
                            theme.heading
                        } else {
                            bg_colors[cell_idx % bg_colors.len()]
                        };
                        if prev_was_cell {
                            spans.push(Span::raw(" "));
                        }
                        spans.push(Span::styled("[", Style::default().fg(theme.fg)));
                        spans.push(Span::styled(
                            text,
                            Style::default().fg(fg).add_modifier(Modifier::BOLD),
                        ));
                        spans.push(Span::styled("]", Style::default().fg(theme.fg)));
                        cell_idx += 1;
                        prev_was_cell = true;
                    }
                    FlatSegment::Composite(comp_spans) => {
                        spans.extend(comp_spans);
                        prev_was_cell = false;
                    }
                    FlatSegment::Splittable(_) => unreachable!(),
                }
            }
        }
        _ => unreachable!(),
    }

    Line::from(spans)
}

fn get_segment_bg(
    seg: &FlatSegment<'_>,
    cell_idx: usize,
    is_header: bool,
    is_right: bool,
    theme: &AppThemeColors,
    bg_colors: &[Color],
) -> Option<Color> {
    match seg {
        FlatSegment::Cell(_) => {
            if is_header && !is_right && cell_idx == 0 {
                Some(theme.heading)
            } else {
                Some(bg_colors[cell_idx % bg_colors.len()])
            }
        }
        FlatSegment::Composite(spans) => spans.first().and_then(|s| s.style.bg),
        FlatSegment::Splittable(_) => unreachable!(),
    }
}

pub struct StatuslineTemplates {
    pub header_left: Cow<'static, str>,
    pub header_right: Cow<'static, str>,
    pub footer_left: Cow<'static, str>,
    pub footer_right: Cow<'static, str>,
}

pub fn effective_templates(cfg: &StatuslineConfig, view: ViewMode) -> StatuslineTemplates {
    let view_override = match view {
        ViewMode::List => cfg.list.as_ref(),
        ViewMode::Edit => cfg.edit.as_ref(),
        ViewMode::Help => cfg.help.as_ref(),
        ViewMode::Graph => cfg.graph.as_ref(),
        ViewMode::Draw => cfg.draw.as_ref(),
        ViewMode::Canvas => cfg.canvas.as_ref(),
        ViewMode::Backup => cfg.backup.as_ref(),
        ViewMode::Outline => cfg.outline.as_ref(),
        ViewMode::Setup => None,
    };

    let header_left = view_override
        .and_then(|o| o.header_left.clone())
        .or_else(|| cfg.header_left.clone())
        .map(Cow::Owned)
        .unwrap_or_else(|| default_template(view, "header_left"));

    let header_right = view_override
        .and_then(|o| o.header_right.clone())
        .or_else(|| cfg.header_right.clone())
        .map(Cow::Owned)
        .unwrap_or_else(|| default_template(view, "header_right"));

    let footer_left = view_override
        .and_then(|o| o.footer_left.clone())
        .or_else(|| cfg.footer_left.clone())
        .map(Cow::Owned)
        .unwrap_or_else(|| default_template(view, "footer_left"));

    let footer_right = view_override
        .and_then(|o| o.footer_right.clone())
        .or_else(|| cfg.footer_right.clone())
        .map(Cow::Owned)
        .unwrap_or_else(|| default_template(view, "footer_right"));

    StatuslineTemplates {
        header_left,
        header_right,
        footer_left,
        footer_right,
    }
}

fn default_template(view: ViewMode, field: &str) -> Cow<'static, str> {
    match field {
        "header_left" => "{title} {preview}".into(),
        "footer_left" => "{pending}{badge}{hints}".into(),
        "header_right" => {
            match view {
                ViewMode::Graph => "Nodes: {node_count} | Edges: {edge_count} | Selected: {selected_node} | Scale: {scale}× | FPS: {fps}   ".into(),
                ViewMode::List => "{sort}  {detail}".into(),
                ViewMode::Setup => "{pinned_count} pinned".into(),
                ViewMode::Help => "Page {help_page}/{help_total_pages}".into(),
                ViewMode::Edit => "{word_count}w {char_count}c {cursor_line}:{cursor_col}".into(),
                _ => "".into(),
            }
        }
        "footer_right" => match view {
            ViewMode::List => "{note_count} notes ({selected_count} selected) | {version}".into(),
            _ => "{version}".into(),
        },
        _ => "".into(),
    }
}

pub(crate) fn render_header_left<'a>(
    ctx: &StatuslineContext<'a>,
    cfg: &StatuslineConfig,
    view: ViewMode,
    theme: &AppThemeColors,
) -> Line<'a> {
    let template = effective_templates(cfg, view).header_left;
    line_from_segments(&render_segments(&template, ctx, theme), theme, true, false)
}

fn field_for_list_variable(name: &str) -> Option<ListHeaderField> {
    match name {
        "tags" => Some(ListHeaderField::Tags),
        "sort" => Some(ListHeaderField::Sort),
        "note_updated_rel" => Some(ListHeaderField::Age),
        "note_size" => Some(ListHeaderField::Size),
        _ => None,
    }
}

fn suppress_list_fields(template: &str, hidden: &[ListHeaderField]) -> String {
    let mut out = String::with_capacity(template.len());
    let mut chars = template.chars().peekable();
    while let Some(ch) = chars.next() {
        if ch != '{' {
            out.push(ch);
            continue;
        }
        if chars.peek() == Some(&'{') {
            out.push('{');
            out.push(chars.next().expect("peeked opening brace"));
            continue;
        }
        let mut name = String::new();
        let mut closed = false;
        for next in chars.by_ref() {
            if next == '}' {
                closed = true;
                break;
            }
            if next == '{' {
                out.push('{');
                out.push_str(&name);
                out.push('{');
                break;
            }
            name.push(next);
        }
        if closed {
            if field_for_list_variable(&name).is_none_or(|field| !hidden.contains(&field)) {
                out.push('{');
                out.push_str(&name);
                out.push('}');
            }
        } else if !name.is_empty() {
            out.push('{');
            out.push_str(&name);
        }
    }
    out
}

pub(crate) fn render_header_right<'a>(
    ctx: &StatuslineContext<'a>,
    cfg: &StatuslineConfig,
    view: ViewMode,
    theme: &AppThemeColors,
    max_width: Option<u16>,
) -> Option<Line<'a>> {
    let template = effective_templates(cfg, view).header_right;
    let mut hidden = Vec::new();
    loop {
        let rendered_template = if view == ViewMode::List {
            suppress_list_fields(&template, &hidden)
        } else {
            template.to_string()
        };
        let detail = ctx
            .list_detail
            .as_ref()
            .map(|value| value.spans_without(&hidden));
        let segments = render_segments_with_detail(&rendered_template, ctx, theme, detail);
        let line = line_from_segments(&segments, theme, true, true);
        if line.width() == 0 || template.trim().is_empty() {
            return None;
        }
        if max_width.is_none_or(|width| line.width() <= usize::from(width)) {
            return Some(line);
        }

        let candidates: &[ListHeaderField] = if ctx.note.is_some() {
            &[
                ListHeaderField::Size,
                ListHeaderField::Age,
                ListHeaderField::Sort,
                ListHeaderField::Tags,
            ]
        } else if ctx.list_detail.as_ref().is_some_and(|detail| {
            detail
                .groups
                .iter()
                .any(|(field, _)| *field == ListHeaderField::Count)
        }) {
            &[ListHeaderField::Count, ListHeaderField::Sort]
        } else {
            &[]
        };
        let Some(next) = candidates
            .iter()
            .copied()
            .find(|field| !hidden.contains(field))
        else {
            return Some(line);
        };
        hidden.push(next);
    }
}

pub fn render_header<'a>(
    ctx: &StatuslineContext<'a>,
    cfg: &StatuslineConfig,
    view: ViewMode,
    theme: &AppThemeColors,
) -> (Line<'a>, Option<Line<'a>>) {
    (
        render_header_left(ctx, cfg, view, theme),
        render_header_right(ctx, cfg, view, theme, None),
    )
}

pub fn render_footer<'a>(
    ctx: &StatuslineContext<'a>,
    cfg: &StatuslineConfig,
    view: ViewMode,
    theme: &AppThemeColors,
) -> (Line<'a>, Option<Line<'a>>) {
    let tmpl = effective_templates(cfg, view);
    let left_segs = render_segments(&tmpl.footer_left, ctx, theme);
    let right_segs = render_segments(&tmpl.footer_right, ctx, theme);

    let left_line = line_from_segments(&left_segs, theme, false, false);
    let right_line = line_from_segments(&right_segs, theme, false, true);

    let right_opt = if right_line.width() > 0 && tmpl.footer_right.trim() != "" {
        Some(right_line)
    } else {
        None
    };

    (left_line, right_opt)
}

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

    fn text_cells(segs: Vec<Segment>) -> Vec<String> {
        segs.into_iter()
            .filter_map(|s| match s {
                Segment::Text(t) => Some(t),
                _ => None,
            })
            .collect()
    }

    #[test]
    fn test_render_segments_escapes() {
        let config = ClinConfig::default();
        let ctx = StatuslineContext::for_overlay(&config, ViewMode::List);
        let theme = AppThemeColors::default();

        let segs = render_segments("hello {{world}} {view} {invalid_var}", &ctx, &theme);
        assert_eq!(
            text_cells(segs),
            vec![
                "hello {world} Notes".to_string(),
                "{invalid_var}".to_string()
            ]
        );
    }

    #[test]
    fn test_statusline_per_variable_cells() {
        let config = ClinConfig::default();
        let ctx = StatuslineContext::for_overlay(&config, ViewMode::List);
        let theme = AppThemeColors::default();

        // Each whitespace-separated variable is its own cell.
        let segs = render_segments("{view} {preset}", &ctx, &theme);
        assert_eq!(
            text_cells(segs),
            vec!["Notes".to_string(), "default".to_string()]
        );
    }

    #[test]
    fn test_statusline_label_glues_to_var() {
        let config = ClinConfig::default();
        let ctx = StatuslineContext::for_overlay(&config, ViewMode::List);
        let theme = AppThemeColors::default();

        // A literal label before a var stays in the same cell.
        let segs = render_segments("Notes: {view}", &ctx, &theme);
        assert_eq!(text_cells(segs), vec!["Notes: Notes".to_string()]);
    }

    #[test]
    fn test_statusline_pipe_separator() {
        let config = ClinConfig::default();
        let ctx = StatuslineContext::for_overlay(&config, ViewMode::List);
        let theme = AppThemeColors::default();

        // Explicit " | " forces a boundary; the pipe is consumed, not rendered.
        let segs = render_segments("{view} | {preset}", &ctx, &theme);
        assert_eq!(
            text_cells(segs),
            vec!["Notes".to_string(), "default".to_string()]
        );
    }

    #[test]
    fn test_default_statusline_rendering() {
        let config = ClinConfig::default();
        let ctx = StatuslineContext::for_overlay(&config, ViewMode::List);
        let theme = AppThemeColors::default();

        let (left, right) = render_header(&ctx, &config.statusline, ViewMode::List, &theme);
        assert_eq!(left.width(), 7); // " Notes " (7 chars)
        assert!(right.is_none());
    }
    #[test]
    fn test_statusline_graph_scale_and_fps() {
        let config = ClinConfig::default();
        let mut ctx = StatuslineContext::for_overlay(&config, ViewMode::Graph);
        let theme = AppThemeColors::default();

        let mut graph_state = crate::graf::graph::GraphState {
            simulation: fdg_sim::Simulation::from_graph(
                fdg_sim::ForceGraph::default(),
                fdg_sim::SimulationParameters::default(),
            ),
            viewport: crate::graf::viewport::Viewport::default(),
            selection: crate::ui::CanvasSelection::new(),
            dragging_node: None,
            drag_target: None,
            is_settled: true,
            alpha: 0.0,
            graph_bounds: (0.0, 0.0, 0.0, 0.0),
            render_cache: parking_lot::Mutex::new(crate::graf::render::RenderCache::new()),
            mouse_pos: None,
            spatial_grid: crate::graf::spatial::SpatialGrid::new(100.0),
            physics_worker_active: false,
            physics_ideal_distance: 80.0,
            context_menu: None,
            connection_source: None,
            deleting_connection_source: None,
            marquee: crate::ui::MarqueeDragState::new(3),
            right_down_pos: None,
            mode_banner: None,
        };
        ctx.graph = Some(&graph_state);
        let segs_scale = render_segments("{scale}", &ctx, &theme);
        assert_eq!(text_cells(segs_scale), vec!["1.00".to_string()]);

        graph_state.viewport.zoom = 2.5;
        let mut ctx2 = StatuslineContext::for_overlay(&config, ViewMode::Graph);
        ctx2.graph = Some(&graph_state);
        let segs_scale2 = render_segments("{scale}", &ctx2, &theme);
        assert_eq!(text_cells(segs_scale2), vec!["2.50".to_string()]);

        ctx2.graph_fps = None;
        let segs_fps_none = render_segments("{fps}", &ctx2, &theme);
        assert_eq!(text_cells(segs_fps_none), vec!["--".to_string()]);

        ctx2.graph_fps = Some(12.34);
        let segs_fps_val = render_segments("{fps}", &ctx2, &theme);
        assert_eq!(text_cells(segs_fps_val), vec!["12.3".to_string()]);
    }

    #[test]
    fn test_statusline_gradient_rendering() {
        let config = ClinConfig::default();
        let ctx = StatuslineContext::for_overlay(&config, ViewMode::List);

        for style in [
            crate::config::HintBarStyle::SharpGradient,
            crate::config::HintBarStyle::RoundedGradient,
            crate::config::HintBarStyle::SlantedGradient,
        ] {
            let theme = AppThemeColors {
                hint_bar_style: style,
                ..Default::default()
            };

            let (left, right) = render_header(&ctx, &config.statusline, ViewMode::List, &theme);
            assert!(left.width() > 0);
            assert!(right.is_none());
        }
    }

    #[test]
    fn test_statusline_new_styles_rendering() {
        let config = ClinConfig::default();
        let ctx = StatuslineContext::for_overlay(&config, ViewMode::List);

        let style = crate::config::HintBarStyle::Hexagon;
        let theme = AppThemeColors {
            hint_bar_style: style,
            ..Default::default()
        };

        let (left, right) = render_header(&ctx, &config.statusline, ViewMode::List, &theme);
        assert!(left.width() > 0);
        assert!(right.is_none());

        // format_keybind_hints
        let items = vec![("g".to_string(), "Go"), ("q".to_string(), "Quit")];
        let hints_line = crate::ui::format_keybind_hints(&theme, &items);
        assert!(hints_line.width() > 0);

        // ext_badge_spans
        let badge_spans = crate::ui::ext_badge_spans(true, &theme, None);
        assert!(!badge_spans.is_empty());
    }
    #[test]
    fn list_header_relative_time_boundaries() {
        let now = 100_000_000;
        for (elapsed, expected) in [
            (59, "just now"),
            (60, "1m ago"),
            (3_599, "59m ago"),
            (3_600, "1h ago"),
            (86_399, "23h ago"),
            (86_400, "1d ago"),
            (604_799, "6d ago"),
            (604_800, "1w ago"),
            (52 * 604_800 - 1, "51w ago"),
            (52 * 604_800, "1y ago"),
        ] {
            assert_eq!(list_relative_age_at(now - elapsed, now), expected);
        }
        assert_eq!(list_relative_age_at(now + 1, now), "just now");
    }
}
