use crate::{
    components::collapsible::{self, collapsible},
    components::icons::{StaticIcon, icon, icon_button},
    components::scrollable,
    components::slide::{self, SlideDirection, slide},
    components::{ButtonHierarchy, ButtonKind, ButtonSize, MenuSize},
    config::{NotificationsModuleConfig, Surface, ToastPosition},
    services::{
        ReadOnlyService, ServiceEvent,
        notifications::{
            Notification, NotificationIcon, NotificationsService, Urgency,
            dbus::{CloseGuard, NotificationDaemon, NotificationEvent},
        },
    },
    t,
    theme::{Paint, surface_border, use_theme},
};
use chrono::{DateTime, Local};
use iced::{
    Alignment, Border, Column, Element, Length, Padding, Row, Size, Subscription, Task, Theme,
    widget::{Space, blur, button, column, container, image, row, sensor, svg, text},
};
use itertools::Itertools;
use log::error;
use std::{
    collections::{HashMap, HashSet, VecDeque},
    time::{Duration, Instant},
};
use zbus::Connection;

const ICON_SIZE: f32 = 36.0;

fn notification_icon<'a, M: 'a>(icon_kind: Option<&NotificationIcon>) -> Element<'a, M> {
    match icon_kind {
        Some(NotificationIcon::Svg(handle)) => svg(handle.clone())
            .width(Length::Fixed(ICON_SIZE))
            .height(Length::Fixed(ICON_SIZE))
            .into(),
        Some(NotificationIcon::Image(handle)) => image(handle.clone())
            .width(Length::Fixed(ICON_SIZE))
            .height(Length::Fixed(ICON_SIZE))
            .into(),
        None => icon(StaticIcon::Bell).size(ICON_SIZE).into(),
    }
}

fn invoke_and_close_task(
    connection: Option<Connection>,
    id: u32,
    action_key: Option<String>,
    guard: CloseGuard,
) -> Task<Message> {
    Task::perform(
        async move {
            if let Some(connection) = connection
                && let Err(e) =
                    NotificationDaemon::invoke_and_close(&connection, id, action_key, guard).await
            {
                error!("Failed to close notification id {}: {}", id, e);
            }
        },
        |_| Message::NotificationClosed,
    )
}

async fn close_notification_ids(connection: Option<Connection>, notification_ids: &[u32]) {
    if let Some(connection) = connection {
        for id in notification_ids {
            if let Err(e) =
                NotificationDaemon::invoke_and_close(&connection, *id, None, CloseGuard::Any).await
            {
                error!("Failed to close notification id {}: {}", id, e);
            }
        }
    }
}

fn delayed_toast_message(delay: Duration, id: u32, msg: fn(u32) -> Message) -> Task<Message> {
    Task::perform(
        async move {
            tokio::time::sleep(delay).await;
            id
        },
        msg,
    )
}

fn toast_timeout(required_timeout: i32, timeout_ms: u64) -> Option<Duration> {
    match required_timeout {
        -1 => Some(Duration::from_millis(timeout_ms)),
        0 => None,
        required if required > 0 => Some(Duration::from_millis(required as u64)),
        _ => None,
    }
}

#[derive(Debug, Clone)]
pub enum Message {
    ConfigReloaded(NotificationsModuleConfig),
    NotificationClicked(u32),
    NotificationClosed,
    CloseNotificationById(u32),
    ClearNotifications,
    NotificationsCleared,
    ClearGroup(String),
    GroupCleared(String, Vec<u32>),
    Event(ServiceEvent<NotificationsService>),
    ToggleGroup(String),
    ExpireToast(u32),
    DismissToast(u32),
    StartCollapse(u32),
    DismissAnimationComplete(u32),
    ToastResized(Size),
}

#[derive(Debug, PartialEq)]
pub enum NotificationStyle {
    Toast,
    Standalone,
    GroupHeader,
    GroupItem,
    GroupLast,
}

