use crate::{chat_panel::ChatPanel, render_avatar, NotificationPanelSettings};
use anyhow::Result;
use channel::ChannelStore;
use client::{Client, Notification, User, UserStore};
use collections::HashMap;
use db::kvp::KEY_VALUE_STORE;
use futures::StreamExt;
use gpui::{
    actions,
    elements::*,
    platform::{CursorStyle, MouseButton},
    serde_json, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View,
    ViewContext, ViewHandle, WeakViewHandle, WindowContext,
};
use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
use project::Fs;
use rpc::proto;
use serde::{Deserialize, Serialize};
use settings::SettingsStore;
use std::{sync::Arc, time::Duration};
use theme::{ui, Theme};
use time::{OffsetDateTime, UtcOffset};
use util::{ResultExt, TryFutureExt};
use workspace::{
    dock::{DockPosition, Panel},
    Workspace,
};

const LOADING_THRESHOLD: usize = 30;
const MARK_AS_READ_DELAY: Duration = Duration::from_secs(1);
const TOAST_DURATION: Duration = Duration::from_secs(5);
const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel";

pub struct NotificationPanel {
    client: Arc<Client>,
    user_store: ModelHandle<UserStore>,
    channel_store: ModelHandle<ChannelStore>,
    notification_store: ModelHandle<NotificationStore>,
    fs: Arc<dyn Fs>,
    width: Option<f32>,
    active: bool,
    notification_list: ListState<Self>,
    pending_serialization: Task<Option<()>>,
    subscriptions: Vec<gpui::Subscription>,
    workspace: WeakViewHandle<Workspace>,
    current_notification_toast: Option<(u64, Task<()>)>,
    local_timezone: UtcOffset,
    has_focus: bool,
    mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
}

#[derive(Serialize, Deserialize)]
struct SerializedNotificationPanel {
    width: Option<f32>,
}

#[derive(Debug)]
pub enum Event {
    DockPositionChanged,
    Focus,
    Dismissed,
}

pub struct NotificationPresenter {
    pub actor: Option<Arc<client::User>>,
    pub text: String,
    pub icon: &'static str,
    pub needs_response: bool,
    pub can_navigate: bool,
}

actions!(notification_panel, [ToggleFocus]);

pub fn init(_cx: &mut AppContext) {}

impl NotificationPanel {
    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
        let fs = workspace.app_state().fs.clone();
        let client = workspace.app_state().client.clone();
        let user_store = workspace.app_state().user_store.clone();
        let workspace_handle = workspace.weak_handle();

        cx.add_view(|cx| {
            let mut status = client.status();
            cx.spawn(|this, mut cx| async move {
                while let Some(_) = status.next().await {
                    if this
                        .update(&mut cx, |_, cx| {
                            cx.notify();
                        })
                        .is_err()
                    {
                        break;
                    }
                }
            })
            .detach();

            let mut notification_list =
                ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
                    this.render_notification(ix, cx)
                        .unwrap_or_else(|| Empty::new().into_any())
                });
            notification_list.set_scroll_handler(|visible_range, count, this, cx| {
                if count.saturating_sub(visible_range.end) < LOADING_THRESHOLD {
                    if let Some(task) = this
                        .notification_store
                        .update(cx, |store, cx| store.load_more_notifications(false, cx))
                    {
                        task.detach();
                    }
                }
            });

            let mut this = Self {
                fs,
                client,
                user_store,
                local_timezone: cx.platform().local_timezone(),
                channel_store: ChannelStore::global(cx),
                notification_store: NotificationStore::global(cx),
                notification_list,
                pending_serialization: Task::ready(None),
                workspace: workspace_handle,
                has_focus: false,
                current_notification_toast: None,
                subscriptions: Vec::new(),
                active: false,
                mark_as_read_tasks: HashMap::default(),
                width: None,
            };

