use crate::components::keyboard;
use iced::widget::button;
use std::path::{Path, PathBuf};

use iced::widget::text::Shaping;
use iced::widget::{Space, column, container, image, row, scrollable, text, text_input};
use iced::{Background, Border, Color, Element, Length, Theme};

use bds_core::i18n::UiLocale;
use bds_core::model::{ChatConversation, ImportDefinition, Media, Post, Script, Template};

use crate::app::Message;
use crate::components::inputs;
use crate::i18n::relative_date::{format_locale, format_relative_date};
use crate::i18n::t;
use crate::state::navigation::SidebarView;
use crate::state::sidebar_filter::{CalendarYear, MediaFilter, PostFilter};
use crate::state::tabs::{Tab, TabType};
use crate::views::git::GitUiState;

/// Sidebar container style — dark background.
fn sidebar_style(_theme: &Theme) -> container::Style {
    container::Style {
        background: Some(Background::Color(inputs::SIDEBAR_BG)),
        ..container::Style::default()
    }
}

/// Sidebar item button style.
fn item_style(_theme: &Theme, status: button::Status) -> button::Style {
    let bg = match status {
        button::Status::Hovered => inputs::HOVER_BG,
        _ => Color::TRANSPARENT,
    };
    button::Style {
        background: Some(Background::Color(bg)),
        text_color: Color::from_rgb(0.80, 0.80, 0.85),
        border: Border {
            radius: 6.0.into(),
            ..Border::default()
        },
        ..button::Style::default()
    }
}

/// Sidebar item button style — active/selected.
fn item_active_style(_theme: &Theme, status: button::Status) -> button::Style {
    let bg = match status {
        button::Status::Hovered => inputs::BORDER_COLOR,
        _ => inputs::ELEVATED_BG,
    };
    button::Style {
        background: Some(Background::Color(bg)),
        text_color: Color::WHITE,
        border: Border {
            radius: 6.0.into(),
            ..Border::default()
        },
        ..button::Style::default()
    }
}

/// 40×40 thumbnail container: rounded corners, dark background.
fn thumbnail_container_style(_theme: &Theme) -> container::Style {
    container::Style {
        background: Some(Background::Color(inputs::APP_BG)),
        border: Border {
            radius: 6.0.into(),
            ..Border::default()
        },
        ..container::Style::default()
    }
}

/// Get the appropriate empty-state message key for each sidebar view.
fn placeholder_key(view: SidebarView) -> &'static str {
    match view {
        SidebarView::Posts => "sidebar.noPostsYet",
        SidebarView::Pages => "sidebar.noPagesYet",
        SidebarView::Media => "sidebar.noMediaYet",
        SidebarView::Scripts => "sidebar.noScriptsYet",
        SidebarView::Templates => "sidebar.noTemplatesYet",
        SidebarView::Tags => "sidebar.tagsHeader",
        SidebarView::Chat => "chat.sidebar.empty",
        SidebarView::Import => "sidebar.importPlaceholder",
        SidebarView::Git => "git.noChanges",
        SidebarView::Settings => "sidebar.settingsHeader",
    }
}

/// sidebar_views.allium media_title_max_length = 60
const MEDIA_TITLE_MAX_LEN: usize = 60;

/// Approximate average character width at font size 12 for proportional fonts.
/// Used to estimate how many characters fit in a given pixel width.
const AVG_CHAR_WIDTH_PX: f32 = 6.8;

/// Sidebar padding on each side (12px) plus item padding (6px each side).
const SIDEBAR_TEXT_OVERHEAD_PX: f32 = 36.0;

/// Truncate a string to fit approximately within `available_px` pixels,
/// appending "…" if truncation occurs.
fn truncate_to_fit(s: &str, available_px: f32) -> String {
    let max_chars = ((available_px / AVG_CHAR_WIDTH_PX) as usize).max(6);
    if s.chars().count() <= max_chars {
        s.to_string()
    } else {
        let truncated: String = s.chars().take(max_chars.saturating_sub(1)).collect();
        format!("{truncated}\u{2026}")
    }
}

/// Format a file size in bytes to a human-readable string (B / KB / MB).
fn format_file_size(size: i64) -> String {
    if size < 1024 {
        format!("{size} B")
    } else if size < 1024 * 1024 {
        format!("{:.1} KB", size as f64 / 1024.0)
    } else {
        format!("{:.1} MB", size as f64 / (1024.0 * 1024.0))
    }
}

/// Truncate a media title to the max length, appending "..." if over limit.
/// Per sidebar_views.allium: JS hard limit of 60 chars on title (substring + "...").
fn truncate_media_title(title: &str) -> String {
    if title.chars().count() > MEDIA_TITLE_MAX_LEN {
        let truncated: String = title.chars().take(MEDIA_TITLE_MAX_LEN).collect();
        format!("{truncated}...")
    } else {
        title.to_string()
    }
}

/// Per sidebar_views.allium PostTypeIcon: map first category to emoji.
fn post_type_icon(categories: &[String]) -> &'static str {
    for cat in categories {
        match cat.to_lowercase().as_str() {
            "picture" => return "\u{1F4F7}",            // 📷 camera
            "article" => return "\u{1F5D2}",            // 🗒 notepad
            "aside" | "blogmark" => return "\u{1F517}", // 🔗 link
            "video" => return "\u{1F3AC}",              // 🎬 film
            "podcast" => return "\u{1F4AC}",            // 💬 speech bubble
            _ => {}
        }
    }
    "\u{1F4C4}" // 📄 document (default)
}

/// Per sidebar_views.allium PostDateFormat: locale-formatted short month,
/// numeric day, and numeric year (for example, "Feb 10, 2026" in en-US).
fn format_post_date(locale: UiLocale, unix_ms: i64) -> String {
    let timestamp = chrono::DateTime::from_timestamp_millis(unix_ms)
        .unwrap_or_default()
        .with_timezone(&chrono::Local);
    let pattern = match locale {
        UiLocale::En => "%b %-d, %Y",
        UiLocale::De => "%-d. %b. %Y",
        UiLocale::Fr | UiLocale::It | UiLocale::Es => "%-d %b %Y",
    };

    timestamp
        .format_localized(pattern, format_locale(locale))
        .to_string()
}