pub enum Action {
    None,
    Task(Task<Message>),
    Show(Task<Message>),
    Hide(Task<Message>),
    UpdateToastInputRegion(Size),
}

// Must match the widget durations so task delays align with animation end.
const SLIDE_ANIMATION: Duration = slide::DEFAULT_DURATION;
const COLLAPSE_ANIMATION: Duration = collapsible::DEFAULT_DURATION;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DismissPhase {
    Sliding,
    Collapsing,
}

pub struct Notifications {
    config: NotificationsModuleConfig,
    connection: Option<Connection>,
    notifications: VecDeque<Notification>,
    expanded_groups: HashSet<String>,
    blocklist: Vec<crate::config::RegexCfg>,
    toasts: VecDeque<u32>,
    dismiss_phases: HashMap<u32, DismissPhase>,
    toast_timers: HashMap<u32, Instant>,
    animations_enabled: bool,
}

impl Notifications {
    pub fn new(config: NotificationsModuleConfig, animations_enabled: bool) -> Self {
        let blocklist = config.blocklist.clone();
        Self {
            config,
            connection: None,
            notifications: VecDeque::new(),
            expanded_groups: HashSet::new(),
            blocklist,
            toasts: VecDeque::new(),
            dismiss_phases: HashMap::new(),
            toast_timers: HashMap::new(),
            animations_enabled,
        }
    }

    pub fn set_animations_enabled(&mut self, enabled: bool) {
        self.animations_enabled = enabled;
    }

    fn is_blocklisted(&self, app_name: &str) -> bool {
        self.blocklist
            .iter()
            .any(|pattern| pattern.is_match(app_name))
    }

    fn find_notification(&self, id: u32) -> Option<&Notification> {
        self.notifications.iter().find(|n| n.id == id)
    }

    fn close_guard(&self, id: u32) -> CloseGuard {
        self.find_notification(id)
            .map_or(CloseGuard::Any, |n| CloseGuard::Revision(n.revision))
    }

    fn find_first_action_key(&self, id: u32) -> Option<String> {
        self.find_notification(id)
            .filter(|n| !n.actions.is_empty())
            .and_then(|n| n.actions.first())
            .cloned()
    }

    fn clear_toasts(&mut self) -> bool {
        let had_toasts = !self.toasts.is_empty();
        self.toasts.clear();
        self.dismiss_phases.clear();
        self.toast_timers.clear();
        had_toasts
    }

    fn remove_toast(&mut self, id: u32) -> bool {
        let had_toasts = !self.toasts.is_empty();
        self.toasts.retain(|&toast_id| toast_id != id);
        self.toast_timers.remove(&id);
        had_toasts
    }

    fn remove_toasts(&mut self, ids: &[u32]) -> bool {
        let had_toasts = !self.toasts.is_empty();
        let ids: HashSet<u32> = ids.iter().copied().collect();
        self.toasts.retain(|toast_id| !ids.contains(toast_id));
        self.toast_timers.retain(|id, _| !ids.contains(id));
        had_toasts
    }

    fn notification_ids_for_app(&self, app_name: &str) -> Vec<u32> {
        self.notifications
            .iter()
            .filter(|notification| notification.app_name == app_name)
            .map(|notification| notification.id)
            .collect()
    }

    fn hide_toasts_if_empty(&self, had_toasts: bool) -> Action {
        if had_toasts && self.toasts.is_empty() {
            Action::Hide(Task::none())
        } else {
            Action::None
        }
    }

    fn hide_toasts_if_empty_with_task(&self, had_toasts: bool, task: Task<Message>) -> Action {
        if had_toasts && self.toasts.is_empty() {
            Action::Hide(task)
        } else {
            Action::Task(task)
        }
    }

