use std::{cell::RefCell, cmp::Ordering, collections::HashSet};

use chrono::{DateTime, Datelike, Duration, Local, Utc};
use human_format::{Formatter, Scales};
use itertools::Itertools;
use letterbox_shared::compute_color;
use log::error;
use seed::{prelude::*, *};
use seed_hooks::{state_access::CloneState, topo, use_state, StateAccessEventHandlers};
use web_sys::{HtmlElement, HtmlInputElement};

use crate::{
    api::urls,
    graphql::{front_page_query::*, show_thread_query::*},
    state::{unread_query, CatchupItem, Context, Model, Msg, RefreshingState, Tag, Version},
};

// TODO(wathiede): create a QueryString enum that wraps single and multiple message ids and thread
// ids, and has a to_query_string() that knows notmuch's syntax. Then remove the smattering of
// format!() calls all over with magic strings representing notmuch specific syntax.
const MAX_RAW_MESSAGE_SIZE: usize = 100_000;

mod tw_classes {
    use seed::{prelude::*, *};

    pub const TAG: &[&str] = &[
        "rounded-md",
        "px-2",
        "py-1",
        "text-xs",
        "[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]",
    ];
    pub const TAG_X: &[&str] = &[
        "rounded-r",
        "bg-neutral-800",
        "px-2",
        "py-1",
        "mr-1",
        "text-xs",
        "[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]",
    ];

    // TODO: should this be a builder pattern?
    pub fn button() -> seed::Attrs {
        button_with_color("bg-neutral-900", "hover:bg-neutral-700")
    }

    pub fn button_with_color<T: ToClasses>(bg: T, hover: T) -> seed::Attrs {
        C![
            "rounded-md",
            "p-2",
            "border",
            "border-neutral-700",
            "text-center",
            "text-sm",
            "transition-all",
            "shadow-md",
            "hover:shadow-lg",
            "disabled:pointer-events-none",
            "disabled:opacity-50",
            "disabled:shadow-none",
            bg,
            hover,
        ]
    }

    pub const CHECKBOX: &[&str] = &[
        "w-8",
        "h-8",
        "accent-green-600",
        "appearance-none",
        "checked:appearance-auto",
        "indeterminate:appearance-auto",
        "rounded",
        "border",
        "border-neutral-500",
    ];
}

pub fn view(model: &Model) -> Node<Msg> {
    let is_loading = match model.refreshing_state {
        RefreshingState::Loading => true,
        _ => false,
    };
    match &model.context {
        Context::None => normal_view(
            div![h1!["Loading"]],
            &model.versions,
            &model.query,
            &model.refreshing_state,
            &model.tags,
        ),
        Context::ThreadResult {
            thread: ShowThreadQueryThread::EmailThread(thread_data),
            open_messages,
        } => {
            if let Some(catchup) = &model.catchup {
                catchup_view(
                    thread(thread_data, open_messages, &model.content_el, true, 0.),
                    &catchup.items,
                    is_loading,
                    model.read_completion_ratio,
                    true, // show spam button for email
                )
            } else {
                normal_view(
                    thread(
                        thread_data,
                        open_messages,
                        &model.content_el,
                        false,
                        model.read_completion_ratio,
                    ),
                    &model.versions,
                    &model.query,
                    &model.refreshing_state,
                    &model.tags,
                )
            }
        }
        Context::ThreadResult {
            thread: ShowThreadQueryThread::NewsPost(post),
            ..
        } => {
            if let Some(catchup) = &model.catchup {
                catchup_view(
                    news_post(post, &model.content_el, true, 0.),
                    &catchup.items,
                    is_loading,
                    model.read_completion_ratio,
                    false, // no spam button for news
                )
            } else {
                normal_view(
                    news_post(post, &model.content_el, false, model.read_completion_ratio),
                    &model.versions,
                    &model.query,
                    &model.refreshing_state,
                    &model.tags,
                )
            }
        }
        Context::SearchResult {
            query,
            results,
            count,
            pager,
            selected_threads,
        } => normal_view(
            search_results(&query, results.as_slice(), *count, pager, selected_threads),
            &model.versions,
            &model.query,
            &model.refreshing_state,
            &model.tags,
        ),
    }
}

fn normal_view(
    content: Node<Msg>,
    versions: &Version,
    query: &str,
    refreshing_state: &RefreshingState,
    tags: &Option<Vec<Tag>>,
) -> Node<Msg> {
    div![
        C![
            "relative",
            "flex",
            "flex-wrap-reverse",
            "bg-black",
            "text-white",
            "lg:flex-nowrap",
            "w-full"
        ],
        div![
            C!["w-full", "lg:w-48", "flex-none", "flex", "flex-col"],
            view_tags(tags),
            view_versions(&versions)
        ],
        div![
            // TODO: This "overflow-hidden" is a hack because I can't figure out
            // how to prevent the search input box on mobile for growing it's
            // parent wider
            C!["flex-auto", "flex", "flex-col", "overflow-hidden"],
            view_header(query, refreshing_state, true),
            content,
            view_header(query, refreshing_state, false),
        ],
    ]
}

fn catchup_view(
    content: Node<Msg>,
    items: &[CatchupItem],
    is_loading: bool,
    read_completion_ratio: f64,
    show_spam_button: bool,
) -> Node<Msg> {
    div![
        C!["w-full", "relative", "text-white"],
        div![
            C![
                "fixed",
                "top-0",
                "right-0",
                "left-0",
                "p-4",
                "border-b",
                "border-gray-500",
                "bg-black/50",
            ],
            div![
                C!["absolute", "top-0", "left-4", "text-green-200", "p-4"],
                IF!(is_loading=>span![i![C!["animate-spin", "fas", "fa-spinner"]]])
            ],
            h1![
                C!["text-center"],
                format!("{} left ", items.iter().filter(|i| !i.seen).count(),)
            ],
            div![
                C!["absolute", "top-0", "right-4", "text-gray-500", "p-4"],
                span![i![C!["fas", "fa-x"]]],
                ev(Ev::Click, move |_| Msg::CatchupExit)
            ],
            div![
                C![
                    "absolute",
                    "left-0",
                    "right-0",
                    "bottom-0",
                    "w-full",
                    "h-1",
                    "bg-gray-200"
                ],
                div![
                    C!["h-1", "bg-green-500"],
                    style! {
                        St::Width => format!("{}%", read_completion_ratio*100.)
                    }
                ]
            ]
        ],
        div![C!["mt-12", "mb-20"], content],
        div![
            C![
                "fixed",
                "bottom-0",
                "left-0",
                "right-0",
                "flex",
                "justify-center",
                "gap-4",
                "p-4",
                "border-t",
                "border-gray-500",
                "bg-black/50",
            ],
            button![
                tw_classes::button(),
                span![i![C!["far", "fa-envelope"]]],
                span![C!["pl-2"], "Keep unread"],
                ev(Ev::Click, |_| Msg::CatchupKeepUnread)
            ],
            button![
                tw_classes::button(),
                span![i![C!["fas", "fa-house"]]],
                span![C!["pl-2"], "Go home"],
                ev(Ev::Click, |_| Msg::MultiMsg(vec![
                    Msg::CatchupExit,
                    Msg::GoToSearchResults
                ]))
            ],
            IF!(show_spam_button => button![
                tw_classes::button(),
                C!["text-red-500"],
                attrs! {At::Title => "Mark as spam"},
                span![i![C!["far", "fa-hand"]]],
                span![C!["pl-2"], "Spam"],
                ev(Ev::Click, |_| Msg::CatchupMarkAsSpam)
            ]),
            button![
                tw_classes::button_with_color("bg-green-800", "hover:bg-green-700"),
                span![i![C!["far", "fa-envelope-open"]]],
                span![C!["pl-2"], "Mark as read"],
                ev(Ev::Click, |_| Msg::CatchupMarkAsRead)
            ]
        ],
    ]
}

