use std::{
    collections::{hash_map::DefaultHasher, HashSet},
    hash::{Hash, Hasher},
};

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

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

mod desktop;
mod mobile;
mod tablet;

// 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;
fn set_title(title: &str) {
    seed::document().set_title(&format!("lb: {}", title));
}

fn tags_chiclet(tags: &[String], is_mobile: bool) -> impl Iterator<Item = Node<Msg>> + '_ {
    tags.iter().map(move |tag| {
        let hex = compute_color(tag);
        let style = style! {St::BackgroundColor=>hex};
        let classes = C!["tag", IF!(is_mobile => "is-small")];
        let tag = tag.clone();
        a![match tag.as_str() {
            "attachment" => span![classes, style, "📎"],
            "replied" => span![classes, style, i![C!["fa-solid", "fa-reply"]]],
            _ => span![classes, style, &tag],
        },]
    })
}

fn removable_tags_chiclet<'a>(
    thread_id: &'a str,
    tags: &'a [String],
    is_mobile: bool,
) -> Node<Msg> {
    div![
        C![
            "message-tags",
            "field",
            "is-grouped",
            "is-grouped-multiline"
        ],
        tags.iter().map(move |tag| {
            let thread_id = thread_id.to_string();
            let hex = compute_color(tag);
            let style = style! {St::BackgroundColor=>hex};
            let classes = C!["tag", IF!(is_mobile => "is-small")];
            let attrs = attrs! {
                At::Href => urls::search(&format!("tag:{tag}"), 0)
            };
            let tag = tag.clone();
            let rm_tag = tag.clone();
            div![
                C!["control"],
                div![
                    C!["tags", "has-addons"],
                    a![
                        classes,
                        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!["tag", "is-delete"],
                        ev(Ev::Click, move |_| Msg::RemoveTag(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
}

fn view_search_results(
    query: &str,
    results: &[FrontPageQuerySearchNodes],
    count: usize,
    pager: &FrontPageQuerySearchPageInfo,
    selected_threads: &HashSet<String>,
    show_icon_text: bool,
) -> Node<Msg> {
    if query.is_empty() {
        set_title("all mail");
    } else {
        set_title(query);
    }
    let show_bulk_edit = !selected_threads.is_empty();
    let all_checked = selected_threads.len() == results.len();
    let partially_checked = !selected_threads.is_empty() && !all_checked;
    let rows = 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 subject = if r.subject.is_empty() {
            "(no subject)"
        } else {
            &r.subject
        };
        tr![
            IF!(unread_idx.is_some() => C!["unread"]),
            td![label![
                C!["b-checkbox", "checkbox"],
                input![attrs! {
                    At::Type=>"checkbox",
                    At::Checked=>selected_threads.contains(&tid).as_at_value(),
                }],
                span![C!["check"]],
                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
                    }
                }),
            ]],
            td![
                C!["from"],
                a![
                    C!["has-text-light", "text"],
                    attrs! {
                        At::Href => urls::thread(&tid)
                    },
                    pretty_authors(&r.authors),
                    IF!(r.total>1 => small![" ", r.total.to_string()]),
                ]
            ],
            td![
                C!["subject"],
                a![
                    tags_chiclet(&tags, false),
                    " ",
                    C!["has-text-light", "text", "subject-link"],
                    attrs! {
                        At::Href => urls::thread(&tid)
                    },
                    &subject,
                ]
            ],
            td![
                C!["date"],
                a![
                    C!["has-text-light", "text"],
                    attrs! {
                        At::Href => urls::thread(&tid)
                    },
                    datetime
                ]
            ]
        ]
    });

    div![
        C!["search-results"],
        search_toolbar(count, pager, show_bulk_edit, show_icon_text),
        table![
            C![
                "table",
                "index",
                "is-fullwidth",
                "is-hoverable",
                "is-narrow",
                "is-striped",
            ],
            thead![tr![
                th![
                    C!["edit"],
                    label![
                        C!["b-checkbox", "checkbox"],
                        input![
                            IF!(partially_checked => C!["is-indeterminate"]),
                            attrs! {
                                At::Type=>"checkbox",
                                At::Checked=>all_checked.as_at_value(),
                            }
                        ],
                        span![C!["check"]],
                        ev(Ev::Click, move |_| if all_checked {
                            Msg::SelectionSetNone
                        } else {
                            Msg::SelectionSetAll
                        })
                    ]
                ],
                th![C!["from"], "From"],
                th![C!["subject"], "Subject"],
                th![C!["date"], "Date"]
            ]],
            tbody![rows]
        ],
        search_toolbar(count, pager, show_bulk_edit, show_icon_text)
    ]
}

fn search_toolbar(
    count: usize,
    pager: &FrontPageQuerySearchPageInfo,
    show_bulk_edit: bool,
    show_icon_text: bool,
) -> Node<Msg> {
    nav![
        C!["level", "is-mobile"],
        div![
            C!["level-left"],
            IF!(show_bulk_edit =>
            div![
                C!["level-item"],
                div![C!["buttons", "has-addons"],
                    button![
                        C!["button", "mark-read"],
                        attrs!{At::Title => "Mark as read"},
                        span![C!["icon", "is-small"], i![C!["far", "fa-envelope-open"]]],
                        IF!(show_icon_text=>span!["Read"]),
                        ev(Ev::Click, |_| Msg::SelectionMarkAsRead)
                    ],
                    button![
                        C!["button", "mark-unread"],
                        attrs!{At::Title => "Mark as unread"},
                        span![C!["icon", "is-small"], i![C!["far", "fa-envelope"]]],
                        IF!(show_icon_text=>span!["Unread"]),
                        ev(Ev::Click, |_| Msg::SelectionMarkAsUnread)
                    ]
                ]
            ]),
            IF!(show_bulk_edit =>
            div![
                C!["level-item"],
                div![C!["buttons", "has-addons"],
                    button![
                        C!["button", "spam"],
                        attrs!{At::Title => "Mark as spam"},
                        span![C!["icon", "is-small"], i![C!["far", "fa-hand"]]],
                        IF!(show_icon_text=>span!["Spam"]),
                        ev(Ev::Click, |_|
                            Msg::MultiMsg(vec![
                                Msg::SelectionAddTag("Spam".to_string()),
                                Msg::SelectionMarkAsRead
                            ])
                        )
                    ],
                ],
            ])
        ],
        div![
            C!["level-right"],
            nav![
                C!["level-item", "pagination"],
                a![
                    C![
                        "pagination-previous",
                        "button",
                        //IF!(!pager.has_previous_page => "is-static"),
                    ],
                    IF!(!pager.has_previous_page => attrs!{ At::Disabled=>true }),
                    "<",
                    IF!(pager.has_previous_page => ev(Ev::Click, |_| Msg::PreviousPage)),
                ],
                a![
                    C![
                        "pagination-next",
                        "button",
                        //IF!(!pager.has_next_page => "is-static")
                    ],
                    IF!(!pager.has_next_page => attrs!{ At::Disabled=>true }),
                    ">",
                    IF!(pager.has_next_page => ev(Ev::Click, |_| Msg::NextPage))
                ],
                ul![C!["pagination-list"], li![format!("{count} results")],],
            ]
        ]
    ]
}

trait Email {
    fn name(&self) -> Option<&str>;
    fn addr(&self) -> Option<&str>;
}

impl<T: Email> Email for &'_ T {
    fn name(&self) -> Option<&str> {
        return (*self).name();
    }
    fn addr(&self) -> Option<&str> {
        return (*self).addr();
    }
}

macro_rules! implement_email {
    ( $($t:ty),+ ) => {$(
        impl Email for $t {
            fn name(&self) -> Option<&str> {
                self.name.as_deref()
            }
            fn addr(&self) -> Option<&str> {
                self.addr.as_deref()
            }
        }
    )+};
}

implement_email!(
    ShowThreadQueryThreadOnEmailThreadMessagesTo,
    ShowThreadQueryThreadOnEmailThreadMessagesCc,
    ShowThreadQueryThreadOnEmailThreadMessagesFrom
);

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"], contents, truncated_msg,]
}

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

fn render_avatar(avatar: Option<String>, from: &str) -> Node<Msg> {
    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();
    if let Some(src) = avatar {
        img![attrs! {At::Src=>src}]
    } else {
        let w = 64;
        let h = 64;
        let from_color = compute_color(from);
        svg![
            attrs! {At::ViewBox=>format!("0 0 {w} {h}") },
            style! {
                St::Display => "block",
                St::FontFamily => "Poppins",
                St::FontSize => pt(32),
            },
            g![
                rect![attrs! {
                    At::Fill=>from_color,
                    At::Stroke=>"black",
                    At::StrokeWidth=>"1",

                    // Round corners
                    //At::Rx => px(10),
                    At::X => 0,
                    At::Y => 0,
                    At::Width => h,
                    At::Height => h,
                }],
                text![
                    attrs! {
                        At::Fill => "white",

                        At::X => percent(50),
                        At::Y => percent(50),
                        At::DominantBaseline => "middle",
                        At::TextAnchor => "middle"
                    },
                    initials
                ]
            ]
        ]
    }
}

fn render_open_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Node<Msg> {
    let (from, from_detail) = match &msg.from {
        Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom {
            name: Some(name),
            addr,
        }) => (name.to_string(), addr.clone()),
        Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom {
            addr: Some(addr), ..
        }) => (addr.to_string(), None),
        _ => (String::from("UNKNOWN"), None),
    };
    // TODO(wathiede): get this from server
    let avatar: Option<String> = None;
    //let avatar: Option<String> = Some(String::from("https://bulma.io/images/placeholders/64x64.png"));
    let id = msg.id.clone();
    let is_unread = has_unread(&msg.tags);
    let img = render_avatar(avatar, &from);
    article![
        C!["media"],
        figure![C!["media-left"], p![C!["image", "is-64x64"], img]],
        div![
            C!["media-content"],
            div![
                C!["content"],
                p![
                    strong![from],
                    br![],
                    small![
                        &from_detail,
                        " ",
                        from_detail.map(|detail| span![
                            i![C!["far", "fa-clone"]],
                            ev(Ev::Click, move |e| {
                                e.stop_propagation();
                                Msg::CopyToClipboard(detail.to_string())
                            })
                        ])
                    ],
                    table![
                        IF!(!msg.to.is_empty() =>
                        tr![
                            td![ "To:" ],
                            //td![ if i==0 { "To" }else { "" } ],
                            td![
                                msg.to.iter().enumerate().map(|(i, to)|
                                    small![
                                        if i>0 { ", " }else { "" },
                                        {
                                            let to = match to {
                                                ShowThreadQueryThreadOnEmailThreadMessagesTo {
                                                    name: Some(name),
                                                    addr:Some(addr),
                                                } => format!("{name} <{addr}>"),
                                                ShowThreadQueryThreadOnEmailThreadMessagesTo {
                                                    name: Some(name),
                                                    addr:None
                                                } => format!("{name}"),
                                                ShowThreadQueryThreadOnEmailThreadMessagesTo {
                                                    addr: Some(addr), ..
                                                } => format!("{addr}"),
                                                _ => String::from("UNKNOWN"),
                                            };
                                            span![
                                                &to, " ",
                                                span![
                                                    i![C!["far", "fa-clone"]],
                                                    ev(Ev::Click, move |e| {
                                                        e.stop_propagation();
                                                        Msg::CopyToClipboard(to)
                                                    })
                                                ]
                                            ]
                                        }

                                    ])
                            ]
                        ]),
                        IF!(!msg.cc.is_empty() =>
                        tr![
                            td![ "CC:" ],
                            td![
                                msg.cc.iter().enumerate().map(|(i, cc)|
                                    small![
                                        if i>0 { ", " }else { "" },
                                        {
                                            let cc = match cc {
                                                ShowThreadQueryThreadOnEmailThreadMessagesCc {
                                                    name: Some(name),
                                                    addr:Some(addr),
                                                } => format!("{name} <{addr}>"),
                                                ShowThreadQueryThreadOnEmailThreadMessagesCc {
                                                    name: Some(name),
                                                    addr:None
                                                } => format!("{name}"),
                                                ShowThreadQueryThreadOnEmailThreadMessagesCc {
                                                    addr: Some(addr), ..
                                                } => format!("<{addr}>"),
                                                _ => String::from("UNKNOWN"),
                                            };
                                            span![
                                                &cc, " ",
                                                span![
                                                    i![C!["far", "fa-clone"]],
                                                    ev(Ev::Click, move |e| {
                                                        e.stop_propagation();
                                                        Msg::CopyToClipboard(cc)
                                                    })
                                                ]
                                            ]
                                        }
                                    ])
                            ]
                        ]),
                        tr![td![
                            attrs! {At::ColSpan=>2},
                            msg.timestamp.map(|ts| span![C!["header"], human_age(ts)])
                        ]]
                    ],
                ],
            ],
        ],
        div![
            C!["media-right"],
            span![
                C!["read-status"],
                i![C![
                    "far",
                    if is_unread {
                        "fa-envelope"
                    } else {
                        "fa-envelope-open"
                    },
                ]]
            ],
            ev(Ev::Click, move |e| {
                e.stop_propagation();
                Msg::SetUnread(id, !is_unread)
            })
        ]
    ]
}