    fn toast_action_for_update_event(&mut self, update_event: &NotificationEvent) -> Action {
        if !self.config.toast {
            return Action::None;
        }

        match update_event {
            NotificationEvent::Received(notification) => {
                if self.config.toast_limit == 0 {
                    let had_toasts = self.clear_toasts();
                    return self.hide_toasts_if_empty(had_toasts);
                }

                if !self.toasts.contains(&notification.id) {
                    while self.toasts.len() >= self.config.toast_limit
                        && let Some(evicted) = self.toasts.pop_front()
                    {
                        self.dismiss_phases.remove(&evicted);
                        self.toast_timers.remove(&evicted);
                    }
                    self.toasts.push_back(notification.id);
                }

                // A replace cancels an in-flight dismissal so the replacement is
                // shown instead of being swallowed by the slide-out.
                self.dismiss_phases.remove(&notification.id);

                // Critical notifications are persistent per the freedesktop
                // spec: they must be acknowledged by the user.
                let timeout = if notification.urgency == Urgency::Critical {
                    None
                } else {
                    toast_timeout(notification.expire_timeout, self.config.toast_timeout)
                };

                let task = if let Some(timeout) = timeout {
                    let deadline = Instant::now() + timeout;
                    let previous = self.toast_timers.insert(notification.id, deadline);
                    // Every wakeup either expires the toast, dropping the
                    // deadline, or reschedules itself, so a stored deadline always
                    // has a sleep in flight: one is only needed when there was no
                    // deadline, or when the new one lands before the pending wakeup.
                    if previous.is_none_or(|previous| deadline < previous) {
                        delayed_toast_message(timeout, notification.id, Message::ExpireToast)
                    } else {
                        Task::none()
                    }
                } else {
                    self.toast_timers.remove(&notification.id);
                    Task::none()
                };

                Action::Show(task)
            }
            NotificationEvent::Closed(id) => {
                let id = *id;
                if self.dismiss_phases.contains_key(&id) {
                    return Action::None;
                }
                let was_showing = self.remove_toast(id);
                self.hide_toasts_if_empty(was_showing)
            }
        }
    }

    fn apply_update_event(&mut self, update_event: NotificationEvent) {
        match update_event {
            NotificationEvent::Received(notification) => {
                self.notifications.retain(|n| n.id != notification.id);
                self.notifications.push_front(*notification);
            }
            NotificationEvent::Closed(id) => {
                self.notifications.retain(|n| n.id != id);
            }
        }
    }