fn search_results(
    query: &str,
    results: &[FrontPageQuerySearchNodes],
    count: usize,
    pager: &FrontPageQuerySearchPageInfo,
    selected_threads: &HashSet<String>,
) -> Node<Msg> {
    if query.is_empty() {
        set_title("all mail");
    } else {
        set_title(query);
    }
    let rows: Vec<_> = results
        .iter()
        .map(|r| {
            let tid = r.thread.clone();
            let check_tid = r.thread.clone();
            let datetime = human_age(r.timestamp as i64);
            let unread_idx = r.tags.iter().position(|e| e == &"unread");
            let mut tags = r.tags.clone();
            if let Some(idx) = unread_idx {
                tags.remove(idx);
            };
            let is_unread = unread_idx.is_some();
            let mut title_break = None;
            const TITLE_LENGTH_WRAP_LIMIT: usize = 40;
            for w in r.subject.split_whitespace() {
                if w.len() > TITLE_LENGTH_WRAP_LIMIT {
                    title_break = Some(C!["break-all", "text-pretty"]);
                }
            }
            div![
                C![
                    "flex",
                    "flex-nowrap",
                    "w-auto",
                    "flex-auto",
                    "py-4",
                    "border-b",
                    "border-neutral-800"
                ],
                div![
                    C!["flex", "items-center", "mr-4"],
                    input![
                        C![&tw_classes::CHECKBOX],
                        attrs! {
                            At::Type=>"checkbox",
                            At::Checked=>selected_threads.contains(&tid).as_at_value(),
                        }
                    ],
                    ev(Ev::Input, move |e| {
                        if let Some(input) = e
                            .target()
                            .as_ref()
                            .expect("failed to get reference to target")
                            .dyn_ref::<web_sys::HtmlInputElement>()
                        {
                            if input.checked() {
                                Msg::SelectionAddThread(check_tid)
                            } else {
                                Msg::SelectionRemoveThread(check_tid)
                            }
                        } else {
                            Msg::Noop
                        }
                    }),
                ],
                a![
                    C!["flex-grow"],
                    IF!(is_unread => C!["font-bold"]),
                    attrs! {
                        At::Href => urls::thread(&tid)
                    },
                    div![C!["line-clamp-2"], title_break, &r.subject],
                    span![C!["line-clamp-2", "text-xs"], pretty_authors(&r.authors)],
                    div![
                        C!["flex", "flex-wrap", "justify-between"],
                        span![tags_chiclet(&tags)],
                        span![C!["text-sm"], datetime]
                    ]
                ]
            ]
        })
        .collect();
    let show_bulk_edit = !selected_threads.is_empty();
    let all_selected = (selected_threads.len() == results.len()) && !rows.is_empty();
    let content = if rows.is_empty() {
        let caught_up = query.contains("is:unread");
        let read_emoji = ["👻", "👽", "👾", "🤖", "💀"];
        let no_results_emoji = ["🙈", "👀", "🤦", "🤷", "🙅", "🛟", "🍩", "🌑", "💿", "🔍"];
        // Randomly choose emoji based on what 10-second window we're currently in
        let now = seed::window()
            .performance()
            .map(|p| p.now() as usize / 10_000)
            .unwrap_or(0);
        let (emoji, text) = if caught_up {
            let idx = now % read_emoji.len();
            (read_emoji[idx], "All caught up!")
        } else {
            let idx = now % no_results_emoji.len();
            (no_results_emoji[idx], "No results")
        };
        div![
            C!["text-center"],
            h1![C!["text-9xl"], emoji],
            p![C!["mt-8", "text-3xl", "font-semibold"], text]
        ]
    } else {
        div![rows]
    };
    div![
        C!["flex", "flex-col", "flex-auto", "p-4"],
        search_toolbar(count, pager, show_bulk_edit, all_selected),
        content,
        search_toolbar(count, pager, show_bulk_edit, all_selected),
    ]
}

fn set_title(title: &str) {
    seed::document().set_title(&format!("lb: {}", title));
}

// TODO: unifiy tags_chiclet, removable_tags_chiclet, and tags inside news_post()
fn tags_chiclet(tags: &[String]) -> impl Iterator<Item = Node<Msg>> + '_ {
    tags.iter().map(move |tag| {
        let hex = compute_color(tag);
        let style = style! {St::BackgroundColor=>hex};
        let tag = tag.clone();
        a![
            C!["mr-1"],
            match tag.as_str() {
                "attachment" => span![C![&tw_classes::TAG], style, "📎"],
                "replied" => span![C![&tw_classes::TAG], style, i![C!["fa-solid", "fa-reply"]]],
                _ => span![C![&tw_classes::TAG], style, &tag],
            }
        ]
    })
}

fn removable_tags_chiclet<'a>(thread_id: &'a str, tags: &'a [String]) -> Node<Msg> {
    div![
        C!["flex"],
        tags.iter().map(move |tag| {
            let thread_id = thread_id.to_string();
            let hex = compute_color(tag);
            let style = style! {St::BackgroundColor=>hex};
            let attrs = attrs! {
                At::Href => urls::search(&format!("tag:{tag}"), 0)
            };
            let tag = tag.clone();
            let rm_tag = tag.clone();
            div![
                a![
                    C![&tw_classes::TAG, "rounded-r-none"],
                    attrs,
                    style,
                    match tag.as_str() {
                        "attachment" => span!["📎"],
                        "replied" => span![i![C!["fa-solid", "fa-reply"]]],
                        _ => span![&tag],
                    },
                    ev(Ev::Click, move |_| Msg::FrontPageRequest {
                        query: format!("tag:{tag}"),
                        after: None,
                        before: None,
                        first: None,
                        last: None,
                    })
                ],
                a![
                    C![&tw_classes::TAG_X],
                    span![i![C!["fa-solid", "fa-xmark"]]],
                    ev(Ev::Click, move |_| Msg::RemoveTagAndGoToSearch(thread_id, rm_tag))
                ]
            ]
        })
    ]
}