            let mut old_dock_position = this.position(cx);
            this.subscriptions.extend([
                cx.observe(&this.notification_store, |_, _, cx| cx.notify()),
                cx.subscribe(&this.notification_store, Self::on_notification_event),
                cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
                    let new_dock_position = this.position(cx);
                    if new_dock_position != old_dock_position {
                        old_dock_position = new_dock_position;
                        cx.emit(Event::DockPositionChanged);
                    }
                    cx.notify();
                }),
            ]);
            this
        })
    }

    pub fn load(
        workspace: WeakViewHandle<Workspace>,
        cx: AsyncAppContext,
    ) -> Task<Result<ViewHandle<Self>>> {
        cx.spawn(|mut cx| async move {
            let serialized_panel = if let Some(panel) = cx
                .background()
                .spawn(async move { KEY_VALUE_STORE.read_kvp(NOTIFICATION_PANEL_KEY) })
                .await
                .log_err()
                .flatten()
            {
                Some(serde_json::from_str::<SerializedNotificationPanel>(&panel)?)
            } else {
                None
            };

            workspace.update(&mut cx, |workspace, cx| {
                let panel = Self::new(workspace, cx);
                if let Some(serialized_panel) = serialized_panel {
                    panel.update(cx, |panel, cx| {
                        panel.width = serialized_panel.width;
                        cx.notify();
                    });
                }
                panel
            })
        })
    }

    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
        let width = self.width;
        self.pending_serialization = cx.background().spawn(
            async move {
                KEY_VALUE_STORE
                    .write_kvp(
                        NOTIFICATION_PANEL_KEY.into(),
                        serde_json::to_string(&SerializedNotificationPanel { width })?,
                    )
                    .await?;
                anyhow::Ok(())
            }
            .log_err(),
        );
    }

    fn render_notification(
        &mut self,
        ix: usize,
        cx: &mut ViewContext<Self>,
    ) -> Option<AnyElement<Self>> {
        let entry = self.notification_store.read(cx).notification_at(ix)?;
        let notification_id = entry.id;
        let now = OffsetDateTime::now_utc();
        let timestamp = entry.timestamp;
        let NotificationPresenter {
            actor,
            text,
            needs_response,
            can_navigate,
            ..
        } = self.present_notification(entry, cx)?;

        let theme = theme::current(cx);
        let style = &theme.notification_panel;
        let response = entry.response;
        let notification = entry.notification.clone();

        let message_style = if entry.is_read {
            style.read_text.clone()
        } else {
            style.unread_text.clone()
        };

        if self.active && !entry.is_read {
            self.did_render_notification(notification_id, &notification, cx);
        }

        enum Decline {}
        enum Accept {}

        Some(
            MouseEventHandler::new::<NotificationEntry, _>(ix, cx, |_, cx| {
                let container = message_style.container;

                Flex::row()
                    .with_children(actor.map(|actor| {
                        render_avatar(actor.avatar.clone(), &style.avatar, style.avatar_container)
                    }))
                    .with_child(
                        Flex::column()
                            .with_child(Text::new(text, message_style.text.clone()))
                            .with_child(
                                Flex::row()
                                    .with_child(
                                        Label::new(
                                            format_timestamp(timestamp, now, self.local_timezone),
                                            style.timestamp.text.clone(),
                                        )
                                        .contained()
                                        .with_style(style.timestamp.container),
                                    )
                                    .with_children(if let Some(is_accepted) = response {
                                        Some(
                                            Label::new(
                                                if is_accepted {
                                                    "You accepted"
                                                } else {
                                                    "You declined"
                                                },
                                                style.read_text.text.clone(),
                                            )
                                            .flex_float()
                                            .into_any(),
                                        )
                                    } else if needs_response {
                                        Some(
                                            Flex::row()
                                                .with_children([
                                                    MouseEventHandler::new::<Decline, _>(
                                                        ix,
                                                        cx,
                                                        |state, _| {
                                                            let button =
                                                                style.button.style_for(state);
                                                            Label::new(
                                                                "Decline",
                                                                button.text.clone(),
                                                            )
                                                            .contained()
                                                            .with_style(button.container)
                                                        },
                                                    )
                                                    .with_cursor_style(CursorStyle::PointingHand)
                                                    .on_click(MouseButton::Left, {
                                                        let notification = notification.clone();
                                                        move |_, view, cx| {
                                                            view.respond_to_notification(
                                                                notification.clone(),
                                                                false,
                                                                cx,
                                                            );
                                                        }
                                                    }),
                                                    MouseEventHandler::new::<Accept, _>(
                                                        ix,
                                                        cx,
                                                        |state, _| {
                                                            let button =
                                                                style.button.style_for(state);
                                                            Label::new(
                                                                "Accept",
                                                                button.text.clone(),
                                                            )
                                                            .contained()
                                                            .with_style(button.container)
                                                        },
                                                    )
                                                    .with_cursor_style(CursorStyle::PointingHand)
                                                    .on_click(MouseButton::Left, {
                                                        let notification = notification.clone();
                                                        move |_, view, cx| {
                                                            view.respond_to_notification(
                                                                notification.clone(),
                                                                true,
                                                                cx,
                                                            );
                                                        }
                                                    }),
                                                ])
                                                .flex_float()
                                                .into_any(),
                                        )
                                    } else {
                                        None
                                    }),
                            )
                            .flex(1.0, true),
                    )
                    .contained()
                    .with_style(container)
                    .into_any()
            })
            .with_cursor_style(if can_navigate {
                CursorStyle::PointingHand
            } else {
                CursorStyle::default()
            })
            .on_click(MouseButton::Left, {
                let notification = notification.clone();
                move |_, this, cx| this.did_click_notification(&notification, cx)
            })
            .into_any(),
        )
    }

    fn present_notification(
        &self,
        entry: &NotificationEntry,
        cx: &AppContext,
    ) -> Option<NotificationPresenter> {
        let user_store = self.user_store.read(cx);
        let channel_store = self.channel_store.read(cx);
        match entry.notification {
            Notification::ContactRequest { sender_id } => {
                let requester = user_store.get_cached_user(sender_id)?;
                Some(NotificationPresenter {
                    icon: "icons/plus.svg",
                    text: format!("{} wants to add you as a contact", requester.github_login),
                    needs_response: user_store.has_incoming_contact_request(requester.id),
                    actor: Some(requester),
                    can_navigate: false,
                })
            }
            Notification::ContactRequestAccepted { responder_id } => {
                let responder = user_store.get_cached_user(responder_id)?;
                Some(NotificationPresenter {
                    icon: "icons/plus.svg",
                    text: format!("{} accepted your contact invite", responder.github_login),
                    needs_response: false,
                    actor: Some(responder),
                    can_navigate: false,
                })
            }
            Notification::ChannelInvitation {
                ref channel_name,
                channel_id,
                inviter_id,
            } => {
                let inviter = user_store.get_cached_user(inviter_id)?;
                Some(NotificationPresenter {
                    icon: "icons/hash.svg",
                    text: format!(
                        "{} invited you to join the #{channel_name} channel",
                        inviter.github_login
                    ),
                    needs_response: channel_store.has_channel_invitation(channel_id),
                    actor: Some(inviter),
                    can_navigate: false,
                })
            }
            Notification::ChannelMessageMention {
                sender_id,
                channel_id,
                message_id,
            } => {
                let sender = user_store.get_cached_user(sender_id)?;
                let channel = channel_store.channel_for_id(channel_id)?;
                let message = self
                    .notification_store
                    .read(cx)
                    .channel_message_for_id(message_id)?;
                Some(NotificationPresenter {
                    icon: "icons/conversations.svg",
                    text: format!(
                        "{} mentioned you in #{}:\n{}",
                        sender.github_login, channel.name, message.body,
                    ),
                    needs_response: false,
                    actor: Some(sender),
                    can_navigate: true,
                })
            }
        }
    }

    fn did_render_notification(
        &mut self,
        notification_id: u64,
        notification: &Notification,
        cx: &mut ViewContext<Self>,
    ) {
        let should_mark_as_read = match notification {
            Notification::ContactRequestAccepted { .. } => true,
            Notification::ContactRequest { .. }
            | Notification::ChannelInvitation { .. }
            | Notification::ChannelMessageMention { .. } => false,
        };

        if should_mark_as_read {
            self.mark_as_read_tasks
                .entry(notification_id)
                .or_insert_with(|| {
                    let client = self.client.clone();
                    cx.spawn(|this, mut cx| async move {
                        cx.background().timer(MARK_AS_READ_DELAY).await;
                        client
                            .request(proto::MarkNotificationRead { notification_id })
                            .await?;
                        this.update(&mut cx, |this, _| {
                            this.mark_as_read_tasks.remove(&notification_id);
                        })?;
                        Ok(())
                    })
                });
        }
    }

    fn did_click_notification(&mut self, notification: &Notification, cx: &mut ViewContext<Self>) {
        if let Notification::ChannelMessageMention {
            message_id,
            channel_id,
            ..
        } = notification.clone()
        {
            if let Some(workspace) = self.workspace.upgrade(cx) {
                cx.app_context().defer(move |cx| {
                    workspace.update(cx, |workspace, cx| {
                        if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
                            panel.update(cx, |panel, cx| {
                                panel
                                    .select_channel(channel_id, Some(message_id), cx)
                                    .detach_and_log_err(cx);
                            });
                        }
                    });
                });
            }
        }
    }

    fn is_showing_notification(&self, notification: &Notification, cx: &AppContext) -> bool {
        if let Notification::ChannelMessageMention { channel_id, .. } = &notification {
            if let Some(workspace) = self.workspace.upgrade(cx) {
                return workspace
                    .read_with(cx, |workspace, cx| {
                        if let Some(panel) = workspace.panel::<ChatPanel>(cx) {
                            return panel.read_with(cx, |panel, cx| {
                                panel.is_scrolled_to_bottom()
                                    && panel.active_chat().map_or(false, |chat| {
                                        chat.read(cx).channel_id == *channel_id
                                    })
                            });
                        }
                        false
                    })
                    .unwrap_or_default();
            }
        }

        false
    }

    fn render_sign_in_prompt(
        &self,
        theme: &Arc<Theme>,
        cx: &mut ViewContext<Self>,
    ) -> AnyElement<Self> {
        enum SignInPromptLabel {}

        MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
            Label::new(
                "Sign in to view your notifications".to_string(),
                theme
                    .chat_panel
                    .sign_in_prompt
                    .style_for(mouse_state)
                    .clone(),
            )
        })
        .with_cursor_style(CursorStyle::PointingHand)
        .on_click(MouseButton::Left, move |_, this, cx| {
            let client = this.client.clone();
            cx.spawn(|_, cx| async move {
                client.authenticate_and_connect(true, &cx).log_err().await;
            })
            .detach();
        })
        .aligned()
        .into_any()
    }

    fn render_empty_state(
        &self,
        theme: &Arc<Theme>,
        _cx: &mut ViewContext<Self>,
    ) -> AnyElement<Self> {
        Label::new(
            "You have no notifications".to_string(),
            theme.chat_panel.sign_in_prompt.default.clone(),
        )
        .aligned()
        .into_any()
    }

    fn on_notification_event(
        &mut self,
        _: ModelHandle<NotificationStore>,
        event: &NotificationEvent,
        cx: &mut ViewContext<Self>,
    ) {
        match event {
            NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx),
            NotificationEvent::NotificationRemoved { entry }
            | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry.id, cx),
            NotificationEvent::NotificationsUpdated {
                old_range,
                new_count,
            } => {
                self.notification_list.splice(old_range.clone(), *new_count);
                cx.notify();
            }
        }
    }

    fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext<Self>) {
        if self.is_showing_notification(&entry.notification, cx) {
            return;
        }

        let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx)
        else {
            return;
        };

        let notification_id = entry.id;
        self.current_notification_toast = Some((
            notification_id,
            cx.spawn(|this, mut cx| async move {
                cx.background().timer(TOAST_DURATION).await;
                this.update(&mut cx, |this, cx| this.remove_toast(notification_id, cx))
                    .ok();
            }),
        ));

        self.workspace
            .update(cx, |workspace, cx| {
                workspace.dismiss_notification::<NotificationToast>(0, cx);
                workspace.show_notification(0, cx, |cx| {
                    let workspace = cx.weak_handle();
                    cx.add_view(|_| NotificationToast {
                        notification_id,
                        actor,
                        text,
                        workspace,
                    })
                })
            })
            .ok();
    }

    fn remove_toast(&mut self, notification_id: u64, cx: &mut ViewContext<Self>) {
        if let Some((current_id, _)) = &self.current_notification_toast {
            if *current_id == notification_id {
                self.current_notification_toast.take();
                self.workspace
                    .update(cx, |workspace, cx| {
                        workspace.dismiss_notification::<NotificationToast>(0, cx)
                    })
                    .ok();
            }
        }
    }

    fn respond_to_notification(
        &mut self,
        notification: Notification,
        response: bool,
        cx: &mut ViewContext<Self>,
    ) {
        self.notification_store.update(cx, |store, cx| {
            store.respond_to_notification(notification, response, cx);
        });
    }
}