fn render_closed_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Node<Msg> {
    let from: String = match &msg.from {
        Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom {
            name: Some(name), ..
        }) => name.to_string(),
        Some(ShowThreadQueryThreadOnEmailThreadMessagesFrom {
            addr: Some(addr), ..
        }) => addr.to_string(),
        _ => String::from("UNKNOWN"),
    };
    // TODO(wathiede): get this from server
    let avatar: Option<String> = None;
    //let avatar: Option<String> = Some(String::from("https://bulma.io/images/placeholders/64x64.png"));
    let id = msg.id.clone();
    let is_unread = has_unread(&msg.tags);
    let img = render_avatar(avatar, &from);
    article![
        C!["media"],
        figure![C!["media-left"], p![C!["image", "is-64x64"], img]],
        div![
            C!["media-content"],
            div![
                C!["content"],
                p![
                    strong![from],
                    br![],
                    IF!(!msg.to.is_empty() =>
                    nodes![
                        small![" to "],
                        msg.to.iter().enumerate().map(|(i, to)| small![
                            if i > 0 { ", " } else { "" },
                            to.name()
                                .as_ref()
                                .unwrap_or(&to.addr().unwrap_or("(UNKNOWN)"))
                        ]).collect::<Vec<_>>()
                    ]),
                    IF!(!msg.cc.is_empty() =>
                    nodes![
                        small![" cc "],
                        msg.cc.iter().enumerate().map(|(i, cc)| small![
                            if i > 0 { ", " } else { "" },
                            cc.name()
                                .as_ref()
                                .unwrap_or(&cc.addr().unwrap_or("(UNKNOWN)"))
                        ]).collect::<Vec<_>>()
                    ]),
                    br![],
                    msg.timestamp.map(|ts| span![C!["header"], human_age(ts)]),
                ],
            ],
        ],
        div![
            C!["media-right"],
            span![
                C!["read-status"],
                i![
                    C![
                        "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();
    div![
        C!["message"],
        div![
            C!["header"],
            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!["body", "mail"],
            match &msg.body {
                ShowThreadQueryThreadOnEmailThreadMessagesBody::UnhandledContentType(
                    ShowThreadQueryThreadOnEmailThreadMessagesBodyOnUnhandledContentType { contents ,content_tree},
                ) => div![
                    raw_text_message(&contents),
                    div![C!["error"],
                        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"],
                    raw![contents],
                    IF!(!msg.attachments.is_empty() =>
                    div![
                        C!["attachments"],
                        hr![],
                        h2!["Attachments"],
                        div![C!["grid","is-col-min-6"],
                            msg.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 = 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!["attachment", "card"],
                                        a.content_type.as_ref().map(|content_type|
                                            IF!(content_type.starts_with("image/") =>
                                            div![C!["card-image","is-1by1"],
                                                div![
                                                    C!["image","is-1by1"],
                                                    style!{
                                                        St::BackgroundImage=>format!(r#"url("{url}");"#),
                                                        St::BackgroundSize=>"cover",
                                                        St::BackgroundPosition=>"center",
                                                    }
                                                ]
                                            ]
                                        )),
                                        div![C!["card-content"],
                                            div![C!["content"],
                                                &a.filename, br![],
                                                small![   fmtr.format(a.size as f64),"B"]
                                            ]
                                        ],
                                        footer![
                                            C!["card-footer"],
                                            a![C!["card-footer-item"],span![C!["icon"], i![C!["fas", "fa-download"]]],
                                                ev(Ev::Click, move |_| {
                                                    seed::window().location().set_href(&url
                                                    ).expect("failed to set URL");
                                                })
                                            ]
                                        ]
                                    ]
                                })
                        ]
                    ]),
                    view_content_tree(&content_tree),
                ],
            }
        ])
    ]
}

#[topo::nested]
fn thread(
    thread: &ShowThreadQueryThreadOnEmailThread,
    open_messages: &HashSet<String>,
    show_icon_text: bool,
    content_el: &ElRef<HtmlElement>,
) -> 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();
    div![
        C!["thread"],
        h3![C!["is-size-5"], subject],
        span![
            C!["tags"],
            removable_tags_chiclet(&thread.thread_id, &tags, false)
        ],
        div![
            C!["level", "is-mobile"],
            div![
                C!["level-item"],
                div![
                    C!["buttons", "has-addons"],
                    button![
                        C!["button", "mark-read"],
                        attrs! {At::Title => "Mark as read"},
                        span![C!["icon", "is-small"], i![C!["far", "fa-envelope-open"]]],
                        IF!(show_icon_text=>span!["Read"]),
                        ev(Ev::Click, move |_| Msg::SetUnread(read_thread_id, false)),
                    ],
                    button![
                        C!["button", "mark-unread"],
                        attrs! {At::Title => "Mark as unread"},
                        span![C!["icon", "is-small"], i![C!["far", "fa-envelope"]]],
                        IF!(show_icon_text=>span!["Unread"]),
                        ev(Ev::Click, move |_| Msg::SetUnread(unread_thread_id, true)),
                    ],
                ],
            ],
            div![
                C!["level-item"],
                div![
                    C!["buttons", "has-addons"],
                    button![
                        C!["button", "spam"],
                        attrs! {At::Title => "Spam"},
                        span![C!["icon", "is-small"], i![C!["far", "fa-hand"]]],
                        IF!(show_icon_text=>span!["Spam"]),
                        ev(Ev::Click, move |_| Msg::MultiMsg(vec![
                            Msg::AddTag(spam_add_thread_id, "Spam".to_string()),
                            Msg::SetUnread(spam_unread_thread_id, false)
                        ])),
                    ],
                ],
            ],
        ],
        div![el_ref(content_el), messages, click_to_top()],
        /* TODO(wathiede): plumb in orignal id
        a![
        attrs! {At::Href=>api::original(&thread_node.0.as_ref().expect("message missing").id)},
        "Original"
        ],
        */
    ]
}

#[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",
            ev(Ev::Click, move |_| {
                debug_open.set(!debug_open.get());
            })
        ],
        IF!(debug_open.get() =>
            pre![C!["content-tree"], content_tree]),
    ]
}