fn pretty_authors(authors: &str) -> impl Iterator<Item = Node<Msg>> + '_ {
    let one_person = authors.matches(',').count() == 0;
    let authors = authors.split(',');

    Itertools::intersperse(
        authors.filter_map(move |author| {
            if one_person {
                return Some(span![
                    attrs! {
                        At::Title => author.trim()
                    },
                    author
                ]);
            }
            author.split_whitespace().nth(0).map(|first| {
                span![
                    attrs! {
                        At::Title => author.trim()
                    },
                    first
                ]
            })
        }),
        span![", "],
    )
}

fn human_age(timestamp: i64) -> String {
    let now = Local::now();
    let yesterday = now - Duration::days(1);
    let ts = DateTime::<Utc>::from_timestamp(timestamp, 0)
        .unwrap()
        .with_timezone(&Local);
    let age = now - ts;
    let datetime = if age < Duration::minutes(1) {
        format!("{} secs. ago", age.num_seconds())
    } else if age < Duration::hours(1) {
        format!("{} min. ago", age.num_minutes())
    } else if ts.date_naive() == now.date_naive() {
        ts.format("Today %H:%M").to_string()
    } else if ts.date_naive() == yesterday.date_naive() {
        ts.format("Yest. %H:%M").to_string()
    } else if age < Duration::weeks(1) {
        ts.format("%a %H:%M").to_string()
    } else if ts.year() == now.year() {
        ts.format("%b %d %H:%M").to_string()
    } else {
        ts.format("%b %d, %Y %H:%M").to_string()
    };
    datetime
}

#[topo::nested]
fn search_toolbar(
    count: usize,
    pager: &FrontPageQuerySearchPageInfo,
    show_bulk_edit: bool,
    all_selected: bool,
) -> Node<Msg> {
    let indeterminate = show_bulk_edit && !all_selected;
    let tristate_el: ElRef<HtmlInputElement> = use_state(|| Default::default()).get();
    let tri = el_ref(&tristate_el);
    if let Some(tri) = tri.get() {
        tri.set_indeterminate(indeterminate);
    }
    let catchup = div![button![
        tw_classes::button(),
        attrs! {At::Title => "Catch up"},
        span![i![C!["far", "fa-eye"]]],
        span![C!["pl-2", "hidden", "md:inline"], "Catch-up"],
        ev(Ev::Click, |_| Msg::CatchupStart)
    ]];
    let tristate_input = div![
        C!["flex", "items-center", "mr-4"],
        input![
            &tri,
            C![&tw_classes::CHECKBOX],
            attrs! {
                At::Type=>"checkbox",
            },
            IF!(all_selected=>attrs!{At::Checked=>true})
        ],
        ev(Ev::Input, move |_| {
            if all_selected {
                Msg::SelectionSetNone
            } else {
                Msg::SelectionSetAll
            }
        }),
    ];
    nav![
        C!["py-4", "flex", "w-full", "justify-between"],
        div![
            C!["gap-2", "flex", IF!(show_bulk_edit => "hidden")],
            &tristate_input,
            &catchup
        ],
        div![
            C!["gap-2", "flex", IF!(!show_bulk_edit => "hidden")],
            &tristate_input,
            &catchup,
            div![
                button![
                    tw_classes::button(),
                    C!["rounded-r-none"],
                    attrs! {At::Title => "Mark as read"},
                    span![i![C!["far", "fa-envelope-open"]]],
                    span![C!["pl-2", "hidden", "md:inline"], "Read"],
                    ev(Ev::Click, |_| Msg::SelectionMarkAsRead)
                ],
                button![
                    tw_classes::button(),
                    C!["rounded-l-none"],
                    attrs! {At::Title => "Mark as unread"},
                    span![i![C!["far", "fa-envelope"]]],
                    span![C!["pl-2", "hidden", "md:inline"], "Unread"],
                    ev(Ev::Click, |_| Msg::SelectionMarkAsUnread)
                ]
            ],
            div![button![
                tw_classes::button(),
                C!["text-red-500"],
                attrs! {At::Title => "Mark as spam"},
                span![i![C!["far", "fa-hand"]]],
                span![C!["pl-2", "hidden", "md:inline"], "Spam"],
                ev(Ev::Click, |_| Msg::MultiMsg(vec![
                    Msg::SelectionAddTag("Spam".to_string()),
                    Msg::SelectionMarkAsRead
                ]))
            ]],
        ],
        div![
            C!["flex", "gap-2", "items-center"],
            p![format!("{count} results")],
            button![
                tw_classes::button(),
                IF!(!pager.has_previous_page => attrs!{ At::Disabled=>true }),
                "<",
                IF!(pager.has_previous_page => ev(
                    Ev::Click, |_| Msg::MultiMsg(vec![
                        Msg::ScrollToTop,
                        Msg::PreviousPage]))),
            ],
            button![
                tw_classes::button(),
                IF!(!pager.has_next_page => attrs!{ At::Disabled=>true }),
                ">",
                IF!(pager.has_next_page => ev(
                    Ev::Click, |_| Msg::MultiMsg(vec![
                        Msg::ScrollToTop,
                        Msg::NextPage])))
            ]
        ]
    ]
}

fn raw_text_message(contents: &str) -> Node<Msg> {
    let (contents, truncated_msg) = if contents.len() > MAX_RAW_MESSAGE_SIZE {
        (
            &contents[..MAX_RAW_MESSAGE_SIZE],
            Some(div!["... contents truncated"]),
        )
    } else {
        (contents, None)
    };
    div![
        C!["view-part-text-plain", "font-mono", "whitespace-pre-line"],
        contents,
        truncated_msg,
    ]
}

fn has_unread(tags: &[String]) -> bool {
    tags.contains(&String::from("unread"))
}

fn render_avatar(photo_url: Option<String>, from: &str, big: bool) -> Node<Msg> {
    let size = if big {
        C!["w-16", "h-16", "text-4xl"]
    } else {
        C!["w-8", "h-8", "text-l"]
    };
    if let Some(photo_url) = photo_url {
        return div![
            size,
            img![attrs! {
                At::Src => photo_url,
            }]
        ];
    }

    let initials: String = from
        .to_lowercase()
        .trim()
        .split(" ")
        .map(|word| word.chars().next().unwrap())
        .filter(|c| c.is_alphanumeric())
        // Limit to 2 characters because more characters don't fit in the box
        .take(2)
        .collect();
    let from_color = compute_color(from);

    div![
        C![
            "[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]",
            "font-extrabold",
            "grid",
            "place-content-center",
            "uppercase"
        ],
        size,
        style! {St::BackgroundColor=>from_color},
        span![initials]
    ]
}