impl Entity for NotificationPanel {
    type Event = Event;
}

impl View for NotificationPanel {
    fn ui_name() -> &'static str {
        "NotificationPanel"
    }

    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
        let theme = theme::current(cx);
        let style = &theme.notification_panel;
        let element = if self.client.user_id().is_none() {
            self.render_sign_in_prompt(&theme, cx)
        } else if self.notification_list.item_count() == 0 {
            self.render_empty_state(&theme, cx)
        } else {
            Flex::column()
                .with_child(
                    Flex::row()
                        .with_child(Label::new("Notifications", style.title.text.clone()))
                        .with_child(ui::svg(&style.title_icon).flex_float())
                        .align_children_center()
                        .contained()
                        .with_style(style.title.container)
                        .constrained()
                        .with_height(style.title_height),
                )
                .with_child(
                    List::new(self.notification_list.clone())
                        .contained()
                        .with_style(style.list)
                        .flex(1., true),
                )
                .into_any()
        };
        element
            .contained()
            .with_style(style.container)
            .constrained()
            .with_min_width(150.)
            .into_any()
    }

    fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
        self.has_focus = true;
    }

    fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
        self.has_focus = false;
    }
}

impl Panel for NotificationPanel {
    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
        settings::get::<NotificationPanelSettings>(cx).dock
    }

    fn position_is_valid(&self, position: DockPosition) -> bool {
        matches!(position, DockPosition::Left | DockPosition::Right)
    }

    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
        settings::update_settings_file::<NotificationPanelSettings>(
            self.fs.clone(),
            cx,
            move |settings| settings.dock = Some(position),
        );
    }

    fn size(&self, cx: &gpui::WindowContext) -> f32 {
        self.width
            .unwrap_or_else(|| settings::get::<NotificationPanelSettings>(cx).default_width)
    }

    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
        self.width = size;
        self.serialize(cx);
        cx.notify();
    }

    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
        self.active = active;
        if self.notification_store.read(cx).notification_count() == 0 {
            cx.emit(Event::Dismissed);
        }
    }

    fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
        (settings::get::<NotificationPanelSettings>(cx).button
            && self.notification_store.read(cx).notification_count() > 0)
            .then(|| "icons/bell.svg")
    }

    fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
        (
            "Notification Panel".to_string(),
            Some(Box::new(ToggleFocus)),
        )
    }

    fn icon_label(&self, cx: &WindowContext) -> Option<String> {
        let count = self.notification_store.read(cx).unread_notification_count();
        if count == 0 {
            None
        } else {
            Some(count.to_string())
        }
    }

    fn should_change_position_on_event(event: &Self::Event) -> bool {
        matches!(event, Event::DockPositionChanged)
    }

    fn should_close_on_event(event: &Self::Event) -> bool {
        matches!(event, Event::Dismissed)
    }

    fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
        self.has_focus
    }

    fn is_focus_event(event: &Self::Event) -> bool {
        matches!(event, Event::Focus)
    }
}