    pub fn update(&mut self, message: Message) -> Action {
        match message {
            Message::ConfigReloaded(config) => {
                let hide = !config.toast && self.config.toast && !self.toasts.is_empty();
                self.blocklist = config.blocklist.clone();
                self.config = config;
                if hide {
                    self.clear_toasts();
                    Action::Hide(Task::none())
                } else {
                    Action::None
                }
            }
            Message::Event(event) => match event {
                ServiceEvent::Init(service) => {
                    self.connection = Some(service.connection);
                    Action::None
                }
                ServiceEvent::Update(update_event) => {
                    if let NotificationEvent::Received(notification) = &update_event
                        && self.is_blocklisted(&notification.app_name)
                    {
                        return Action::None;
                    }

                    let toast_action = self.toast_action_for_update_event(&update_event);
                    self.apply_update_event(update_event);

                    toast_action
                }
                ServiceEvent::Error(_) => Action::None,
            },
            Message::NotificationClicked(id) => {
                let connection = self.connection.clone();
                let action_key = self.find_first_action_key(id);
                let guard = self.close_guard(id);
                Action::Task(invoke_and_close_task(connection, id, action_key, guard))
            }
            Message::NotificationClosed => Action::None,
            Message::ClearNotifications => {
                let connection = self.connection.clone();
                let notification_ids: Vec<u32> = self.notifications.iter().map(|n| n.id).collect();

                Action::Task(Task::perform(
                    async move {
                        close_notification_ids(connection, &notification_ids).await;
                    },
                    |_| Message::NotificationsCleared,
                ))
            }
            Message::NotificationsCleared => {
                let had_toasts = self.clear_toasts();
                self.hide_toasts_if_empty(had_toasts)
            }
            Message::ClearGroup(app_name) => {
                let connection = self.connection.clone();
                let notification_ids = self.notification_ids_for_app(&app_name);

                Action::Task(Task::perform(
                    async move {
                        close_notification_ids(connection, &notification_ids).await;
                        (app_name, notification_ids)
                    },
                    |(app_name, ids)| Message::GroupCleared(app_name, ids),
                ))
            }
            Message::GroupCleared(app_name, group_ids) => {
                self.expanded_groups.remove(&app_name);
                let had_toasts = self.remove_toasts(&group_ids);
                self.hide_toasts_if_empty(had_toasts)
            }
            Message::ToggleGroup(app_name) => {
                if !self.expanded_groups.remove(&app_name) {
                    self.expanded_groups.insert(app_name);
                }
                Action::None
            }
            Message::ExpireToast(id) => {
                let Some(&deadline) = self.toast_timers.get(&id) else {
                    return Action::None;
                };
                let now = Instant::now();
                if now < deadline {
                    // Replaced with a later deadline: wait out the remainder.
                    return Action::Task(delayed_toast_message(
                        deadline - now,
                        id,
                        Message::ExpireToast,
                    ));
                }
                self.toast_timers.remove(&id);

                if self.toasts.contains(&id) && !self.dismiss_phases.contains_key(&id) {
                    if !self.animations_enabled {
                        let had_toasts = self.remove_toast(id);
                        return self.hide_toasts_if_empty(had_toasts);
                    }
                    self.dismiss_phases.insert(id, DismissPhase::Sliding);
                    Action::Task(delayed_toast_message(
                        SLIDE_ANIMATION,
                        id,
                        Message::StartCollapse,
                    ))
                } else {
                    Action::None
                }
            }
            Message::CloseNotificationById(id) => {
                let connection = self.connection.clone();
                let guard = self.close_guard(id);
                let had_toasts = self.remove_toast(id);
                self.dismiss_phases.remove(&id);

                let task = invoke_and_close_task(connection, id, None, guard);
                self.hide_toasts_if_empty_with_task(had_toasts, task)
            }
            Message::DismissToast(id) => {
                if self.toasts.contains(&id) && !self.dismiss_phases.contains_key(&id) {
                    let connection = self.connection.clone();
                    let action_key = self.find_first_action_key(id);
                    let guard = self.close_guard(id);
                    let invoke_task = invoke_and_close_task(connection, id, action_key, guard);
                    if !self.animations_enabled {
                        let had_toasts = self.remove_toast(id);
                        let hide_action =
                            self.hide_toasts_if_empty_with_task(had_toasts, invoke_task);
                        return hide_action;
                    }
                    self.dismiss_phases.insert(id, DismissPhase::Sliding);
                    Action::Task(Task::batch(vec![
                        invoke_task,
                        delayed_toast_message(SLIDE_ANIMATION, id, Message::StartCollapse),
                    ]))
                } else {
                    Action::None
                }
            }
            Message::StartCollapse(id) => {
                if self.dismiss_phases.get(&id) == Some(&DismissPhase::Sliding) {
                    self.dismiss_phases.insert(id, DismissPhase::Collapsing);
                    Action::Task(delayed_toast_message(
                        COLLAPSE_ANIMATION,
                        id,
                        Message::DismissAnimationComplete,
                    ))
                } else {
                    Action::None
                }
            }
            Message::DismissAnimationComplete(id) => {
                if self.dismiss_phases.remove(&id).is_none() {
                    // Cancelled by a replace: the toast is on screen again.
                    return Action::None;
                }
                let had_toasts = self.remove_toast(id);
                self.hide_toasts_if_empty(had_toasts)
            }
            Message::ToastResized(size) => Action::UpdateToastInputRegion(size),
        }
    }

    fn format_timestamp(&self, timestamp: std::time::SystemTime) -> String {
        let datetime: DateTime<Local> = timestamp.into();
        datetime.format(&self.config.format).to_string()
    }