fn copy_text_widget(text: &str) -> Node<Msg> {
    let text = text.to_string();
    span![
        i![C!["NOTPORTED", "far", "fa-clone"]],
        ev(Ev::Click, move |e| {
            e.stop_propagation();
            Msg::CopyToClipboard(text)
        })
    ]
}
fn render_open_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Node<Msg> {
    let (from, from_detail, photo_url) = match &msg.from {
        Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom {
            name: Some(name),
            addr,
            photo_url,
        }) => (name.to_string(), addr.clone(), photo_url.clone()),
        Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom {
            addr: Some(addr),
            photo_url,
            ..
        }) => (addr.to_string(), None, photo_url.clone()),
        _ => (String::from("UNKNOWN"), None, None),
    };
    let id = msg.id.clone();
    let is_unread = has_unread(&msg.tags);
    let avatar = render_avatar(photo_url, &from, true);
    let unknown = "UNKNOWN".to_string();
    let to_xinu = RefCell::new(false);
    let to_addrs: Vec<_> = msg
        .to
        .iter()
        .map(|to| {
            let ShowThreadQueryThreadOnEmailThreadMessagesTo { name, addr } = to;
            span![
                addr.as_ref().map(|addr| {
                    if addr.ends_with("xinu.tv") {
                        *to_xinu.borrow_mut() = true;
                    }
                    attrs! {
                        At::Title => addr
                    }
                }),
                name.as_ref()
                    .unwrap_or_else(|| addr.as_ref().unwrap_or(&unknown)),
                " ",
                addr.as_ref().map(|addr| copy_text_widget(&addr)),
                " "
            ]
        })
        .collect();
    let cc_addrs: Vec<_> = msg
        .cc
        .iter()
        .map(|cc| {
            let ShowThreadQueryThreadOnEmailThreadMessagesCc { name, addr } = cc;
            span![
                addr.as_ref().map(|addr| {
                    if addr.ends_with("xinu.tv") {
                        *to_xinu.borrow_mut() = true;
                    }
                    attrs! {
                                At::Title => addr
                    }
                }),
                name.as_ref()
                    .unwrap_or_else(|| addr.as_ref().unwrap_or(&unknown)),
                " ",
                addr.as_ref().map(|addr| copy_text_widget(&addr)),
                " "
            ]
        })
        .collect();
    let show_x_original_to = !*to_xinu.borrow() && msg.x_original_to.is_some();
    let show_delivered_to = !*to_xinu.borrow() && !show_x_original_to && msg.delivered_to.is_some();
    let host = seed::window().location().host().expect("couldn't get host");
    let href = letterbox_shared::urls::view_original(Some(&host), &msg.id);
    div![
        C!["flex", "p-4", "bg-neutral-800"],
        div![avatar],
        div![
            C!["px-4", "flex-1"],
            div![
                C!["flex"],
                div![
                    C!["font-semibold", "text-sm", "flex-1"],
                    from_detail.as_ref().map(|addr| attrs! {
                        At::Title => addr
                    }),
                    &from,
                    " ",
                    from_detail.as_ref().map(|text| copy_text_widget(&text))
                ],
                snooze_buttons(msg.timestamp, &id),
            ],
            IF!(!msg.to.is_empty() =>div![
                C!["text-xs"],
                span![
                    C!["font-semibold"],
                    "To: "
                ],
                span![
                to_addrs
                ]
            ]),
            IF!(!msg.cc.is_empty() =>div![
                C!["text-xs"],
                span![
                    C!["font-semibold"],
                    "CC: "
                ],
                span![
                    cc_addrs
                ]
            ]),
            IF!(show_x_original_to => div![
                C!["text-xs"],
                span![
                    C!["font-semibold"],
                    "Original To: "
                ],
                span![
                    msg.x_original_to.as_ref().map(|to| {
                        let ShowThreadQueryThreadOnEmailThreadMessagesXOriginalTo { name, addr } = to;
                        span![
                            addr.as_ref().map(|addr| attrs! {
                                At::Title => addr
                            }),
                            name.as_ref().unwrap_or_else(|| addr.as_ref().unwrap_or(&unknown)),
                            " ",
                            addr.as_ref().map(|addr| copy_text_widget(&addr)),
                            " "
                        ]
                    })
                ]
            ]),
            IF!(show_delivered_to => div![
                C!["text-xs"],
                span![
                    C!["font-semibold"],
                    "Delivered To: "
                ],
                span![
                    msg.delivered_to.as_ref().map(|to| {
                        let ShowThreadQueryThreadOnEmailThreadMessagesDeliveredTo { name, addr } = to;
                        span![
                            addr.as_ref().map(|addr| attrs! {
                                At::Title => addr
                            }),
                            name.as_ref().unwrap_or_else(|| addr.as_ref().unwrap_or(&unknown)),
                            " ",
                            addr.as_ref().map(|addr| copy_text_widget(&addr)),
                            " "
                        ]
                    })
                ]
            ]),
        ],
        span![
            C!["text-right"],
            msg.timestamp
                .map(|ts| div![C!["text-xs", "text-nowrap"], human_age(ts)]),
            div![
                C!["p-2"],
                i![C![
                    "mx-4",
                    "read-status",
                    "far",
                    if is_unread {
                        "fa-envelope"
                    } else {
                        "fa-envelope-open"
                    },
                ]],
                ev(Ev::Click, move |e| {
                    e.stop_propagation();
                    Msg::SetUnread(id, !is_unread)
                }),
            ],
            div![
                C!["text-xs"],
                span![a![
                    attrs! {
                        At::Href=>href,
                        At::Target=>"_blank",
                    },
                    "View original",
                    ev(Ev::Click, move |e| {
                        e.stop_propagation();
                    })
                ]]
            ]
        ]
    ]
}