/// Search input style.
fn search_input_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
    inputs::field_style(theme, status)
}

/// Style for filter toggle / clear buttons in the header.
fn filter_button_style(_theme: &Theme, status: button::Status) -> button::Style {
    let bg = match status {
        button::Status::Hovered => inputs::BORDER_COLOR,
        _ => Color::TRANSPARENT,
    };
    button::Style {
        background: Some(Background::Color(bg)),
        text_color: Color::from_rgb(0.60, 0.60, 0.65),
        border: Border {
            radius: 6.0.into(),
            ..Border::default()
        },
        ..button::Style::default()
    }
}

/// Active filter toggle button.
fn filter_button_active_style(_theme: &Theme, status: button::Status) -> button::Style {
    let bg = match status {
        button::Status::Hovered => Color::from_rgb(0.25, 0.30, 0.40),
        _ => Color::from_rgb(0.20, 0.25, 0.35),
    };
    button::Style {
        background: Some(Background::Color(bg)),
        text_color: Color::from_rgb(0.55, 0.70, 0.95),
        border: Border {
            radius: 6.0.into(),
            ..Border::default()
        },
        ..button::Style::default()
    }
}

/// Tag/category chip style (unselected).
fn chip_style(_theme: &Theme, status: button::Status) -> button::Style {
    let bg = match status {
        button::Status::Hovered => inputs::HOVER_BG,
        _ => inputs::ELEVATED_BG,
    };
    button::Style {
        background: Some(Background::Color(bg)),
        text_color: Color::from_rgb(0.70, 0.70, 0.75),
        border: Border {
            color: Color::from_rgb(0.30, 0.30, 0.35),
            width: 1.0,
            radius: 10.0.into(),
        },
        ..button::Style::default()
    }
}

/// Tag/category chip style (selected).
fn chip_selected_style(_theme: &Theme, status: button::Status) -> button::Style {
    let bg = match status {
        button::Status::Hovered => Color::from_rgb(0.25, 0.35, 0.55),
        _ => Color::from_rgb(0.20, 0.30, 0.50),
    };
    button::Style {
        background: Some(Background::Color(bg)),
        text_color: Color::from_rgb(0.85, 0.90, 1.0),
        border: Border {
            color: Color::from_rgb(0.40, 0.55, 0.80),
            width: 1.0,
            radius: 10.0.into(),
        },
        ..button::Style::default()
    }
}

/// Calendar year/month button style.
fn calendar_style(_theme: &Theme, status: button::Status) -> button::Style {
    let bg = match status {
        button::Status::Hovered => inputs::HOVER_BG,
        _ => Color::TRANSPARENT,
    };
    button::Style {
        background: Some(Background::Color(bg)),
        text_color: Color::from_rgb(0.70, 0.70, 0.75),
        border: Border::default(),
        ..button::Style::default()
    }
}

/// Calendar year/month button style (selected).
fn calendar_selected_style(_theme: &Theme, status: button::Status) -> button::Style {
    let bg = match status {
        button::Status::Hovered => Color::from_rgb(0.25, 0.30, 0.40),
        _ => Color::from_rgb(0.20, 0.25, 0.35),
    };
    button::Style {
        background: Some(Background::Color(bg)),
        text_color: Color::from_rgb(0.55, 0.70, 0.95),
        border: Border::default(),
        ..button::Style::default()
    }
}

/// "Clear All Filters" button style.
fn clear_filters_style(_theme: &Theme, status: button::Status) -> button::Style {
    let color = match status {
        button::Status::Hovered => Color::from_rgb(0.90, 0.50, 0.50),
        _ => Color::from_rgb(0.75, 0.45, 0.45),
    };
    button::Style {
        background: Some(Background::Color(Color::TRANSPARENT)),
        text_color: color,
        border: Border::default(),
        ..button::Style::default()
    }
}

/// Row delete button (×) revealed on row hover; turns red on button hover.
/// Per sidebar_views.allium ScriptListItemEntry RowLayout.
fn row_delete_style(_theme: &Theme, status: button::Status) -> button::Style {
    let text_color = match status {
        button::Status::Hovered | button::Status::Pressed => Color::from_rgb(0.90, 0.30, 0.30),
        _ => Color::from_rgb(0.60, 0.60, 0.65),
    };
    button::Style {
        background: Some(Background::Color(inputs::HOVER_BG)),
        text_color,
        border: Border {
            radius: 4.0.into(),
            ..Border::default()
        },
        ..button::Style::default()
    }
}

/// Wrap a sidebar row in a hover overlay carrying its delete button (×).
/// Per sidebar_views.allium *ListItemEntry RowLayout: right-aligned, visible
/// only on row hover, routed to the row's delete message.
fn with_row_delete(
    open_button: impl Into<Element<'static, Message>>,
    on_delete: Message,
) -> Element<'static, Message> {
    let delete_button = keyboard::button(
        text("\u{2715}") // ✕
            .size(11)
            .shaping(Shaping::Advanced),
    )
    .on_press(on_delete)
    .padding([2, 6])
    .style(row_delete_style);
    iced::widget::hover(
        open_button.into(),
        container(delete_button)
            .width(Length::Fill)
            .height(Length::Fill)
            .align_x(iced::alignment::Horizontal::Right)
            .align_y(iced::alignment::Vertical::Center)
            .padding([0, 4]),
    )
}

/// Locale-formatted short month name for calendar display.
fn month_abbr(locale: UiLocale, month: u32) -> String {
    chrono::NaiveDate::from_ymd_opt(2000, month, 1)
        .map(|date| {
            date.format_localized("%b", format_locale(locale))
                .to_string()
        })
        .unwrap_or_default()
}