    fn notification_button_style(
        style: NotificationStyle,
        urgency: Urgency,
    ) -> impl Fn(&Theme, iced::widget::button::Status) -> iced::widget::button::Style + use<> {
        let radius = use_theme(|t| t.radius);
        move |iced_theme: &Theme, status| {
            let mut border = match style {
                NotificationStyle::Toast => surface_border(iced_theme, radius.lg),
                NotificationStyle::Standalone => Border::default().rounded(radius.lg),
                NotificationStyle::GroupHeader => Border::default().rounded(iced::border::Radius {
                    top_left: radius.lg,
                    top_right: radius.lg,
                    bottom_left: radius.sm,
                    bottom_right: radius.sm,
                }),
                NotificationStyle::GroupItem => Border::default().rounded(radius.sm),
                NotificationStyle::GroupLast => Border::default().rounded(iced::border::Radius {
                    top_left: 0.0,
                    top_right: 0.0,
                    bottom_left: radius.lg,
                    bottom_right: radius.lg,
                }),
            };

            if urgency == Urgency::Critical {
                border.width = 1.0;
                border.color = iced_theme.palette().danger;
            }

            let mut button_style = iced::widget::button::Style {
                text_color: iced_theme.palette().text,
                border,
                ..iced::widget::button::Style::default()
            };
            let bg = Paint::surface(
                iced_theme,
                if style == NotificationStyle::Toast {
                    iced_theme.extended_palette().background.base.color
                } else {
                    iced_theme.extended_palette().background.weak.color
                },
            );
            button_style.background = Some(
                if matches!(status, iced::widget::button::Status::Hovered) {
                    bg.hovered(iced_theme)
                } else {
                    bg
                }
                .into(),
            );
            button_style
        }
    }