fn render_closed_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Node<Msg> {
    let (from, from_detail, photo_url) = match &msg.from {
        Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom {
            name: Some(name),
            addr,
            photo_url,
        }) => (name.to_string(), addr.clone(), photo_url.clone()),
        Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom {
            addr: Some(addr),
            photo_url,
            ..
        }) => (addr.to_string(), None, photo_url.clone()),
        _ => (String::from("UNKNOWN"), None, None),
    };
    let id = msg.id.clone();
    let is_unread = has_unread(&msg.tags);
    let avatar = render_avatar(photo_url, &from, false);
    let unknown = "UNKNOWN".to_string();
    let to_xinu = RefCell::new(false);
    let to_addrs: Vec<_> = msg
        .to
        .iter()
        .enumerate()
        .map(|(i, to)| {
            let ShowThreadQueryThreadOnEmailThreadMessagesTo { name, addr } = to;
            span![
                addr.as_ref().map(|addr| {
                    if addr.ends_with("xinu.tv") {
                        *to_xinu.borrow_mut() = true;
                    }
                    attrs! {
                        At::Title => addr
                    }
                }),
                if i > 0 { ", " } else { "" },
                name.as_ref()
                    .unwrap_or_else(|| addr.as_ref().unwrap_or(&unknown))
            ]
        })
        .collect();
    let cc_addrs: Vec<_> = msg
        .cc
        .iter()
        .enumerate()
        .map(|(i, cc)| {
            let ShowThreadQueryThreadOnEmailThreadMessagesCc { name, addr } = cc;
            span![
                addr.as_ref().map(|addr| {
                    if addr.ends_with("xinu.tv") {
                        *to_xinu.borrow_mut() = true;
                    }
                    attrs! {
                        At::Title => addr
                    }
                }),
                if i > 0 { ", " } else { "" },
                name.as_ref()
                    .unwrap_or_else(|| addr.as_ref().unwrap_or(&unknown))
            ]
        })
        .collect();
    let show_x_original_to = !*to_xinu.borrow() && msg.x_original_to.is_some();
    let show_delivered_to = !*to_xinu.borrow() && !show_x_original_to && msg.delivered_to.is_some();
    let common_style = C!["text-sm", "pr-2", "text-gray-500"];
    div![
        C!["flex", "bg-neutral-800"],
        div![C!["self-center"], avatar],
        div![
            C![
                "mx-2",
                "flex-1",
                "flex",
                "flex-nowrap",
                "items-center",
                "truncate",
            ],
            div![
                C!["font-semibold", "text-white"],
                &common_style,
                from_detail.as_ref().map(|addr| attrs! {
                    At::Title => addr
                }),
                &from
            ],
            " ",
            IF!(!msg.to.is_empty() => div![
                &common_style,
                span![
                    C!["font-semibold"],
                    "To: "
                ],
                span![
                    to_addrs
                ],
                " "
            ]),
            IF!(!msg.cc.is_empty() => div![
                &common_style,
                span![
                    C!["font-semibold"],
                    "CC: "
                ],
                cc_addrs
            ]),
            IF!(show_x_original_to => div![
                &common_style,
                span![
                    C!["font-semibold"],
                    "Original To: "
                ],
                span![
                    msg.x_original_to.as_ref().map(|to| {
                        let ShowThreadQueryThreadOnEmailThreadMessagesXOriginalTo { name, addr } = to;
                        span![
                            addr.as_ref().map(|addr| attrs! {
                                At::Title => addr
                            }),
                            name.as_ref().unwrap_or_else(|| addr.as_ref().unwrap_or(&unknown)),
                            " ",
                        ]
                    })
                ]
            ]),
            IF!(show_delivered_to => div![
                &common_style,
                span![
                    C!["font-semibold"],
                    "Delivered To: "
                ],
                span![
                    msg.delivered_to.as_ref().map(|to| {
                        let ShowThreadQueryThreadOnEmailThreadMessagesDeliveredTo { name, addr } = to;
                        span![
                            addr.as_ref().map(|addr| attrs! {
                                At::Title => addr
                            }),
                            name.as_ref().unwrap_or_else(|| addr.as_ref().unwrap_or(&unknown)),
                            " ",
                        ]
                    })
                ]
            ]),
        ],
        msg.timestamp.map(|ts| div![
            C!["text-xs", "text-nowrap", "justify-self-end", "self-center"],
            human_age(ts)
        ]),
        div![
            i![C![
                "m-2",
                "read-status",
                "far",
                if is_unread {
                    "fa-envelope"
                } else {
                    "fa-envelope-open"
                },
            ]],
            ev(Ev::Click, move |e| {
                e.stop_propagation();
                Msg::SetUnread(id, !is_unread)
            })
        ]
    ]
}

fn message_render(msg: &ShowThreadQueryThreadOnEmailThreadMessages, open: bool) -> Node<Msg> {
    let expand_id = msg.id.clone();
    let from = match &msg.from {
        Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom {
            addr: Some(addr), ..
        }) => Some(addr.to_string()),
        _ => None,
    };
    let from = from.map(|f| f.replace('.', "-").replace('@', "-"));
    div![
        C!["pb-1"],
        div![
            if open {
                render_open_header(&msg)
            } else {
                render_closed_header(&msg)
            },
            ev(Ev::Click, move |e| {
                e.stop_propagation();
                if open {
                    Msg::MessageCollapse(expand_id)
                } else {
                    Msg::MessageExpand(expand_id)
                }
            })
        ],
        IF!(open =>
        div![
            C!["content", "bg-white", "text-black", "p-4", "min-w-full", "w-0","overflow-x-auto", from.map(|f|format!("from-{f}"))],
            match &msg.body {
                ShowThreadQueryThreadOnEmailThreadMessagesBody::UnhandledContentType(
                    ShowThreadQueryThreadOnEmailThreadMessagesBodyOnUnhandledContentType { contents ,content_tree},
                ) => div![
                    raw_text_message(&contents),
                    div![C!["bg-red-500","p-4", "min-w-full", "w-0", "overflow-x-auto"],
                        view_content_tree(&content_tree),
                    ]
                ],
                ShowThreadQueryThreadOnEmailThreadMessagesBody::PlainText(
                    ShowThreadQueryThreadOnEmailThreadMessagesBodyOnPlainText {
                    contents,
                    content_tree,
                },
                ) => div![
                    raw_text_message(&contents),
                    view_content_tree(&content_tree),
                ],
                ShowThreadQueryThreadOnEmailThreadMessagesBody::Html(
                    ShowThreadQueryThreadOnEmailThreadMessagesBodyOnHtml {
                    contents,
                    content_tree,
                },
                ) => div![
                    C!["view-part-text-html"],
                    // If there isn't any HTML tags, treat more like plain text
                    IF!(!(contents.contains('<') && contents.contains('>')) => C!["whitespace-pre-line"]),
                    raw![contents],
                    IF!(!msg.attachments.is_empty() => render_attachements(&msg.attachments)),
                    view_content_tree(&content_tree),
                ],
            }
        ])
    ]
}