fn view_header(query: &str, refresh_request: &RefreshingState) -> 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());
    nav![
        C!["navbar"],
        attrs! {At::Role=>"navigation"},
        div![
            C!["navbar-start"],
            a![
                C!["navbar-item", "button", IF![is_error => "is-danger"]],
                span![i![C![
                    "fa-solid",
                    "fa-arrow-rotate-right",
                    "refresh",
                    IF![is_loading => "loading"],
                ]]],
                ev(Ev::Click, |_| Msg::RefreshStart),
            ],
            a![
                C!["navbar-item", "button"],
                attrs! {
                    At::Href => urls::search(unread_query(), 0)
                },
                "Unread",
            ],
            a![
                C!["navbar-item", "button"],
                attrs! {
                    At::Href => urls::search("", 0)
                },
                "All",
            ],
            input![
                C!["navbar-item", "input"],
                attrs! {
                    At::Placeholder => "Search";
                    At::AutoFocus => true.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
                }),
            ]
        ]
    ]
}

// `view` describes what to display.
pub fn view(model: &Model) -> Node<Msg> {
    let win = seed::window();
    let w = win
        .inner_width()
        .expect("window width")
        .as_f64()
        .expect("window width f64");
    let _h = win
        .inner_height()
        .expect("window height")
        .as_f64()
        .expect("window height f64");
    div![match w {
        w if w < 800. => div![C!["mobile"], mobile::view(model)],
        w if w < 1024. => div![C!["tablet"], tablet::view(model)],
        _ => div![C!["desktop"], desktop::view(model)],
    },]
}
pub fn tags(model: &Model) -> Node<Msg> {
    fn view_tag_li(display_name: &str, indent: usize, t: &Tag, search_unread: bool) -> Node<Msg> {
        let href = if search_unread {
            urls::search(&format!("is:unread tag:{}", t.name), 0)
        } else {
            urls::search(&format!("tag:{}", t.name), 0)
        };
        li![a![
            attrs! {
                At::Href => href
            },
            (0..indent).map(|_| span![C!["tag-indent"], ""]),
            i![
                C!["tag-tag", "fa-solid", "fa-tag"],
                style! {
                    //"--fa-primary-color" => t.fg_color,
                    St::Color => t.bg_color,
                },
            ],
            display_name,
            IF!(t.unread>0 => format!(" ({})", t.unread)),
            ev(Ev::Click, |_| {
                // Scroll window to the top when searching for a tag.
                info!("scrolling to the top because you clicked a tag");
                web_sys::window().unwrap().scroll_to_with_x_and_y(0., 0.);
            })
        ]]
    }
    fn matches(a: &[&str], b: &[&str]) -> usize {
        std::iter::zip(a.iter(), b.iter())
            .take_while(|(a, b)| a == b)
            .count()
    }
    fn view_tag_list<'a>(
        tags: impl Iterator<Item = &'a Tag>,
        search_unread: bool,
    ) -> Vec<Node<Msg>> {
        let mut lis = 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 <= parts.len() - 2 && parts.len() > 1 {
                // Synthesize fake tags for proper indenting.
                for i in n..parts.len() - 1 {
                    let display_name = parts[n];
                    lis.push(view_tag_li(
                        &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];
            lis.push(view_tag_li(&display_name, n, t, search_unread));
            last = parts;
        }
        lis
    }
    let unread = model
        .tags
        .as_ref()
        .map(|tags| tags.iter().filter(|t| t.unread > 0).collect())
        .unwrap_or(Vec::new());
    let tags_open = use_state(|| false);
    let force_tags_open = unread.is_empty();
    aside![
        C!["tags-menu", "menu"],
        IF!(!unread.is_empty() => p![C!["menu-label"], "Unread"]),
        IF!(!unread.is_empty() => ul![C!["menu-list"], view_tag_list(unread.into_iter(),true)]),
        p![
            C!["menu-label"],
            IF!(!force_tags_open =>
            i![C![
                "fa-solid",
                if tags_open.get() {
                    "fa-angle-up"
                } else {
                    "fa-angle-down"
                }
            ]]),
            " Tags",
            ev(Ev::Click, move |_| {
                tags_open.set(!tags_open.get());
            })
        ],
        ul![
            C!["menu-list"],
            IF!(force_tags_open||tags_open.get() => model.tags.as_ref().map(|tags| view_tag_list(tags.iter(),false))),
        ]
    ]
}
fn news_post(
    post: &ShowThreadQueryThreadOnNewsPost,
    show_icon_text: bool,
    content_el: &ElRef<HtmlElement>,
) -> Node<Msg> {
    // TODO(wathiede): show per-message subject if it changes significantly from top-level subject
    let subject = &post.title;
    set_title(subject);
    let read_thread_id = post.thread_id.clone();
    let unread_thread_id = post.thread_id.clone();
    div![
        C!["thread"],
        h3![C!["is-size-5"], subject],
        div![
            C!["level", "is-mobile"],
            div![
                C!["level-item"],
                div![
                    C!["buttons", "has-addons"],
                    button![
                        C!["button", "mark-read"],
                        attrs! {At::Title => "Mark as read"},
                        span![C!["icon", "is-small"], i![C!["far", "fa-envelope-open"]]],
                        IF!(show_icon_text=>span!["Read"]),
                        ev(Ev::Click, move |_| Msg::SetUnread(read_thread_id, false)),
                    ],
                    button![
                        C!["button", "mark-unread"],
                        attrs! {At::Title => "Mark as unread"},
                        span![C!["icon", "is-small"], i![C!["far", "fa-envelope"]]],
                        IF!(show_icon_text=>span!["Unread"]),
                        ev(Ev::Click, move |_| Msg::SetUnread(unread_thread_id, true)),
                    ],
                ],
            ],
            // This would be the holder for spam buttons on emails, needed to keep layout
            // consistent
            div![C!["level-item"], div![]]
        ],
        div![
            C!["message"],
            div![C!["header"], render_news_post_header(&post)],
            div![
                C!["body", "news-post", format!("site-{}", post.slug)],
                el_ref(content_el),
                raw![&post.body]
            ]
        ],
        /* TODO(wathiede): plumb in orignal id
        a![
        attrs! {At::Href=>api::original(&thread_node.0.as_ref().expect("message missing").id)},
        "Original"
        ],
        */
        click_to_top(),
    ]
}
fn render_news_post_header(post: &ShowThreadQueryThreadOnNewsPost) -> Node<Msg> {
    let from = &post.site;
    let from_detail = post.url.clone();
    let avatar: Option<String> = None;
    //let avatar: Option<String> = Some(String::from("https://bulma.io/images/placeholders/64x64.png"));
    let id = post.thread_id.clone();
    let is_unread = !post.is_read;
    let img = render_avatar(avatar, &from);
    article![
        C!["media"],
        figure![C!["media-left"], p![C!["image", "is-64x64"], img]],
        div![
            C!["media-content"],
            div![
                C!["content"],
                p![
                    strong![from],
                    br![],
                    small![
                        &from_detail,
                        " ",
                        span![
                            i![C!["far", "fa-clone"]],
                            ev(Ev::Click, move |e| {
                                e.stop_propagation();
                                Msg::CopyToClipboard(from_detail.to_string())
                            })
                        ]
                    ],
                    table![tr![td![
                        attrs! {At::ColSpan=>2},
                        span![C!["header"], human_age(post.timestamp)]
                    ]]],
                ],
            ],
        ],
        div![
            C!["media-right"],
            span![
                C!["read-status"],
                i![C![
                    "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.;
    progress![
        C![
            "read-progress",
            "progress",
            "is-success",
            "is-small",
            IF!(percent<1. => "is-invisible")
        ],
        attrs! {
            At::Value=>percent,
            At::Max=>"100"
        },
        format!("{percent}%")
    ]
}
pub fn versions(versions: &crate::state::Version) -> Node<Msg> {
    debug!("versions {versions:?}");
    aside![
        C!["tags-menu", "menu"],
        p![C!["menu-label"], "Versions"],
        ul![
            C!["menu-list"],
            li!["Client"],
            li![span![C!["tag-indent"], &versions.client]]
        ],
        versions.server.as_ref().map(|v| ul![
            C!["menu-list"],
            li!["Server"],
            li![span![C!["tag-indent"], v]]
        ])
    ]
}

fn click_to_top() -> Node<Msg> {
    button![
        C!["button", "is-danger", "is-small"],
        span!["Top"],
        span![C!["icon"], i![C!["fas", "fa-arrow-turn-up"]]],
        ev(Ev::Click, move |_| web_sys::window()
            .unwrap()
            .scroll_to_with_x_and_y(0., 0.))
    ]
}