/// Build the calendar archive tree widget.
fn calendar_widget(
    years: &[CalendarYear],
    selected_year: Option<i32>,
    selected_month: Option<u32>,
    on_year: impl Fn(Option<i32>) -> Message + 'static + Clone,
    on_month: impl Fn(Option<u32>) -> Message + 'static + Clone,
    locale: UiLocale,
) -> Element<'static, Message> {
    let muted = Color::from_rgb(0.50, 0.50, 0.55);
    let mut items: Vec<Element<'static, Message>> = Vec::new();

    let section_label = text(t(locale, "sidebar.filter.calendar"))
        .size(10)
        .shaping(Shaping::Advanced)
        .color(muted);
    items.push(section_label.into());

    for cy in years {
        let year_selected = selected_year == Some(cy.year);
        let total: usize = cy.months.iter().map(|m| m.count).sum();
        let label = format!("{} ({})", cy.year, total);
        let year_val = cy.year;
        let on_year_clone = on_year.clone();
        let style_fn = if year_selected {
            calendar_selected_style
        } else {
            calendar_style
        };
        let year_btn = keyboard::button(text(label).size(11).shaping(Shaping::Advanced))
            .on_press(if year_selected {
                on_year_clone(None)
            } else {
                on_year_clone(Some(year_val))
            })
            .padding([2, 4])
            .style(style_fn);
        items.push(year_btn.into());

        // Show months only when this year is selected
        if year_selected {
            for cm in &cy.months {
                let month_selected = selected_month == Some(cm.month);
                let label = format!("  {} ({})", month_abbr(locale, cm.month), cm.count);
                let month_val = cm.month;
                let on_month_clone = on_month.clone();
                let style_fn = if month_selected {
                    calendar_selected_style
                } else {
                    calendar_style
                };
                let month_btn = keyboard::button(text(label).size(10).shaping(Shaping::Advanced))
                    .on_press(if month_selected {
                        on_month_clone(None)
                    } else {
                        on_month_clone(Some(month_val))
                    })
                    .padding([1, 4])
                    .style(style_fn);
                items.push(month_btn.into());
            }
        }
    }

    iced::widget::Column::with_children(items).spacing(1).into()
}

/// Build chip selector for tags or categories.
fn chip_selector(
    label: &str,
    available: &[String],
    selected: &[String],
    on_toggle: impl Fn(String) -> Message + 'static + Clone,
    locale: UiLocale,
) -> Element<'static, Message> {
    let muted = Color::from_rgb(0.50, 0.50, 0.55);
    let mut items: Vec<Element<'static, Message>> = Vec::new();

    let section_label = text(t(locale, label))
        .size(10)
        .shaping(Shaping::Advanced)
        .color(muted);
    items.push(section_label.into());

    // Wrap chips in rows
    let mut chip_row: Vec<Element<'static, Message>> = Vec::new();
    for tag in available {
        let is_selected = selected.contains(tag);
        let tag_clone = tag.clone();
        let on_toggle_clone = on_toggle.clone();
        let style_fn = if is_selected {
            chip_selected_style
        } else {
            chip_style
        };
        let chip = keyboard::button(text(tag.clone()).size(10).shaping(Shaping::Advanced))
            .on_press(on_toggle_clone(tag_clone))
            .padding([2, 6])
            .style(style_fn);
        chip_row.push(chip.into());
    }

    if !chip_row.is_empty() {
        let mut collected: Vec<Element<'static, Message>> = chip_row.into_iter().collect();
        while !collected.is_empty() {
            let chunk_size = 3.min(collected.len());
            let chunk: Vec<Element<'static, Message>> = collected.drain(..chunk_size).collect();
            items.push(iced::widget::Row::with_children(chunk).spacing(4).into());
        }
    }

    iced::widget::Column::with_children(items).spacing(2).into()
}

fn single_select_chip_selector(
    label: &str,
    available: &[(String, String)],
    selected: Option<&str>,
    on_toggle: impl Fn(Option<String>) -> Message + 'static + Clone,
    locale: UiLocale,
) -> Element<'static, Message> {
    let muted = Color::from_rgb(0.50, 0.50, 0.55);
    let mut items: Vec<Element<'static, Message>> = Vec::new();

    items.push(
        text(t(locale, label))
            .size(10)
            .shaping(Shaping::Advanced)
            .color(muted)
            .into(),
    );

    let chips = available
        .iter()
        .map(|(value, display)| {
            let is_selected = selected == Some(value.as_str());
            let style_fn = if is_selected {
                chip_selected_style
            } else {
                chip_style
            };
            let value = value.clone();
            let on_toggle = on_toggle.clone();
            keyboard::button(text(display.clone()).size(10).shaping(Shaping::Advanced))
                .on_press(if is_selected {
                    on_toggle(None)
                } else {
                    on_toggle(Some(value))
                })
                .padding([2, 6])
                .style(style_fn)
                .into()
        })
        .collect::<Vec<Element<'static, Message>>>();

    if !chips.is_empty() {
        items.push(row(chips).spacing(4).into());
    }

    iced::widget::Column::with_children(items).spacing(2).into()
}