fn render_attachements(
    attachments: &[ShowThreadQueryThreadOnEmailThreadMessagesAttachments],
) -> Node<Msg> {
    div![
        C!["border-t", "border-neutral-200", "mt-2", "pt-2"],
        h2![C!["text-lg"], "Attachments"],
        div![
            C!["flex", "flex-wrap", "gap-2"],
            attachments.iter().map(|a| {
                let default = "UNKNOWN_FILE".to_string();
                let filename = a.filename.as_ref().unwrap_or(&default);
                let host = seed::window().location().host().expect("couldn't get host");
                let url = letterbox_shared::urls::download_attachment(
                    Some(&host),
                    &a.id,
                    &a.idx,
                    filename,
                );
                let mut fmtr = Formatter::new();
                fmtr.with_separator(" ");
                fmtr.with_scales(Scales::Binary());

                div![
                    C![
                        "flex",
                        "flex-col",
                        "flex-none",
                        "p-2",
                        "bg-neutral-200",
                        "border",
                        "border-neutral-400",
                    ],
                    a.content_type.as_ref().map(
                        |content_type| IF!(content_type.starts_with("image/") => img![
                            C!["w-32", "h-32", "md:w-64", "md:h-64", "object-cover"],
                            attrs!{At::Src=>url},
                        ])
                    ),
                    div![
                        C!["py-2", "flex", "flex-nowrap", "items-center"],
                        div![
                            C!["flex", "flex-col", "grow", "w-16", "md:w-32"],
                            span![C!["shrink", "truncate"], &a.filename],
                            span![C!["text-xs"], fmtr.format(a.size as f64), "B"]
                        ],
                        a![
                            C![
                                "aspect-square",
                                "px-2",
                                "pt-1",
                                "bg-neutral-300",
                                "border",
                                "border-neutral-400"
                            ],
                            span![i![C!["fas", "fa-download"]]],
                            ev(Ev::Click, move |_| {
                                seed::window()
                                    .location()
                                    .set_href(&url)
                                    .expect("failed to set URL");
                            })
                        ]
                    ]
                ]
            })
        ]
    ]
}

#[topo::nested]
fn thread(
    thread: &ShowThreadQueryThreadOnEmailThread,
    open_messages: &HashSet<String>,
    content_el: &ElRef<HtmlElement>,
    catchup_mode: bool,
    read_completion_ratio: f64,
) -> Node<Msg> {
    // TODO(wathiede): show per-message subject if it changes significantly from top-level subject
    let subject = if thread.subject.is_empty() {
        "(no subject)"
    } else {
        &thread.subject
    };
    set_title(subject);
    let mut tags: Vec<_> = thread
        .messages
        .iter()
        .fold(HashSet::new(), |mut tags, msg| {
            tags.extend(msg.tags.clone());
            tags
        })
        .into_iter()
        .collect();
    tags.sort();
    let messages = thread.messages.iter().map(|msg| {
        let open = open_messages.contains(&msg.id);
        message_render(&msg, open)
    });
    let read_thread_id = thread.thread_id.clone();
    let unread_thread_id = thread.thread_id.clone();
    let spam_add_thread_id = thread.thread_id.clone();
    let spam_unread_thread_id = thread.thread_id.clone();
    let mut title_break = None;
    const TITLE_LENGTH_WRAP_LIMIT: usize = 40;
    for w in subject.split_whitespace() {
        if w.len() > TITLE_LENGTH_WRAP_LIMIT {
            title_break = Some(C!["break-all", "text-pretty"]);
        }
    }
    div![
        C!["lg:p-4", "max-w-4xl"],
        div![
            C!["p-4", "lg:p-0"],
            h3![C!["text-xl"], title_break, subject],
            span![removable_tags_chiclet(&thread.thread_id, &tags)],
            IF!(!catchup_mode => div![
                C!["pt-4", "gap-2", "flex", "justify-around"],
                div![
                    button![
                        tw_classes::button(),
                        C!["rounded-r-none"],
                        attrs! {At::Title => "Mark as read"},
                        span![i![C!["far", "fa-envelope-open"]]],
                        span![C!["pl-2"], "Read"],
                        ev(Ev::Click, move |_| Msg::MultiMsg(vec![
                            Msg::SetUnread(read_thread_id, false),
                            Msg::GoToSearchResults
                        ])),
                    ],
                    button![
                        tw_classes::button(),
                        C!["rounded-l-none"],
                        attrs! {At::Title => "Mark as unread"},
                        span![i![C!["far", "fa-envelope"]]],
                        span![C!["pl-2"], "Unread"],
                        ev(Ev::Click, move |_| Msg::MultiMsg(vec![
                            Msg::SetUnread(unread_thread_id, true),
                            Msg::GoToSearchResults
                        ])),
                    ],
                ],
                div![button![
                    tw_classes::button(),
                    C!["text-red-500"],
                    attrs! {At::Title => "Spam"},
                    span![i![C!["far", "fa-hand"]]],
                    span![C!["pl-2"], "Spam"],
                    ev(Ev::Click, move |_| Msg::MultiMsg(vec![
                        Msg::AddTag(spam_add_thread_id, "Spam".to_string()),
                        Msg::SetUnread(spam_unread_thread_id, false),
                        Msg::GoToSearchResults
                    ])),
                ]]
            ]),
        ],
        div![
            C!["lg:mt-4", "mail-thread"],
            el_ref(content_el),
            messages,
            IF!(!catchup_mode => click_to_top())
        ],
        reading_progress(read_completion_ratio)
    ]
}

#[topo::nested]
fn view_content_tree(content_tree: &str) -> Node<Msg> {
    let debug_open = use_state(|| false);
    div![
        small![
            i![C![
                "fa-solid",
                if debug_open.get() {
                    "fa-angle-up"
                } else {
                    "fa-angle-down"
                }
            ]],
            " Debug",
            debug_open.on_click(|d| *d = !*d)
        ],
        IF!(debug_open.get() =>
            pre![C!["NOTPORTED","content-tree"], content_tree]),
    ]
}