    fn notification_card<'a>(
        &'a self,
        notification: &'a Notification,
        on_press: Message,
        toast: bool,
    ) -> Element<'a, Message> {
        let (space, font_size) = use_theme(|t| (t.space, t.font_size));
        let timestamp_element = if self.config.show_timestamps {
            Some(
                container(text(self.format_timestamp(notification.timestamp)).size(font_size.xs))
                    .padding([0., space.xxs]),
            )
        } else {
            None
        };

        let body_element = if (!toast || self.config.show_bodies) && !notification.body.is_empty() {
            Some(
                text(&notification.body)
                    .size(font_size.sm)
                    .wrapping(text::Wrapping::WordOrGlyph),
            )
        } else {
            None
        };

        let notification_id = notification.id;

        let app_icon_button = notification_icon(notification.icon.as_ref());

        let mut card = container(
            column!(
                Row::new()
                    .push(
                        Row::new()
                            .push(app_icon_button)
                            .push(column!(
                                text(&notification.app_name)
                                    .size(font_size.md)
                                    .wrapping(text::Wrapping::WordOrGlyph),
                                timestamp_element
                            ))
                            .width(Length::Fill)
                            .spacing(space.xxs)
                            .padding(space.xs)
                            .align_y(Alignment::Center),
                    )
                    .push(
                        icon_button(StaticIcon::Close)
                            .kind(ButtonKind::Transparent)
                            .hierarchy(ButtonHierarchy::Danger)
                            .on_press(Message::CloseNotificationById(notification_id))
                    ),
                column!(
                    text(&notification.summary).wrapping(text::Wrapping::WordOrGlyph),
                    body_element,
                )
                .spacing(space.xxs)
                .padding(Padding::new(space.xs).top(0.))
            )
            .spacing(space.xxs),
        );

        if toast {
            card = card.max_height(self.config.toast_max_height).clip(true);
        }

        button(card)
            .on_press(on_press)
            .width(Length::Fill)
            .padding(space.xxs)
            .style(Self::notification_button_style(
                if toast {
                    NotificationStyle::Toast
                } else {
                    NotificationStyle::Standalone
                },
                notification.urgency,
            ))
            .into()
    }

    fn group_item<'a>(
        &'a self,
        notification: &'a Notification,
        is_last: bool,
    ) -> Element<'a, Message> {
        let (space, font_size) = use_theme(|t| (t.space, t.font_size));
        button(
            column!(
                row!(
                    text(&notification.summary)
                        .wrapping(text::Wrapping::WordOrGlyph)
                        .width(Length::Fill),
                    text(self.format_timestamp(notification.timestamp)).size(font_size.sm)
                ),
                text(&notification.body).wrapping(text::Wrapping::WordOrGlyph)
            )
            .padding(space.xs)
            .spacing(space.xs),
        )
        .style(Self::notification_button_style(
            if is_last {
                NotificationStyle::GroupLast
            } else {
                NotificationStyle::GroupItem
            },
            notification.urgency,
        ))
        .on_press(Message::NotificationClicked(notification.id))
        .into()
    }

    pub fn view(&'_ self) -> Element<'_, Message> {
        if !self.notifications.is_empty() {
            icon(StaticIcon::BellBadge).into()
        } else {
            icon(StaticIcon::Bell).into()
        }
    }

    pub fn menu_view<'a>(&'a self) -> Element<'a, Message> {
        let (space, font_size) = use_theme(|t| (t.space, t.font_size));
        let is_empty = self.notifications.is_empty();

        let content = if is_empty {
            container(text(t!("notifications-empty")).size(font_size.md))
                .width(Length::Fill)
                .center_x(Length::Fill)
                .padding(space.xxl)
                .into()
        } else if self.config.grouped {
            self.grouped_notifications()
        } else {
            self.list_notifications()
        };

        column!(
            Row::new()
                .push(
                    text(t!("notifications-heading"))
                        .width(Length::Fill)
                        .size(font_size.lg)
                )
                .push((!is_empty).then(|| {
                    icon_button(StaticIcon::Delete).on_press(Message::ClearNotifications)
                })),
            container(scrollable(content).spacing(space.xs)).max_height(400.),
        )
        .width(MenuSize::Medium)
        .spacing(space.sm)
        .into()
    }

    pub fn toast_view<'a>(&'a self) -> Element<'a, Message> {
        let space = use_theme(|t| t.space);
        if self.toasts.is_empty() {
            return Space::new().into();
        }

        let slide_direction = match self.config.toast_position {
            ToastPosition::TopRight | ToastPosition::BottomRight => SlideDirection::Right,
            ToastPosition::TopLeft | ToastPosition::BottomLeft => SlideDirection::Left,
        };
        let card_width = MenuSize::Medium.size();
        // A toast is alone on its surface, so it publishes its own blur region.
        let (blur_enabled, card_radius) =
            use_theme(|t| (t.surface(Surface::Notifications).blur, t.radius.lg));

        let mut toast_column = column!().spacing(space.sm);

        for &toast_id in &self.toasts {
            if let Some(notification) = self.find_notification(toast_id) {
                let phase = self.dismiss_phases.get(&toast_id).copied();
                let is_dismissing = phase.is_some();
                let is_collapsing = phase == Some(DismissPhase::Collapsing);
                toast_column = toast_column.push(
                    collapsible(
                        !is_collapsing,
                        slide(!is_dismissing, slide_direction, card_width, {
                            let card = self.notification_card(
                                notification,
                                Message::DismissToast(notification.id),
                                true,
                            );
                            if blur_enabled {
                                blur(card_radius, card).into()
                            } else {
                                card
                            }
                        })
                        .animated(self.animations_enabled)
                        .key(toast_id as u64),
                    )
                    .animated(self.animations_enabled)
                    .key(toast_id as u64),
                );
            }
        }

        let v_align = match self.config.toast_position {
            ToastPosition::TopLeft | ToastPosition::TopRight => Alignment::Start,
            ToastPosition::BottomLeft | ToastPosition::BottomRight => Alignment::End,
        };
        let h_align = match self.config.toast_position {
            ToastPosition::TopLeft | ToastPosition::BottomLeft => Alignment::Start,
            ToastPosition::TopRight | ToastPosition::BottomRight => Alignment::End,
        };

        let toast_content = sensor(
            container(toast_column)
                .width(MenuSize::Medium)
                .padding(space.sm),
        )
        .on_resize(Message::ToastResized);

        container(toast_content)
            .width(Length::Fill)
            .height(Length::Fill)
            .align_y(v_align)
            .align_x(h_align)
            .into()
    }

    pub fn subscription(&self) -> Subscription<Message> {
        NotificationsService::subscribe().map(Message::Event)
    }

    fn grouped_notifications<'a>(&'a self) -> Element<'a, Message> {
        let (space, font_size) = use_theme(|t| (t.space, t.font_size));
        let mut content = column!()
            .spacing(space.sm)
            .padding(Padding::default().right(space.md).left(space.xs));

        for (_app_name, group) in self
            .notifications
            .iter()
            .sorted_by(|a, b| a.app_name.cmp(&b.app_name))
            .chunk_by(|n| n.app_name.clone())
            .into_iter()
        {
            let mut iter = group.peekable();
            let first = match iter.next() {
                Some(n) => n,
                None => continue,
            };

            // Single notification in the group — use the normal card layout.
            if iter.peek().is_none() {
                content = content.push(self.notification_card(
                    first,
                    Message::NotificationClicked(first.id),
                    false,
                ));
                continue;
            }

            // Multiple notifications — use the group layout.
            let app_name = first.app_name.clone();
            let is_expanded = self.expanded_groups.contains(&app_name);
            let app_icon = notification_icon(first.icon.as_ref());

            let mut count = 1;
            let mut has_critical = first.urgency == Urgency::Critical;
            let mut group_notifications = vec![];

            if is_expanded {
                group_notifications.push(self.group_item(first, false));
                while let Some(notification) = iter.next() {
                    count += 1;
                    has_critical = has_critical || notification.urgency == Urgency::Critical;
                    let is_last = iter.peek().is_none();
                    group_notifications.push(self.group_item(notification, is_last));
                }
            } else {
                for notification in iter {
                    count += 1;
                    has_critical = has_critical || notification.urgency == Urgency::Critical;
                }
                group_notifications.push(self.group_item(first, true));
            }

            let clear_msg = Message::ClearGroup(app_name.clone());
            let toggle_msg = Message::ToggleGroup(app_name.clone());

            let header = row!(
                app_icon,
                text(app_name)
                    .size(font_size.md)
                    .wrapping(text::Wrapping::WordOrGlyph)
                    .width(Length::Fill),
                text(t!("notifications-group-count", count = count)),
                icon_button(StaticIcon::Delete)
                    .on_press(clear_msg)
                    .size(ButtonSize::Large)
                    .kind(ButtonKind::Transparent)
                    .hierarchy(ButtonHierarchy::Danger)
            )
            .spacing(space.xs)
            .align_y(Alignment::Center);

            let item = Column::new()
                .push(
                    button(header)
                        .style(Self::notification_button_style(
                            NotificationStyle::GroupHeader,
                            if has_critical {
                                Urgency::Critical
                            } else {
                                Urgency::Normal
                            },
                        ))
                        .on_press(toggle_msg),
                )
                .extend(group_notifications)
                .spacing(space.xxs);

            content = content.push(item);
        }
        content.into()
    }

    fn list_notifications<'a>(&'a self) -> Element<'a, Message> {
        let space = use_theme(|t| t.space);
        Column::with_children(
            self.notifications
                .iter()
                .map(|notification| {
                    self.notification_card(
                        notification,
                        Message::NotificationClicked(notification.id),
                        false,
                    )
                })
                .collect::<Vec<Element<'a, Message>>>(),
        )
        .padding(Padding::default().right(space.md).left(space.xs))
        .spacing(space.sm)
        .into()
    }

    pub fn toast_position(&self) -> ToastPosition {
        self.config.toast_position
    }
}