/// Build the filter panel for Posts/Pages view.
fn post_filter_panel(
    filter: &PostFilter,
    is_pages: bool,
    locale: UiLocale,
) -> Element<'static, Message> {
    let mut sections: Vec<Element<'static, Message>> = Vec::new();

    sections.push(single_select_chip_selector(
        "sidebar.filter.status",
        &[
            ("draft".to_string(), t(locale, "sidebar.drafts")),
            ("published".to_string(), t(locale, "sidebar.published")),
            ("archived".to_string(), t(locale, "sidebar.archived")),
        ],
        filter.status_filter.as_deref(),
        Message::SetPostStatusFilter,
        locale,
    ));
    sections.push(Space::new().height(4.0).into());

    if !filter.available_languages.is_empty() {
        let languages = filter
            .available_languages
            .iter()
            .map(|lang| (lang.clone(), lang.to_uppercase()))
            .collect::<Vec<_>>();
        sections.push(single_select_chip_selector(
            "sidebar.filter.language",
            &languages,
            filter.language_filter.as_deref(),
            Message::SetPostLanguageFilter,
            locale,
        ));
        sections.push(Space::new().height(4.0).into());
    }

    // Calendar archive
    if !filter.calendar_years.is_empty() {
        sections.push(calendar_widget(
            &filter.calendar_years,
            filter.calendar.selected_year,
            filter.calendar.selected_month,
            Message::SetPostCalendarYear,
            Message::SetPostCalendarMonth,
            locale,
        ));
        sections.push(Space::new().height(4.0).into());
    }

    // Tag chips
    if !filter.available_tags.is_empty() {
        sections.push(chip_selector(
            "sidebar.filter.tags",
            &filter.available_tags,
            &filter.tag_filter,
            Message::TogglePostTagFilter,
            locale,
        ));
        sections.push(Space::new().height(4.0).into());
    }

    // Category chips (not shown for Pages since Pages IS a category filter)
    if !is_pages && !filter.available_categories.is_empty() {
        sections.push(chip_selector(
            "sidebar.filter.categories",
            &filter.available_categories,
            &filter.category_filter,
            Message::TogglePostCategoryFilter,
            locale,
        ));
        sections.push(Space::new().height(4.0).into());
    }

    sections.push(
        iced::widget::Column::with_children(vec![
            text(t(locale, "sidebar.filter.dateRange"))
                .size(10)
                .shaping(Shaping::Advanced)
                .color(Color::from_rgb(0.50, 0.50, 0.55))
                .into(),
            row![
                text_input(&t(locale, "sidebar.filter.from"), &filter.from_date)
                    .on_input(Message::SetPostFromDate)
                    .size(10)
                    .padding([3, 5])
                    .style(search_input_style),
                text_input(&t(locale, "sidebar.filter.to"), &filter.to_date)
                    .on_input(Message::SetPostToDate)
                    .size(10)
                    .padding([3, 5])
                    .style(search_input_style),
            ]
            .spacing(4)
            .into(),
        ])
        .spacing(2)
        .into(),
    );
    sections.push(Space::new().height(4.0).into());

    // Clear all filters button
    if filter.has_active_filters() {
        sections.push(
            keyboard::button(
                text(t(locale, "sidebar.filter.clearAll"))
                    .size(10)
                    .shaping(Shaping::Advanced),
            )
            .on_press(Message::ClearPostFilters)
            .padding([2, 4])
            .style(clear_filters_style)
            .into(),
        );
    }

    iced::widget::Column::with_children(sections)
        .spacing(2)
        .into()
}

/// Build the filter panel for Media view.
fn media_filter_panel(filter: &MediaFilter, locale: UiLocale) -> Element<'static, Message> {
    let mut sections: Vec<Element<'static, Message>> = Vec::new();

    if !filter.calendar_years.is_empty() {
        sections.push(calendar_widget(
            &filter.calendar_years,
            filter.calendar.selected_year,
            filter.calendar.selected_month,
            Message::SetMediaCalendarYear,
            Message::SetMediaCalendarMonth,
            locale,
        ));
        sections.push(Space::new().height(4.0).into());
    }

    if !filter.available_tags.is_empty() {
        sections.push(chip_selector(
            "sidebar.filter.tags",
            &filter.available_tags,
            &filter.tag_filter,
            Message::ToggleMediaTagFilter,
            locale,
        ));
        sections.push(Space::new().height(4.0).into());
    }

    if filter.has_active_filters() {
        sections.push(
            keyboard::button(
                text(t(locale, "sidebar.filter.clearAll"))
                    .size(10)
                    .shaping(Shaping::Advanced),
            )
            .on_press(Message::ClearMediaFilters)
            .padding([2, 4])
            .style(clear_filters_style)
            .into(),
        );
    }

    iced::widget::Column::with_children(sections)
        .spacing(2)
        .into()
}