fn view_header(
    query: &str,
    refresh_request: &RefreshingState,
    auto_focus_search: bool,
) -> Node<Msg> {
    let is_loading = refresh_request == &RefreshingState::Loading;
    let is_error = if let RefreshingState::Error(err) = refresh_request {
        error!("Failed to refresh: {err:?}");
        true
    } else {
        false
    };
    let query = Url::decode_uri_component(query).unwrap_or("".to_string());
    let filter_all = String::new();
    let filter_unread = unread_query().to_string();
    let filter_news = "is:unread is:news".to_string();
    let filter_mail = "is:unread is:mail".to_string();
    let highlight_color = "bg-sky-800";

    nav![
        C!["flex", "flex-col"],
        div![
            C!["flex-auto", "flex"],
            button![
                C![IF!(is_error => "bg-red-500"), "rounded-none"],
                tw_classes::button(),
                span![i![C![
                    "fa-solid",
                    "fa-arrow-rotate-right",
                    IF![is_loading => "animate-spin"],
                ]]],
                ev(Ev::Click, |_| Msg::RefreshStart),
            ],
            button![
                IF!(query == filter_all => C![highlight_color]),
                tw_classes::button(),
                C!["grow", "rounded-none"],
                "All",
                ev(Ev::Click, |_| Msg::SearchQuery(filter_all)),
            ],
            button![
                IF!(query == filter_unread => C![highlight_color]),
                tw_classes::button(),
                C!["grow", "rounded-none"],
                span![i![C!["far", "fa-envelope"]]],
                " Unread",
                ev(Ev::Click, |_| Msg::SearchQuery(filter_unread)),
            ],
            button![
                IF!(query == filter_news => C![highlight_color]),
                tw_classes::button(),
                C!["grow", "rounded-none"],
                span![i![C!["far", "fa-envelope"]]],
                " News",
                ev(Ev::Click, |_| Msg::SearchQuery(filter_news)),
            ],
            button![
                IF!(query == filter_mail => C![highlight_color]),
                tw_classes::button(),
                C!["grow", "rounded-none"],
                span![i![C!["far", "fa-envelope"]]],
                " Mail",
                ev(Ev::Click, |_| Msg::SearchQuery(filter_mail)),
            ],
        ],
        div![
            C!["flex-auto", "flex"],
            input![
                C!["grow", "text-black", "p-2", "focus-visible:outline-0"],
                attrs! {
                    At::Placeholder => "Search";
                    At::AutoFocus => auto_focus_search.as_at_value();
                    At::Value => query,
                },
                input_ev(Ev::Input, |q| Msg::UpdateQuery(q)),
                // Send search on enter.
                keyboard_ev(Ev::KeyUp, move |e| if e.key_code() == 0x0d {
                    Msg::SearchQuery(query)
                } else {
                    Msg::Noop
                }),
            ]
        ]
    ]
}

pub fn view_tags(tags: &Option<Vec<Tag>>) -> Node<Msg> {
    fn inner_view_tag(
        display_name: &str,
        indent: usize,
        t: &Tag,
        search_unread: bool,
    ) -> Node<Msg> {
        // Hacky, but necessary for tailwind to see all the classes we're using
        let indent_cls = match indent {
            0 => "",
            1 => "pl-2",
            2 => "pl-4",
            3 => "pl-6",
            4 => "pl-8",
            5 => "pl-10",
            _ => "pl-12",
        };
        let href = if search_unread {
            urls::search(&format!("is:unread tag:{}", t.name), 0)
        } else {
            urls::search(&format!("tag:{}", t.name), 0)
        };
        div![
            C!["flex", "flex-row", "flex-nowrap"],
            i![
                C![indent_cls, "pr-1", "fa-solid", "fa-tag"],
                style! {
                    //"--fa-primary-color" => t.fg_color,
                    St::Color => t.bg_color,
                },
            ],
            a![
                C![indent_cls, "grow", "truncate"],
                attrs! {
                    At::Href => href
                },
                display_name,
            ],
            span![
                C![
                    "self-center",
                    "justify-self-end",
                    "text-sm",
                    "text-neutral-400"
                ],
                IF!(t.unread>0 => format!("{}", t.unread))
            ],
            ev(Ev::Click, |_| { Msg::ScrollToTop })
        ]
    }
    fn matches(a: &[&str], b: &[&str]) -> usize {
        std::iter::zip(a.iter(), b.iter())
            .take_while(|(a, b)| a == b)
            .count()
    }
    fn inner_view_tags<'a>(
        tags: impl Iterator<Item = &'a Tag>,
        search_unread: bool,
    ) -> Vec<Node<Msg>> {
        let mut tag_els = Vec::new();
        let mut last = Vec::new();
        for t in tags {
            let parts: Vec<_> = t.name.split('/').collect();
            let mut n = matches(&last, &parts);
            if n + 2 <= parts.len() && parts.len() > 1 {
                // Synthesize fake tags for proper indenting.
                for i in n..parts.len() - 1 {
                    let display_name = parts[n];
                    tag_els.push(inner_view_tag(
                        &display_name,
                        n,
                        &Tag {
                            name: parts[..i + 1].join("/"),
                            bg_color: "#fff".to_string(),
                            unread: 0,
                        },
                        search_unread,
                    ));
                }
                n = parts.len() - 1;
            }
            let display_name = parts[n];
            tag_els.push(inner_view_tag(&display_name, n, t, search_unread));
            last = parts;
        }
        tag_els
    }
    let mut unread = tags
        .as_ref()
        .map(|tags| tags.iter().filter(|t| t.unread > 0).collect())
        .unwrap_or(Vec::new());
    unread.sort_by(|a, b| {
        let r = if a.name.starts_with('@') && b.name.starts_with('@') {
            a.name.cmp(&b.name)
        } else if a.name.starts_with('@') {
            Ordering::Less
        } else if b.name.starts_with('@') {
            Ordering::Greater
        } else {
            a.name.cmp(&b.name)
        };
        return r;
    });
    let tags_open = use_state(|| false);
    let force_tags_open = unread.is_empty();
    aside![
        C!["p-2"],
        IF!(!unread.is_empty() => p![C!["uppercase", "font-bold"], "Unread"]),
        IF!(!unread.is_empty() => div![C!["flex","flex-col"], inner_view_tags(unread.into_iter(), true)]),
        p![
            span![C!["uppercase", "font-bold", "pr-2"], "Tags"],
            IF!(!force_tags_open =>
            i![C![
                "fa-solid",
                if tags_open.get() {
                    "fa-angle-up"
                } else {
                    "fa-angle-down"
                }
            ]]),
            tags_open.on_click(|t| *t = !*t)
        ],
        div![
            IF!(force_tags_open||tags_open.get() => tags.as_ref().map(|t| inner_view_tags(t.iter(),false))),
        ]
    ]
}