pub struct NotificationToast {
    notification_id: u64,
    actor: Option<Arc<User>>,
    text: String,
    workspace: WeakViewHandle<Workspace>,
}

pub enum ToastEvent {
    Dismiss,
}

impl NotificationToast {
    fn focus_notification_panel(&self, cx: &mut AppContext) {
        let workspace = self.workspace.clone();
        let notification_id = self.notification_id;
        cx.defer(move |cx| {
            workspace
                .update(cx, |workspace, cx| {
                    if let Some(panel) = workspace.focus_panel::<NotificationPanel>(cx) {
                        panel.update(cx, |panel, cx| {
                            let store = panel.notification_store.read(cx);
                            if let Some(entry) = store.notification_for_id(notification_id) {
                                panel.did_click_notification(&entry.clone().notification, cx);
                            }
                        });
                    }
                })
                .ok();
        })
    }
}

impl Entity for NotificationToast {
    type Event = ToastEvent;
}

impl View for NotificationToast {
    fn ui_name() -> &'static str {
        "ContactNotification"
    }

    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
        let user = self.actor.clone();
        let theme = theme::current(cx).clone();
        let theme = &theme.contact_notification;

        MouseEventHandler::new::<Self, _>(0, cx, |_, cx| {
            Flex::row()
                .with_children(user.and_then(|user| {
                    Some(
                        Image::from_data(user.avatar.clone()?)
                            .with_style(theme.header_avatar)
                            .aligned()
                            .constrained()
                            .with_height(
                                cx.font_cache()
                                    .line_height(theme.header_message.text.font_size),
                            )
                            .aligned()
                            .top(),
                    )
                }))
                .with_child(
                    Text::new(self.text.clone(), theme.header_message.text.clone())
                        .contained()
                        .with_style(theme.header_message.container)
                        .aligned()
                        .top()
                        .left()
                        .flex(1., true),
                )
                .with_child(
                    MouseEventHandler::new::<ToastEvent, _>(0, cx, |state, _| {
                        let style = theme.dismiss_button.style_for(state);
                        Svg::new("icons/x.svg")
                            .with_color(style.color)
                            .constrained()
                            .with_width(style.icon_width)
                            .aligned()
                            .contained()
                            .with_style(style.container)
                            .constrained()
                            .with_width(style.button_width)
                            .with_height(style.button_width)
                    })
                    .with_cursor_style(CursorStyle::PointingHand)
                    .with_padding(Padding::uniform(5.))
                    .on_click(MouseButton::Left, move |_, _, cx| {
                        cx.emit(ToastEvent::Dismiss)
                    })
                    .aligned()
                    .constrained()
                    .with_height(
                        cx.font_cache()
                            .line_height(theme.header_message.text.font_size),
                    )
                    .aligned()
                    .top()
                    .flex_float(),
                )
                .contained()
        })
        .with_cursor_style(CursorStyle::PointingHand)
        .on_click(MouseButton::Left, move |_, this, cx| {
            this.focus_notification_panel(cx);
            cx.emit(ToastEvent::Dismiss);
        })
        .into_any()
    }
}

impl workspace::notifications::Notification for NotificationToast {
    fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
        matches!(event, ToastEvent::Dismiss)
    }
}

fn format_timestamp(
    mut timestamp: OffsetDateTime,
    mut now: OffsetDateTime,
    local_timezone: UtcOffset,
) -> String {
    timestamp = timestamp.to_offset(local_timezone);
    now = now.to_offset(local_timezone);

    let today = now.date();
    let date = timestamp.date();
    if date == today {
        let difference = now - timestamp;
        if difference >= Duration::from_secs(3600) {
            format!("{}h", difference.whole_seconds() / 3600)
        } else if difference >= Duration::from_secs(60) {
            format!("{}m", difference.whole_seconds() / 60)
        } else {
            "just now".to_string()
        }
    } else if date.next_day() == Some(today) {
        format!("yesterday")
    } else {
        format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
    }
}