#[expect(
    clippy::too_many_arguments,
    reason = "arguments are independent sidebar state slices"
)]
pub fn view(
    sidebar_view: SidebarView,
    posts: &[Post],
    media: &[Media],
    scripts: &[Script],
    templates: &[Template],
    imports: &[ImportDefinition],
    conversations: &[ChatConversation],
    post_filter: &PostFilter,
    media_filter: &MediaFilter,
    media_thumbs: &std::collections::HashMap<String, Option<PathBuf>>,
    posts_has_more: bool,
    media_has_more: bool,
    width: f32,
    active_tab: Option<&str>,
    locale: UiLocale,
    _data_dir: Option<&Path>,
    git_state: &GitUiState,
    offline_mode: bool,
) -> Element<'static, Message> {
    let header_text = t(locale, sidebar_view.i18n_key());
    let muted = Color::from_rgb(0.50, 0.50, 0.55);
    // Reference date for RelativeDateFormat's diff_days (sidebar_views.allium).
    let today = chrono::Local::now().date_naive();

    let header = text(header_text)
        .size(13)
        .shaping(Shaping::Advanced)
        .color(Color::from_rgb(0.85, 0.85, 0.90));

    let create_action = match sidebar_view {
        SidebarView::Posts => Some(Message::CreatePost),
        SidebarView::Pages => Some(Message::CreatePage),
        SidebarView::Media => Some(Message::CreateMedia),
        SidebarView::Scripts => Some(Message::CreateScript),
        SidebarView::Templates => Some(Message::CreateTemplate),
        SidebarView::Import => Some(Message::CreateImport),
        SidebarView::Chat => Some(Message::ChatCreate),
        _ => None,
    };

    let header_row: Element<'static, Message> = if let Some(action) = create_action {
        row![
            header,
            Space::new().width(Length::Fill),
            keyboard::button(
                text(t(locale, "common.add"))
                    .size(11)
                    .shaping(Shaping::Advanced)
            )
            .on_press(action)
            .padding([3, 8])
            .style(filter_button_style),
        ]
        .align_y(iced::Alignment::Center)
        .into()
    } else {
        header.into()
    };

    let body: Element<'static, Message> = match sidebar_view {
        SidebarView::Posts | SidebarView::Pages => {
            let is_pages = sidebar_view == SidebarView::Pages;
            let filter = post_filter;

            let mut top_items: Vec<Element<'static, Message>> = Vec::new();

            // Search input + filter toggle row
            let search = text_input(&t(locale, "sidebar.filter.search"), &filter.search_query)
                .on_input(Message::PostSearchChanged)
                .size(11)
                .padding([4, 6])
                .width(Length::Fill)
                .style(search_input_style);

            let has_filters = filter.has_active_filters();
            let toggle_style = if filter.filter_panel_visible || has_filters {
                filter_button_active_style
            } else {
                filter_button_style
            };
            let toggle_label = if has_filters {
                "\u{2B50}" // ⭐ indicates active filters
            } else {
                "\u{25BC}" // ▼ toggle icon
            };
            let filter_toggle =
                keyboard::button(text(toggle_label).size(11).shaping(Shaping::Advanced))
                    .on_press(Message::TogglePostFilterPanel)
                    .padding([4, 6])
                    .style(toggle_style);

            top_items.push(row![search, filter_toggle].spacing(4).into());

            // Filter panel (collapsible)
            if filter.filter_panel_visible {
                top_items.push(Space::new().height(4.0).into());
                top_items.push(post_filter_panel(filter, is_pages, locale));
                top_items.push(Space::new().height(4.0).into());
            }

            // Post list
            if posts.is_empty() {
                let key = if has_filters {
                    "sidebar.filter.noResults"
                } else {
                    placeholder_key(sidebar_view)
                };
                top_items.push(
                    text(t(locale, key))
                        .size(12)
                        .shaping(Shaping::Advanced)
                        .color(muted)
                        .into(),
                );
            } else {
                let section_header = |label: &str| -> Element<'static, Message> {
                    text(label.to_string())
                        .size(11)
                        .shaping(Shaping::Advanced)
                        .color(Color::from_rgb(0.55, 0.55, 0.60))
                        .into()
                };

                let make_post_item = |p: &Post| -> Element<'static, Message> {
                    let is_active = active_tab == Some(p.id.as_str());
                    let icon = post_type_icon(&p.categories);
                    let status_indicator = match p.status {
                        bds_core::model::PostStatus::Draft => "\u{25CB}",
                        bds_core::model::PostStatus::Published => "\u{25CF}",
                        bds_core::model::PostStatus::Archived => "\u{25A1}",
                    };
                    let date = format_post_date(locale, p.created_at);
                    let prefix_chars: usize = 5;
                    let text_px = width
                        - SIDEBAR_TEXT_OVERHEAD_PX
                        - (prefix_chars as f32 * AVG_CHAR_WIDTH_PX);
                    let display_title = truncate_to_fit(&p.title, text_px);
                    let label = format!("{icon} {status_indicator} {display_title}");
                    let label_text = text(label)
                        .size(12)
                        .shaping(Shaping::Advanced)
                        .wrapping(iced::widget::text::Wrapping::None);
                    let date_text = text(date)
                        .size(10)
                        .shaping(Shaping::Advanced)
                        .color(Color::from_rgb(0.50, 0.50, 0.55));
                    let style_fn = if is_active {
                        item_active_style
                    } else {
                        item_style
                    };
                    keyboard::button(
                        container(column![label_text, date_text].spacing(1))
                            .width(Length::Fill)
                            .clip(true),
                    )
                    .on_press(Message::OpenTab(Tab {
                        id: p.id.clone(),
                        tab_type: TabType::Post,
                        title: p.title.clone(),
                        is_transient: true,
                        is_dirty: false,
                    }))
                    .padding([5, 8])
                    .width(Length::Fill)
                    .style(style_fn)
                    .into()
                };

                // Draft section
                let drafts: Vec<&Post> = posts
                    .iter()
                    .filter(|p| p.status == bds_core::model::PostStatus::Draft)
                    .collect();
                if !drafts.is_empty() {
                    top_items.push(section_header(&t(locale, "sidebar.drafts")));
                    for p in &drafts {
                        top_items.push(make_post_item(p));
                    }
                    top_items.push(Space::new().height(6.0).into());
                }

                // Published section
                let published: Vec<&Post> = posts
                    .iter()
                    .filter(|p| p.status == bds_core::model::PostStatus::Published)
                    .collect();
                if !published.is_empty() {
                    top_items.push(section_header(&t(locale, "sidebar.published")));
                    for p in &published {
                        top_items.push(make_post_item(p));
                    }
                    top_items.push(Space::new().height(6.0).into());
                }

                // Archived section
                let archived: Vec<&Post> = posts
                    .iter()
                    .filter(|p| p.status == bds_core::model::PostStatus::Archived)
                    .collect();
                if !archived.is_empty() {
                    top_items.push(section_header(&t(locale, "sidebar.archived")));
                    for p in &archived {
                        top_items.push(make_post_item(p));
                    }
                }
            }

            iced::widget::Column::with_children(top_items)
                .spacing(1)
                .into()
        }
        SidebarView::Media => {
            let filter = media_filter;
            let mut top_items: Vec<Element<'static, Message>> = Vec::new();

            // Search input + filter toggle row
            let search = text_input(&t(locale, "sidebar.filter.search"), &filter.search_query)
                .on_input(Message::MediaSearchChanged)
                .size(11)
                .padding([4, 6])
                .width(Length::Fill)
                .style(search_input_style);

            let has_filters = filter.has_active_filters();
            let toggle_style = if filter.filter_panel_visible || has_filters {
                filter_button_active_style
            } else {
                filter_button_style
            };
            let toggle_label = if has_filters {
                "\u{2B50}" // ⭐ indicates active filters
            } else {
                "\u{25BC}" // ▼ toggle icon
            };
            let filter_toggle =
                keyboard::button(text(toggle_label).size(11).shaping(Shaping::Advanced))
                    .on_press(Message::ToggleMediaFilterPanel)
                    .padding([4, 6])
                    .style(toggle_style);

            top_items.push(row![search, filter_toggle].spacing(4).into());

            // Filter panel (collapsible)
            if filter.filter_panel_visible {
                top_items.push(Space::new().height(4.0).into());
                top_items.push(media_filter_panel(filter, locale));
                top_items.push(Space::new().height(4.0).into());
            }

            if media.is_empty() {
                let key = if has_filters {
                    "sidebar.filter.noResults"
                } else {
                    placeholder_key(sidebar_view)
                };
                top_items.push(
                    text(t(locale, key))
                        .size(12)
                        .shaping(Shaping::Advanced)
                        .color(muted)
                        .into(),
                );
            } else {
                let items: Vec<Element<'static, Message>> = media
                    .iter()
                    .map(|m| {
                        let is_active = active_tab == Some(m.id.as_str());
                        let display_name = match m.title.as_deref() {
                            Some(title) => truncate_media_title(title),
                            None => m.original_name.clone(),
                        };
                        let text_px = width - SIDEBAR_TEXT_OVERHEAD_PX - 48.0;
                        let display_name = truncate_to_fit(&display_name, text_px);
                        let style_fn = if is_active {
                            item_active_style
                        } else {
                            item_style
                        };

                        // Left: 40x40 thumbnail or file icon (pre-resolved, no filesystem I/O)
                        let thumb_path = media_thumbs.get(&m.id).and_then(|opt| opt.as_ref());
                        let left: Element<'static, Message> = if let Some(path) = thumb_path {
                            container(
                                image(path.to_string_lossy().to_string())
                                    .width(Length::Fixed(40.0))
                                    .height(Length::Fixed(40.0))
                                    .content_fit(iced::ContentFit::Cover),
                            )
                            .width(Length::Fixed(40.0))
                            .height(Length::Fixed(40.0))
                            .clip(true)
                            .style(thumbnail_container_style)
                            .into()
                        } else {
                            container(
                                text("\u{1F4C4}") // 📄 generic file icon
                                    .size(20)
                                    .shaping(Shaping::Advanced),
                            )
                            .width(Length::Fixed(40.0))
                            .height(Length::Fixed(40.0))
                            .align_x(iced::alignment::Horizontal::Center)
                            .align_y(iced::alignment::Vertical::Center)
                            .style(thumbnail_container_style)
                            .into()
                        };

                        // Right column: name + metadata line
                        let name_text = text(display_name.clone())
                            .size(12)
                            .shaping(Shaping::Advanced)
                            .wrapping(iced::widget::text::Wrapping::None);

                        let file_size = format_file_size(m.size);
                        let meta_label = match (m.width, m.height) {
                            (Some(w), Some(h)) => format!("{file_size} · {w}×{h}"),
                            _ => file_size,
                        };
                        let meta_text = text(meta_label)
                            .size(10)
                            .shaping(Shaping::Advanced)
                            .color(Color::from_rgb(0.50, 0.50, 0.55));

                        let right = column![name_text, meta_text].spacing(1);

                        let content = row![left, right]
                            .spacing(8)
                            .align_y(iced::Alignment::Center);

                        keyboard::button(container(content).width(Length::Fill).clip(true))
                            .on_press(Message::OpenTab(Tab {
                                id: m.id.clone(),
                                tab_type: TabType::Media,
                                title: display_name.clone(),
                                is_transient: true,
                                is_dirty: false,
                            }))
                            .padding([4, 6])
                            .width(Length::Fill)
                            .style(style_fn)
                            .into()
                    })
                    .collect();
                top_items.extend(items);
            }

            iced::widget::Column::with_children(top_items)
                .spacing(1)
                .into()
        }
        SidebarView::Scripts => {
            if scripts.is_empty() {
                text(t(locale, placeholder_key(sidebar_view)))
                    .size(12)
                    .shaping(Shaping::Advanced)
                    .color(muted)
                    .into()
            } else {
                let items: Vec<Element<'static, Message>> = scripts
                    .iter()
                    .map(|s| {
                        let is_active = active_tab == Some(s.id.as_str());
                        // Reserve room on the right for the hover-revealed delete button.
                        let text_px = width - SIDEBAR_TEXT_OVERHEAD_PX - 20.0;
                        let display_title = truncate_to_fit(&s.title, text_px);
                        let label_text = text(display_title.clone())
                            .size(12)
                            .shaping(Shaping::Advanced)
                            .wrapping(iced::widget::text::Wrapping::None);
                        // sidebar_views.allium ScriptListItemEntry RowLayout:
                        // line 2 is the date in relative format, smaller and muted.
                        let date_text = text(format_relative_date(locale, s.updated_at, today))
                            .size(10)
                            .shaping(Shaping::Advanced)
                            .color(muted);
                        let style_fn = if is_active {
                            item_active_style
                        } else {
                            item_style
                        };
                        let open_button = keyboard::button(
                            container(column![label_text, date_text].spacing(1))
                                .width(Length::Fill)
                                .clip(true),
                        )
                        .on_press(Message::OpenTab(Tab {
                            id: s.id.clone(),
                            tab_type: TabType::Scripts,
                            title: display_title,
                            is_transient: true,
                            is_dirty: false,
                        }))
                        .padding([5, 8])
                        .width(Length::Fill)
                        .style(style_fn);
                        with_row_delete(open_button, Message::ScriptDeleteRequested(s.id.clone()))
                    })
                    .collect();
                iced::widget::Column::with_children(items).spacing(1).into()
            }
        }
        SidebarView::Templates => {
            if templates.is_empty() {
                text(t(locale, placeholder_key(sidebar_view)))
                    .size(12)
                    .shaping(Shaping::Advanced)
                    .color(muted)
                    .into()
            } else {
                let items: Vec<Element<'static, Message>> = templates
                    .iter()
                    .map(|tmpl| {
                        let is_active = active_tab == Some(tmpl.id.as_str());
                        // Reserve room on the right for the hover-revealed delete button.
                        let text_px = width - SIDEBAR_TEXT_OVERHEAD_PX - 20.0;
                        let display_title = truncate_to_fit(&tmpl.title, text_px);
                        let label_text = text(display_title.clone())
                            .size(12)
                            .shaping(Shaping::Advanced)
                            .wrapping(iced::widget::text::Wrapping::None);
                        // sidebar_views.allium TemplateListItemEntry RowLayout:
                        // line 2 is the date in relative format, smaller and muted.
                        let date_text = text(format_relative_date(locale, tmpl.updated_at, today))
                            .size(10)
                            .shaping(Shaping::Advanced)
                            .color(muted);
                        let style_fn = if is_active {
                            item_active_style
                        } else {
                            item_style
                        };
                        let open_button = keyboard::button(
                            container(column![label_text, date_text].spacing(1))
                                .width(Length::Fill)
                                .clip(true),
                        )
                        .on_press(Message::OpenTab(Tab {
                            id: tmpl.id.clone(),
                            tab_type: TabType::Templates,
                            title: display_title,
                            is_transient: true,
                            is_dirty: false,
                        }))
                        .padding([5, 8])
                        .width(Length::Fill)
                        .style(style_fn);
                        with_row_delete(
                            open_button,
                            Message::TemplateDeleteRequested(tmpl.id.clone()),
                        )
                    })
                    .collect();
                iced::widget::Column::with_children(items).spacing(1).into()
            }
        }
        SidebarView::Import => {
            if imports.is_empty() {
                text(t(locale, placeholder_key(sidebar_view)))
                    .size(12)
                    .shaping(Shaping::Advanced)
                    .color(muted)
                    .into()
            } else {
                let items = imports
                    .iter()
                    .map(|definition| {
                        let definition_name = definition.name.clone();
                        let style = if active_tab == Some(definition.id.as_str()) {
                            item_active_style
                        } else {
                            item_style
                        };
                        // sidebar_views.allium ImportListItemEntry RowLayout:
                        // line 2 is the date in relative format, smaller and muted.
                        let date_text =
                            text(format_relative_date(locale, definition.updated_at, today))
                                .size(10)
                                .shaping(Shaping::Advanced)
                                .color(muted);
                        let open_button = keyboard::button(
                            container(
                                column![
                                    text(definition_name).size(12).shaping(Shaping::Advanced),
                                    date_text,
                                ]
                                .spacing(1),
                            )
                            .width(Length::Fill),
                        )
                        .on_press(Message::OpenTab(Tab {
                            id: definition.id.clone(),
                            tab_type: TabType::Import,
                            title: definition.name.clone(),
                            is_transient: true,
                            is_dirty: false,
                        }))
                        .padding([5, 8])
                        .width(Length::Fill)
                        .style(style);
                        with_row_delete(
                            open_button,
                            Message::ImportDeleteRequested(definition.id.clone()),
                        )
                    })
                    .collect::<Vec<Element<'static, Message>>>();
                iced::widget::Column::with_children(items).spacing(1).into()
            }
        }
        SidebarView::Chat => {
            if conversations.is_empty() {
                text(t(locale, "chat.sidebar.empty"))
                    .size(12)
                    .shaping(Shaping::Advanced)
                    .color(muted)
                    .into()
            } else {
                let items = conversations
                    .iter()
                    .map(|conversation| {
                        let style = if active_tab == Some(conversation.id.as_str()) {
                            item_active_style
                        } else {
                            item_style
                        };
                        // sidebar_views.allium ChatListItemEntry RowLayout:
                        // line 2 is the date in relative format, smaller and muted.
                        let date_text =
                            text(format_relative_date(locale, conversation.updated_at, today))
                                .size(10)
                                .shaping(Shaping::Advanced)
                                .color(muted);
                        let open_button = keyboard::button(
                            container(
                                column![
                                    text(conversation.title.clone())
                                        .size(12)
                                        .shaping(Shaping::Advanced),
                                    date_text,
                                ]
                                .spacing(1),
                            )
                            .width(Length::Fill),
                        )
                        .on_press(Message::OpenTab(Tab {
                            id: conversation.id.clone(),
                            tab_type: TabType::Chat,
                            title: conversation.title.clone(),
                            is_transient: false,
                            is_dirty: false,
                        }))
                        .padding([5, 8])
                        .width(Length::Fill)
                        .style(style);
                        // Per action_patterns.allium confirmation_assignments chat
                        // delete has no confirmation; ChatDelete removes the
                        // conversation and closes its tab.
                        with_row_delete(open_button, Message::ChatDelete(conversation.id.clone()))
                    })
                    .collect::<Vec<Element<'static, Message>>>();
                iced::widget::Column::with_children(items).spacing(1).into()
            }
        }
        SidebarView::Settings => {
            // Per sidebar_views.allium SettingsNav: 9 fixed-order sections
            use crate::views::settings_view::SettingsSection;
            let sections: &[(&str, Option<SettingsSection>)] = &[
                ("settings.nav.project", Some(SettingsSection::Project)),
                ("settings.nav.editor", Some(SettingsSection::Editor)),
                ("settings.nav.ai", Some(SettingsSection::AI)),
                ("settings.nav.technology", Some(SettingsSection::Technology)),
                ("settings.nav.publishing", Some(SettingsSection::Publishing)),
                ("settings.nav.data", Some(SettingsSection::Data)),
                ("settings.nav.mcp", Some(SettingsSection::MCP)),
                ("settings.nav.style", None),
            ];
            let items: Vec<Element<'static, Message>> = sections
                .iter()
                .map(|(key, section_opt)| {
                    let label = t(locale, key);
                    let label_text = text(label).size(12).shaping(Shaping::Advanced);
                    let msg = if let Some(section) = section_opt {
                        Message::OpenSettingsSection(section.clone())
                    } else {
                        Message::OpenTab(Tab {
                            id: "style".to_string(),
                            tab_type: TabType::Style,
                            title: t(locale, "tabBar.style"),
                            is_transient: false,
                            is_dirty: false,
                        })
                    };
                    keyboard::button(container(label_text).width(Length::Fill))
                        .on_press(msg)
                        .padding([5, 8])
                        .width(Length::Fill)
                        .style(item_style)
                        .into()
                })
                .collect();
            iced::widget::Column::with_children(items).spacing(1).into()
        }
        SidebarView::Tags => {
            let sections = [
                (
                    "tags.nav.cloud",
                    crate::views::tags_view::TagsSection::Cloud,
                ),
                (
                    "tags.nav.manage",
                    crate::views::tags_view::TagsSection::Manage,
                ),
                (
                    "tags.nav.merge",
                    crate::views::tags_view::TagsSection::Merge,
                ),
                (
                    "tags.nav.categories",
                    crate::views::tags_view::TagsSection::Categories,
                ),
            ];
            let items: Vec<Element<'static, Message>> = sections
                .iter()
                .map(|(key, section)| {
                    let label = t(locale, key);
                    let label_text = text(label).size(12).shaping(Shaping::Advanced);
                    keyboard::button(container(label_text).width(Length::Fill))
                        .on_press(Message::OpenTagsSection(*section))
                        .padding([5, 8])
                        .width(Length::Fill)
                        .style(item_style)
                        .into()
                })
                .collect();
            iced::widget::Column::with_children(items).spacing(1).into()
        }
        SidebarView::Git => crate::views::git::sidebar_view(git_state, offline_mode, locale),
    };

    let content = column![header_row, Space::new().height(8.0), body,]
        .spacing(4)
        .padding(12);

    // layout.allium: sidebar width is resizable, passed as parameter
    let mut scrolling = scrollable(content)
        .direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
        .style(inputs::scrollable_style);
    if matches!(sidebar_view, SidebarView::Posts | SidebarView::Pages) && posts_has_more
        || sidebar_view == SidebarView::Media && media_has_more
    {
        scrolling =
            scrolling.on_scroll(|viewport| Message::SidebarScrolled(viewport.relative_offset().y));
    }

    container(scrolling)
        .width(Length::Fixed(width))
        .height(Length::Fill)
        .style(sidebar_style)
        .into()
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::{Local, TimeZone};

    fn local_ms(year: i32, month: u32, day: u32) -> i64 {
        Local
            .with_ymd_and_hms(year, month, day, 12, 0, 0)
            .single()
            .expect("unambiguous local time")
            .timestamp_millis()
    }

    #[test]
    fn post_date_uses_locale_order_month_name_and_punctuation() {
        let timestamp = local_ms(2026, 2, 10);

        assert_eq!(format_post_date(UiLocale::En, timestamp), "Feb 10, 2026");
        assert_eq!(format_post_date(UiLocale::De, timestamp), "10. Feb. 2026");
        assert_eq!(format_post_date(UiLocale::Fr, timestamp), "10 févr. 2026");
        assert_eq!(format_post_date(UiLocale::It, timestamp), "10 feb 2026");
        assert_eq!(format_post_date(UiLocale::Es, timestamp), "10 feb 2026");
    }

    #[test]
    fn calendar_month_abbreviations_use_ui_locale() {
        assert_eq!(month_abbr(UiLocale::En, 3), "Mar");
        assert_eq!(month_abbr(UiLocale::De, 3), "Mär");
        assert_eq!(month_abbr(UiLocale::Fr, 2), "févr.");
        assert_eq!(month_abbr(UiLocale::It, 1), "gen");
        assert_eq!(month_abbr(UiLocale::Es, 1), "ene");
    }

    #[test]
    fn truncate_media_title_short() {
        assert_eq!(truncate_media_title("short title"), "short title");
    }

    #[test]
    fn truncate_media_title_exact_60() {
        let title: String = "a".repeat(60);
        assert_eq!(truncate_media_title(&title), title);
    }

    #[test]
    fn truncate_media_title_over_60() {
        let title: String = "a".repeat(65);
        let expected = format!("{}...", "a".repeat(60));
        assert_eq!(truncate_media_title(&title), expected);
    }

    #[test]
    fn truncate_media_title_unicode() {
        // 61 Unicode chars should trigger truncation
        let title: String = "\u{00FC}".repeat(61); // ü × 61
        let expected = format!("{}...", "\u{00FC}".repeat(60));
        assert_eq!(truncate_media_title(&title), expected);
    }

    #[test]
    fn truncate_to_fit_short() {
        // 100px at ~6.8px/char ≈ 14 chars; "Hello" fits.
        assert_eq!(truncate_to_fit("Hello", 100.0), "Hello");
    }

    #[test]
    fn truncate_to_fit_long() {
        // 50px at ~6.8px/char ≈ 7 chars; 20-char string truncated.
        let result = truncate_to_fit(&"a".repeat(20), 50.0);
        assert!(result.ends_with('\u{2026}'));
        assert!(result.chars().count() <= 8);
    }

    #[test]
    fn truncate_to_fit_narrow() {
        // Very narrow (10px): minimum 6 chars enforced.
        let result = truncate_to_fit(&"a".repeat(20), 10.0);
        assert!(result.ends_with('\u{2026}'));
        assert!(result.chars().count() >= 2);
    }

    #[test]
    fn format_file_size_bytes() {
        assert_eq!(format_file_size(0), "0 B");
        assert_eq!(format_file_size(512), "512 B");
        assert_eq!(format_file_size(1023), "1023 B");
    }

    #[test]
    fn format_file_size_kilobytes() {
        assert_eq!(format_file_size(1024), "1.0 KB");
        assert_eq!(format_file_size(1536), "1.5 KB");
        assert_eq!(format_file_size(1024 * 1024 - 1), "1024.0 KB");
    }

    #[test]
    fn format_file_size_megabytes() {
        assert_eq!(format_file_size(1024 * 1024), "1.0 MB");
        assert_eq!(format_file_size(5 * 1024 * 1024), "5.0 MB");
    }
}