// TODO: add cathup_mode:bool and hide elements when true
fn news_post(
    post: &ShowThreadQueryThreadOnNewsPost,
    content_el: &ElRef<HtmlElement>,
    catchup_mode: bool,
    read_completion_ratio: f64,
) -> Node<Msg> {
    let subject = &post.title;
    set_title(subject);
    let read_thread_id = post.thread_id.clone();
    let unread_thread_id = post.thread_id.clone();
    fn tag(tag: String) -> Node<Msg> {
        let hex = compute_color(&tag);
        let style = style! {St::BackgroundColor=>hex};
        let attrs = attrs! {
            At::Href => urls::search(&format!("tag:{tag}"), 0)
        };
        let tag = tag.clone();
        a![
            attrs,
            span![C![&tw_classes::TAG], style, &tag],
            ev(Ev::Click, move |_| Msg::FrontPageRequest {
                query: format!("tag:{tag}"),
                after: None,
                before: None,
                first: None,
                last: None,
            })
        ]
    }

    let mut title_break = None;
    const TITLE_LENGTH_WRAP_LIMIT: usize = 40;
    for w in subject.split_whitespace() {
        if w.len() > TITLE_LENGTH_WRAP_LIMIT {
            title_break = Some(C!["break-all", "text-pretty"]);
        }
    }
    div![
        C!["lg:p-4", "max-w-4xl"],
        div![
            C!["p-4", "lg:p-0"],
            h3![C!["text-xl"], title_break, subject],
            span![tag(format!("News/{}", post.slug))],
            IF!(!catchup_mode => div![
                C!["pt-4", "gap-2", "flex", "justify-around"],
                div![
                    button![
                        tw_classes::button(),
                        C!["rounded-r-none"],
                        attrs! {At::Title => "Mark as read"},
                        span![i![C!["far", "fa-envelope-open"]]],
                        span![C!["pl-2"], "Read"],
                        ev(Ev::Click, move |_| Msg::MultiMsg(vec![
                            Msg::SetUnread(read_thread_id, false),
                            Msg::GoToSearchResults
                        ])),
                    ],
                    button![
                        tw_classes::button(),
                        C!["rounded-l-none"],
                        attrs! {At::Title => "Mark as unread"},
                        span![i![C!["far", "fa-envelope"]]],
                        span![C!["pl-2"], "Unread"],
                        ev(Ev::Click, move |_| Msg::MultiMsg(vec![
                            Msg::SetUnread(unread_thread_id, true),
                            Msg::GoToSearchResults
                        ])),
                    ],
                ],
                // Placeholder for symmetry with email view that has Spam button
                div![],
            ]),
        ],
        div![
            C!["lg:mt-4"],
            div![render_news_post_header(&post)],
            div![
                C![
                    "bg-white",
                    "text-black",
                    "p-4",
                    "lg:mb-4",
                    "min-w-full",
                    "overflow-x-auto",
                    "news-post",
                    format!("site-{}", post.slug)
                ],
                el_ref(content_el),
                raw![&post.body]
            ]
        ],
        IF!(!catchup_mode => click_to_top()),
        reading_progress(read_completion_ratio)
    ]
}
fn render_news_post_header(post: &ShowThreadQueryThreadOnNewsPost) -> Node<Msg> {
    let from = &post.site;
    let id = post.thread_id.clone();
    let is_unread = !post.is_read;
    let url = &post.url;
    let idx = url
        .match_indices('/')
        .nth(2)
        .map(|(idx, _)| idx)
        .unwrap_or(url.len());
    let domain = &url[..idx];
    let add_archive_url = format!("https://archive.is/?url={url}");
    let view_archive_url = format!("https://archive.is/newest/{url}");
    let favicon = div![
        C![
            "w-16",
            "h-16",
            "text-4xl",
            "[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]",
            "font-extrabold",
            "grid",
            "place-content-center",
            "uppercase"
        ],
        img![
            C!["object-cover", "w-16", "h-16"],
            attrs! {At::Src=>format!("https://www.google.com/s2/favicons?sz=64&domain={domain}")}
        ],
    ];
    div![
        C!["flex", "p-4", "bg-neutral-800"],
        div![favicon],
        div![
            C!["px-4", "mr-auto", "flex-1"],
            div![
                div![
                    C!["flex"],
                    div![C!["font-semibold", "text-sm", "flex-1"], from],
                    snooze_buttons(Some(post.timestamp), &id),
                ],
                div![
                    C!["flex", "gap-2", "pt-2", "text-sm"],
                    a![
                        tw_classes::button(),
                        attrs! {
                            At::Href => post.url,
                            At::Target => "_blank",
                        },
                        span![C!["hidden", "md:inline"], "Source "],
                        i![C!["fas", "fa-up-right-from-square"]],
                    ],
                    a![
                        tw_classes::button(),
                        attrs! {
                            At::Href => add_archive_url,
                            At::Target => "_blank",
                        },
                        span![C!["hidden", "md:inline"], "Archive "],
                        i![C!["fas", "fa-plus"]],
                    ],
                    a![
                        tw_classes::button(),
                        attrs! {
                            At::Href => view_archive_url,
                            At::Target => "_blank",
                        },
                        span![C!["hidden", "md:inline"], "Archive "],
                        i![C!["fas", "fa-magnifying-glass"]],
                    ]
                ]
            ]
        ],
        span![
            C!["text-right"],
            div![C!["text-xs", "text-nowrap"], human_age(post.timestamp)],
            i![C![
                "mx-4"
                "read-status",
                "far",
                if is_unread {
                    "fa-envelope"
                } else {
                    "fa-envelope-open"
                },
            ]],
            ev(Ev::Click, move |e| {
                e.stop_propagation();
                Msg::SetUnread(id, !is_unread)
            })
        ]
    ]
}
fn reading_progress(ratio: f64) -> Node<Msg> {
    let percent = ratio * 100.;
    div![
        C![
            "fixed",
            "top-0",
            "left-0",
            "w-full",
            "h-1",
            "bg-gray-200",
            IF!(percent<1. => "hidden")
        ],
        div![
            C!["h-1", "bg-green-500"],
            style! {
                St::Width => format!("{}%", percent)
            }
        ]
    ]
}
pub fn view_versions(versions: &Version) -> Node<Msg> {
    aside![
        C!["p-2"],
        p![C!["uppercase", "font-bold"], "Versions"],
        p![C!["pl-2"], "Client"],
        p![C!["pl-4"], &versions.client],
        versions
            .server
            .as_ref()
            .map(|v| vec![p![C!["pl-2"], "Server"], p![C!["pl-4"], v]])
    ]
}

fn click_to_top() -> Node<Msg> {
    button![
        tw_classes::button_with_color("bg-red-500", "hover:bg-neutral-700"),
        C!["lg:m-0", "m-4"],
        span!["Top"],
        span![i![C!["fas", "fa-arrow-turn-up"]]],
        ev(Ev::Click, |_| Msg::ScrollToTop)
    ]
}

fn snooze_buttons(timestamp: Option<i64>, id: &str) -> Node<Msg> {
    div![
        span![C!["px-2"], "⏰"],
        button![
            tw_classes::button(),
            C!["rounded-r-none"],
            "1d",
            ev(Ev::Click, {
                let id = id.to_string();
                move |e| {
                    e.stop_propagation();
                    Msg::Snooze(id, Utc::now() + chrono::Days::new(1))
                }
            })
        ],
        button![
            tw_classes::button(),
            C!["rounded-none"],
            "7d",
            ev(Ev::Click, {
                let id = id.to_string();
                move |e| {
                    e.stop_propagation();
                    Msg::Snooze(id, Utc::now() + chrono::Days::new(7))
                }
            })
        ],
        timestamp.map(
            |ts| chrono::DateTime::from_timestamp(ts, 0).map(|ts| button![
                tw_classes::button(),
                C!["rounded-l-none"],
                "+6m",
                ev(Ev::Click, {
                    let id = id.to_string();
                    move |e| {
                        e.stop_propagation();
                        Msg::Snooze(id, ts + chrono::Days::new(180))
                    }
                })
            ])
        ),
    ]
}
