use std::{
    collections::{HashMap, HashSet},
    fs::File,
};

use letterbox_notmuch::Notmuch;
use letterbox_shared::{compute_color, Rule};
use mailparse::{parse_content_type, parse_mail, MailHeader, MailHeaderMap, ParsedMail};
use memmap::MmapOptions;
use sqlx::{types::Json, PgPool};
use tracing::{error, info, info_span, instrument, warn};

use crate::{
    compute_offset_limit,
    error::ServerError,
    graphql::{
        Attachment, Body, Corpus, DispositionType, Email, EmailThread, Header, Html, Message,
        PlainText, Tag, Thread, ThreadSummary, UnhandledContentType,
    },
    linkify_html, InlineStyle, Query, SanitizeHtml, Transformer,
};

const IMAGE_JPEG: &'static str = "image/jpeg";
const IMAGE_PJPEG: &'static str = "image/pjpeg";
const IMAGE_PNG: &'static str = "image/png";
const MESSAGE_RFC822: &'static str = "message/rfc822";
const MULTIPART_ALTERNATIVE: &'static str = "multipart/alternative";
const MULTIPART_MIXED: &'static str = "multipart/mixed";
const MULTIPART_RELATED: &'static str = "multipart/related";
const TEXT_HTML: &'static str = "text/html";
const TEXT_PLAIN: &'static str = "text/plain";

const MAX_RAW_MESSAGE_SIZE: usize = 100_000;

fn is_notmuch_query(query: &Query) -> bool {
    query.is_notmuch || query.corpus == Some(Corpus::Notmuch)
}

pub fn is_notmuch_thread_or_id(id: &str) -> bool {
    id.starts_with("id:") || id.starts_with("thread:")
}

// TODO(wathiede): decide good error type
pub fn threadset_to_messages(
    thread_set: letterbox_notmuch::ThreadSet,
) -> Result<Vec<Message>, ServerError> {
    for t in thread_set.0 {
        for _tn in t.0 {}
    }
    Ok(Vec::new())
}

#[instrument(name="nm::count", skip_all, fields(query=%query))]
pub async fn count(nm: &Notmuch, query: &Query) -> Result<usize, ServerError> {
    if !is_notmuch_query(query) {
        return Ok(0);
    }
    let query = query.to_notmuch();
    Ok(nm.count(&query)?)
}

#[instrument(name="nm::search", skip_all, fields(query=%query))]
pub async fn search(
    nm: &Notmuch,
    after: Option<i32>,
    before: Option<i32>,
    first: Option<i32>,
    last: Option<i32>,
    query: &Query,
) -> Result<Vec<(i32, ThreadSummary)>, async_graphql::Error> {
    if !is_notmuch_query(query) {
        return Ok(Vec::new());
    }
    let query = query.to_notmuch();
    let (offset, mut limit) = compute_offset_limit(after, before, first, last);
    if before.is_none() {
        // When searching forward, the +1 is to see if there are more pages of data available.
        // Searching backwards implies there's more pages forward, because the value represented by
        // `before` is on the next page.
        limit = limit + 1;
    }
    Ok(nm
        .search(&query, offset as usize, limit as usize)?
        .0
        .into_iter()
        .enumerate()
        .map(|(i, ts)| {
            (
                offset + i as i32,
                ThreadSummary {
                    thread: format!("thread:{}", ts.thread),
                    timestamp: ts.timestamp,
                    date_relative: ts.date_relative,
                    matched: ts.matched,
                    total: ts.total,
                    authors: ts.authors,
                    subject: ts.subject,
                    tags: ts.tags,
                    corpus: Corpus::Notmuch,
                },
            )
        })
        .collect())
}

#[instrument(name="nm::tags", skip_all, fields(needs_unread=needs_unread))]
pub fn tags(nm: &Notmuch, needs_unread: bool) -> Result<Vec<Tag>, ServerError> {
    let unread_msg_cnt: HashMap<String, usize> = if needs_unread {
        // 10000 is an arbitrary number, if there's more than 10k unread messages, we'll
        //  get an inaccurate count.
        nm.search("is:unread", 0, 10000)?
            .0
            .iter()
            .fold(HashMap::new(), |mut m, ts| {
                ts.tags.iter().for_each(|t| {
                    m.entry(t.clone()).and_modify(|c| *c += 1).or_insert(1);
                });
                m
            })
    } else {
        HashMap::new()
    };
    let tags: Vec<_> = nm
        .tags()?
        .into_iter()
        .map(|tag| {
            let hex = compute_color(&tag);
            let unread = if needs_unread {
                *unread_msg_cnt.get(&tag).unwrap_or(&0)
            } else {
                0
            };
            Tag {
                name: tag,
                fg_color: "white".to_string(),
                bg_color: hex,
                unread,
            }
        })
        .chain(
            nm.unread_recipients()?
                .into_iter()
                .filter_map(|(name, unread)| {
                    let Some(idx) = name.find('@') else {
                        return None;
                    };
                    let name = format!("{}/{}", &name[idx..], &name[..idx]);
                    let bg_color = compute_color(&name);
                    Some(Tag {
                        name,
                        fg_color: "white".to_string(),
                        bg_color,
                        unread,
                    })
                }),
        )
        .collect();
    Ok(tags)
}

#[instrument(name="nm::thread", skip_all, fields(thread_id=thread_id))]
pub async fn thread(
    nm: &Notmuch,
    pool: &PgPool,
    thread_id: String,
    debug_content_tree: bool,
) -> Result<Thread, ServerError> {
    // TODO(wathiede): normalize all email addresses through an address book with preferred
    // display names (that default to the most commonly seen name).
    let mut messages = Vec::new();
    for (path, id) in std::iter::zip(nm.files(&thread_id)?, nm.message_ids(&thread_id)?) {
        let tags = nm.tags_for_query(&format!("id:{id}"))?;
        let file = File::open(&path)?;
        let mmap = unsafe { MmapOptions::new().map(&file)? };
        let m = parse_mail(&mmap)?;
        let from = email_addresses(&path, &m, "from")?;
        let mut from = match from.len() {
            0 => None,
            1 => from.into_iter().next(),
            _ => {
                warn!(
                    "Got {} from addresses in message, truncating: {:?}",
                    from.len(),
                    from
                );
                from.into_iter().next()
            }
        };
        match from.as_mut() {
            Some(from) => {
                if let Some(addr) = from.addr.as_mut() {
                    let photo_url = photo_url_for_email_address(&pool, &addr).await?;
                    from.photo_url = photo_url;
                }
            }
            _ => (),
        }

        let to = email_addresses(&path, &m, "to")?;
        let cc = email_addresses(&path, &m, "cc")?;
        let delivered_to = email_addresses(&path, &m, "delivered-to")?.pop();
        let x_original_to = email_addresses(&path, &m, "x-original-to")?.pop();
        let subject = m.headers.get_first_value("subject");
        let timestamp = m
            .headers
            .get_first_value("date")
            .and_then(|d| mailparse::dateparse(&d).ok());
        let cid_prefix = letterbox_shared::urls::cid_prefix(None, &id);
        let base_url = None;
        let mut part_addr = Vec::new();
        part_addr.push(id.to_string());
        let body = match extract_body(&m, &mut part_addr)? {
            Body::PlainText(PlainText { text, content_tree }) => {
                let text = if text.len() > MAX_RAW_MESSAGE_SIZE {
                    format!(
                        "{}...\n\nMESSAGE WAS TRUNCATED @ {} bytes",
                        &text[..MAX_RAW_MESSAGE_SIZE],
                        MAX_RAW_MESSAGE_SIZE
                    )
                } else {
                    text
                };

                Body::Html(Html {
                    html: {
                        let body_tranformers: Vec<Box<dyn Transformer>> = vec![
                            Box::new(InlineStyle),
                            Box::new(SanitizeHtml {
                                cid_prefix: &cid_prefix,
                                base_url: &base_url,
                            }),
                        ];
                        let mut html = linkify_html(&text.trim_matches('\n'));
                        for t in body_tranformers.iter() {
                            if t.should_run(&None, &html) {
                                html = t.transform(&None, &html).await?;
                            }
                        }

                        format!(
                            r#"<p class="view-part-text-plain font-mono whitespace-pre-line">{}</p>"#,
                            // Trim newlines to prevent excessive white space at the beginning/end of
                            // presenation. Leave tabs and spaces incase plain text attempts to center a
                            // header on the first line.
                            html
                        )
                    },
                    content_tree: if debug_content_tree {
                        render_content_type_tree(&m)
                    } else {
                        content_tree
                    },
                })
            }
            Body::Html(Html {
                mut html,
                content_tree,
            }) => Body::Html(Html {
                html: {
                    let body_tranformers: Vec<Box<dyn Transformer>> = vec![
                        // TODO: this breaks things like emails from calendar
                        //Box::new(InlineStyle),
                        Box::new(SanitizeHtml {
                            cid_prefix: &cid_prefix,
                            base_url: &base_url,
                        }),
                    ];
                    for t in body_tranformers.iter() {
                        if t.should_run(&None, &html) {
                            html = t.transform(&None, &html).await?;
                        }
                    }
                    html
                },

                content_tree: if debug_content_tree {
                    render_content_type_tree(&m)
                } else {
                    content_tree
                },
            }),

            Body::UnhandledContentType(UnhandledContentType { content_tree, .. }) => {
                let body_start = mmap
                    .windows(2)
                    .take(20_000)
                    .position(|w| w == b"\n\n")
                    .unwrap_or(0);
                let body = mmap[body_start + 2..].to_vec();
                Body::UnhandledContentType(UnhandledContentType {
                    text: String::from_utf8(body)?,
                    content_tree: if debug_content_tree {
                        render_content_type_tree(&m)
                    } else {
                        content_tree
                    },
                })
            }
        };
        let headers = m
            .headers
            .iter()
            .map(|h| Header {
                key: h.get_key(),
                value: h.get_value(),
            })
            .collect();
        // TODO(wathiede): parse message and fill out attachments
        let attachments = extract_attachments(&m, &id)?;
        messages.push(Message {
            id: format!("id:{id}"),
            from,
            to,
            cc,
            subject,
            tags,
            timestamp,
            headers,
            body,
            path,
            attachments,
            delivered_to,
            x_original_to,
        });
    }
    messages.reverse();
    // Find the first subject that's set. After reversing the vec, this should be the oldest
    // message.
    let subject: String = messages
        .iter()
        .skip_while(|m| m.subject.is_none())
        .next()
        .and_then(|m| m.subject.clone())
        .unwrap_or("(NO SUBJECT)".to_string());
    Ok(Thread::Email(EmailThread {
        thread_id,
        subject,
        messages,
    }))
}

fn email_addresses(
    _path: &str,
    m: &ParsedMail,
    header_name: &str,
) -> Result<Vec<Email>, ServerError> {
    let mut addrs = Vec::new();
    for header_value in m.headers.get_all_values(header_name) {
        match mailparse::addrparse(&header_value) {
            Ok(mal) => {
                for ma in mal.into_inner() {
                    match ma {
                        mailparse::MailAddr::Group(gi) => {
                            if !gi.group_name.contains("ndisclosed") {}
                        }
                        mailparse::MailAddr::Single(s) => addrs.push(Email {
                            name: s.display_name,
                            addr: Some(s.addr),
                            photo_url: None,
                        }), //println!("Single: {s}"),
                    }
                }
            }
            Err(_) => {
                let v = header_value;
                if v.matches('@').count() == 1 {
                    if v.matches('<').count() == 1 && v.ends_with('>') {
                        let idx = v.find('<').unwrap();
                        let addr = &v[idx + 1..v.len() - 1].trim();
                        let name = &v[..idx].trim();
                        addrs.push(Email {
                            name: Some(name.to_string()),
                            addr: Some(addr.to_string()),
                            photo_url: None,
                        });
                    }
                } else {
                    addrs.push(Email {
                        name: Some(v),
                        addr: None,
                        photo_url: None,
                    });
                }
            }
        }
    }
    Ok(addrs)
}

pub fn cid_attachment_bytes(nm: &Notmuch, id: &str, cid: &str) -> Result<Attachment, ServerError> {
    let files = nm.files(id)?;
    let Some(path) = files.first() else {
        warn!("failed to find files for message {id}");
        return Err(ServerError::PartNotFound);
    };
    let file = File::open(&path)?;
    let mmap = unsafe { MmapOptions::new().map(&file)? };
    let m = parse_mail(&mmap)?;
    if let Some(attachment) = walk_attachments(&m, |sp, _cur_idx| {
        info!("{cid} {:?}", get_content_id(&sp.headers));
        if let Some(h_cid) = get_content_id(&sp.headers) {
            let h_cid = &h_cid[1..h_cid.len() - 1];
            if h_cid == cid {
                let attachment = extract_attachment(&sp, id, &[]).unwrap_or(Attachment {
                    ..Attachment::default()
                });
                return Some(attachment);
            }
        }
        None
    }) {
        return Ok(attachment);
    }

    Err(ServerError::PartNotFound)
}

pub fn attachment_bytes(nm: &Notmuch, id: &str, idx: &[usize]) -> Result<Attachment, ServerError> {
    let files = nm.files(id)?;
    let Some(path) = files.first() else {
        warn!("failed to find files for message {id}");
        return Err(ServerError::PartNotFound);
    };
    let file = File::open(&path)?;
    let mmap = unsafe { MmapOptions::new().map(&file)? };
    let m = parse_mail(&mmap)?;
    if let Some(attachment) = walk_attachments(&m, |sp, cur_idx| {
        if cur_idx == idx {
            let attachment = extract_attachment(&sp, id, idx).unwrap_or(Attachment {
                ..Attachment::default()
            });
            return Some(attachment);
        }
        None
    }) {
        return Ok(attachment);
    }

    Err(ServerError::PartNotFound)
}

fn extract_body(m: &ParsedMail, part_addr: &mut Vec<String>) -> Result<Body, ServerError> {
    let body = m.get_body()?;
    let ret = match m.ctype.mimetype.as_str() {
        TEXT_PLAIN => return Ok(Body::text(body)),
        TEXT_HTML => return Ok(Body::html(body)),
        MULTIPART_MIXED => extract_mixed(m, part_addr),
        MULTIPART_ALTERNATIVE => extract_alternative(m, part_addr),
        MULTIPART_RELATED => extract_related(m, part_addr),
        _ => extract_unhandled(m),
    };
    if let Err(err) = ret {
        error!("Failed to extract body: {err:?}");
        return Ok(extract_unhandled(m)?);
    }
    ret
}

fn extract_unhandled(m: &ParsedMail) -> Result<Body, ServerError> {
    let msg = format!(
        "Unhandled body content type:\n{}\n{}",
        render_content_type_tree(m),
        m.get_body()?,
    );
    Ok(Body::UnhandledContentType(UnhandledContentType {
        text: msg,
        content_tree: render_content_type_tree(m),
    }))
}

// multipart/alternative defines multiple representations of the same message, and clients should
// show the fanciest they can display. For this program, the priority is text/html, text/plain,
// then give up.
fn extract_alternative(m: &ParsedMail, part_addr: &mut Vec<String>) -> Result<Body, ServerError> {
    let handled_types = vec![
        MULTIPART_ALTERNATIVE,
        MULTIPART_MIXED,
        MULTIPART_RELATED,
        TEXT_HTML,
        TEXT_PLAIN,
    ];
    for sp in &m.subparts {
        if sp.ctype.mimetype.as_str() == MULTIPART_ALTERNATIVE {
            return extract_alternative(sp, part_addr);
        }
    }
    for sp in &m.subparts {
        if sp.ctype.mimetype.as_str() == MULTIPART_MIXED {
            return extract_mixed(sp, part_addr);
        }
    }
    for sp in &m.subparts {
        if sp.ctype.mimetype.as_str() == MULTIPART_RELATED {
            return extract_related(sp, part_addr);
        }
    }
    for sp in &m.subparts {
        if sp.ctype.mimetype.as_str() == TEXT_HTML {
            let body = sp.get_body()?;
            return Ok(Body::html(body));
        }
    }
    for sp in &m.subparts {
        if sp.ctype.mimetype.as_str() == TEXT_PLAIN {
            let body = sp.get_body()?;
            return Ok(Body::text(body));
        }
    }
    Err(ServerError::StringError(format!(
        "extract_alternative failed to find suitable subpart, searched: {:?}",
        handled_types
    )))
}

// multipart/mixed defines multiple types of context all of which should be presented to the user
// 'serially'.
fn extract_mixed(m: &ParsedMail, part_addr: &mut Vec<String>) -> Result<Body, ServerError> {
    //todo!("add some sort of visual indicator there are unhandled types, i.e. .ics files");
    let handled_types = vec![
        IMAGE_JPEG,
        IMAGE_PJPEG,
        IMAGE_PNG,
        MESSAGE_RFC822,
        MULTIPART_ALTERNATIVE,
        MULTIPART_RELATED,
        TEXT_HTML,
        TEXT_PLAIN,
    ];
    let mut unhandled_types: Vec<_> = m
        .subparts
        .iter()
        .map(|sp| sp.ctype.mimetype.as_str())
        .filter(|mt| !handled_types.contains(&mt))
        .collect();
    unhandled_types.sort();
    if !unhandled_types.is_empty() {
        warn!("{MULTIPART_MIXED} contains the following unhandled mimetypes {unhandled_types:?}");
    }
    let mut parts = Vec::new();
    for (idx, sp) in m.subparts.iter().enumerate() {
        part_addr.push(idx.to_string());
        match sp.ctype.mimetype.as_str() {
            MESSAGE_RFC822 => parts.push(extract_rfc822(&sp, part_addr)?),
            MULTIPART_RELATED => parts.push(extract_related(sp, part_addr)?),
            MULTIPART_ALTERNATIVE => parts.push(extract_alternative(sp, part_addr)?),
            TEXT_PLAIN => parts.push(Body::text(sp.get_body()?)),
            TEXT_HTML => parts.push(Body::html(sp.get_body()?)),
            IMAGE_PJPEG | IMAGE_JPEG | IMAGE_PNG => {
                let pcd = sp.get_content_disposition();
                let filename = pcd
                    .params
                    .get("filename")
                    .map(|s| s.clone())
                    .unwrap_or("".to_string());
                // Only add inline images, attachments are handled as an attribute of the top level Message and rendered separate client-side.
                if pcd.disposition == mailparse::DispositionType::Inline {
                    // TODO: make URL generation more programatic based on what the frontend has
                    // mapped
                    parts.push(Body::html(format!(
                        r#"<img src="/api/view/attachment/{}/{}/{filename}">"#,
                        part_addr[0],
                        part_addr
                            .iter()
                            .skip(1)
                            .map(|i| i.to_string())
                            .collect::<Vec<_>>()
                            .join(".")
                    )));
                }
            }
            mt => parts.push(unhandled_html(MULTIPART_MIXED, mt)),
        }
        part_addr.pop();
    }
    Ok(flatten_body_parts(&parts))
}

fn unhandled_html(parent_type: &str, child_type: &str) -> Body {
    Body::Html(Html {
        html: format!(
            r#"
<div class="p-4 error">
Unhandled mimetype {child_type} in a {parent_type} message
</div>
    "#
        ),
        content_tree: String::new(),
    })
}
fn flatten_body_parts(parts: &[Body]) -> Body {
    let html = parts
        .iter()
        .map(|p| match p {
            Body::PlainText(PlainText { text, .. }) => {
                format!(
                    r#"<p class="view-part-text-plain font-mono whitespace-pre-line">{}</p>"#,
                    // Trim newlines to prevent excessive white space at the beginning/end of
                    // presenation. Leave tabs and spaces incase plain text attempts to center a
                    // header on the first line.
                    linkify_html(&html_escape::encode_text(text).trim_matches('\n'))
                )
            }
            Body::Html(Html { html, .. }) => html.clone(),
            Body::UnhandledContentType(UnhandledContentType { text, .. }) => {
                error!("text len {}", text.len());
                format!(
                    r#"<p class="view-part-unhandled">{}</p>"#,
                    // Trim newlines to prevent excessive white space at the beginning/end of
                    // presenation. Leave tabs and spaces incase plain text attempts to center a
                    // header on the first line.
                    linkify_html(&html_escape::encode_text(text).trim_matches('\n'))
                )
            }
        })
        .collect::<Vec<_>>()
        .join("\n");

    info!("flatten_body_parts {}", parts.len());
    Body::html(html)
}

fn extract_related(m: &ParsedMail, part_addr: &mut Vec<String>) -> Result<Body, ServerError> {
    // TODO(wathiede): collect related things and change return type to new Body arm.
    let handled_types = vec![
        MULTIPART_ALTERNATIVE,
        TEXT_HTML,
        TEXT_PLAIN,
        IMAGE_JPEG,
        IMAGE_PJPEG,
        IMAGE_PNG,
    ];
    let mut unhandled_types: Vec<_> = m
        .subparts
        .iter()
        .map(|sp| sp.ctype.mimetype.as_str())
        .filter(|mt| !handled_types.contains(&mt))
        .collect();
    unhandled_types.sort();
    if !unhandled_types.is_empty() {
        warn!("{MULTIPART_RELATED} contains the following unhandled mimetypes {unhandled_types:?}");
    }

    for (i, sp) in m.subparts.iter().enumerate() {
        if sp.ctype.mimetype == IMAGE_PNG
            || sp.ctype.mimetype == IMAGE_JPEG
            || sp.ctype.mimetype == IMAGE_PJPEG
        {
            info!("sp.ctype {:#?}", sp.ctype);
            //info!("sp.headers {:#?}", sp.headers);
            if let Some(cid) = sp.headers.get_first_value("Content-Id") {
                let mut part_id = part_addr.clone();
                part_id.push(i.to_string());
                info!("cid: {cid} part_id {part_id:?}");
            }
        }
    }
    for sp in &m.subparts {
        if sp.ctype.mimetype == MULTIPART_ALTERNATIVE {
            return extract_alternative(m, part_addr);
        }
    }
    for sp in &m.subparts {
        if sp.ctype.mimetype == TEXT_HTML {
            let body = sp.get_body()?;
            return Ok(Body::html(body));
        }
    }
    for sp in &m.subparts {
        if sp.ctype.mimetype == TEXT_PLAIN {
            let body = sp.get_body()?;
            return Ok(Body::text(body));
        }
    }
    Err(ServerError::StringError(format!(
        "extract_related failed to find suitable subpart, searched: {:?}",
        handled_types
    )))
}

fn walk_attachments<T, F: Fn(&ParsedMail, &[usize]) -> Option<T> + Copy>(
    m: &ParsedMail,
    visitor: F,
) -> Option<T> {
    let mut cur_addr = Vec::new();
    walk_attachments_inner(m, visitor, &mut cur_addr)
}

fn walk_attachments_inner<T, F: Fn(&ParsedMail, &[usize]) -> Option<T> + Copy>(
    m: &ParsedMail,
    visitor: F,
    cur_addr: &mut Vec<usize>,
) -> Option<T> {
    for (idx, sp) in m.subparts.iter().enumerate() {
        cur_addr.push(idx);
        let val = visitor(sp, &cur_addr);
        if val.is_some() {
            return val;
        }
        let val = walk_attachments_inner(sp, visitor, cur_addr);
        if val.is_some() {
            return val;
        }
        cur_addr.pop();
    }
    None
}

// TODO(wathiede): make this walk_attachments that takes a closure.
// Then implement one closure for building `Attachment` and imlement another that can be used to
// get the bytes for serving attachments of HTTP
fn extract_attachments(m: &ParsedMail, id: &str) -> Result<Vec<Attachment>, ServerError> {
    let mut attachments = Vec::new();
    for (idx, sp) in m.subparts.iter().enumerate() {
        if let Some(attachment) = extract_attachment(sp, id, &[idx]) {
            // Filter out inline attachements, they're flattened into the body of the message.
            if attachment.disposition == DispositionType::Attachment {
                attachments.push(attachment);
            }
        }
    }
    Ok(attachments)
}

fn extract_attachment(m: &ParsedMail, id: &str, idx: &[usize]) -> Option<Attachment> {
    let pcd = m.get_content_disposition();
    let pct = m
        .get_headers()
        .get_first_value("Content-Type")
        .map(|s| parse_content_type(&s));
    let filename = match (
        pcd.params.get("filename").map(|f| f.clone()),
        pct.map(|pct| pct.params.get("name").map(|f| f.clone())),
    ) {
        // Use filename from Content-Disposition
        (Some(filename), _) => filename,
        // Use filename from Content-Type
        (_, Some(Some(name))) => name,
        // No known filename, assume it's not an attachment
        _ => return None,
    };
    info!("filename {filename}");

    // TODO: grab this from somewhere
    let content_id = None;
    let bytes = match m.get_body_raw() {
        Ok(bytes) => bytes,
        Err(err) => {
            error!("failed to get body for attachment: {err}");
            return None;
        }
    };
    return Some(Attachment {
        id: id.to_string(),
        idx: idx
            .iter()
            .map(|i| i.to_string())
            .collect::<Vec<_>>()
            .join("."),
        disposition: pcd.disposition.into(),
        filename: Some(filename),
        size: bytes.len(),
        // TODO: what is the default for ctype?
        // TODO: do we want to use m.ctype.params for anything?
        content_type: Some(m.ctype.mimetype.clone()),
        content_id,
        bytes,
    });
}
fn email_address_strings(emails: &[Email]) -> Vec<String> {
    emails
        .iter()
        .map(|e| e.to_string())
        .inspect(|e| info!("e {e}"))
        .collect()
}

fn extract_rfc822(m: &ParsedMail, part_addr: &mut Vec<String>) -> Result<Body, ServerError> {
    fn extract_headers(m: &ParsedMail) -> Result<Body, ServerError> {
        let path = "<in-memory>";
        let from = email_address_strings(&email_addresses(path, &m, "from")?).join(", ");
        let to = email_address_strings(&email_addresses(path, &m, "to")?).join(", ");
        let cc = email_address_strings(&email_addresses(path, &m, "cc")?).join(", ");
        let date = m.headers.get_first_value("date").unwrap_or(String::new());
        let subject = m
            .headers
            .get_first_value("subject")
            .unwrap_or(String::new());
        let text = format!(
            r#"
---------- Forwarded message ----------
From: {from}
To: {to}
CC: {cc}
Date: {date}
Subject: {subject}
"#
        );
        Ok(Body::text(text))
    }
    let inner_body = m.get_body()?;
    let inner_m = parse_mail(inner_body.as_bytes())?;
    let headers = extract_headers(&inner_m)?;
    let body = extract_body(&inner_m, part_addr)?;

    Ok(flatten_body_parts(&[headers, body]))
}

pub fn get_attachment_filename(header_value: &str) -> &str {
    info!("get_attachment_filename {header_value}");
    // Strip last "
    let v = &header_value[..header_value.len() - 1];
    if let Some(idx) = v.rfind('"') {
        &v[idx + 1..]
    } else {
        ""
    }
}

pub fn get_content_type<'a>(headers: &[MailHeader<'a>]) -> Option<String> {
    if let Some(v) = headers.get_first_value("Content-Type") {
        if let Some(idx) = v.find(';') {
            return Some(v[..idx].to_string());
        } else {
            return Some(v);
        }
    }
    None
}

fn get_content_id<'a>(headers: &[MailHeader<'a>]) -> Option<String> {
    headers.get_first_value("Content-Id")
}

fn render_content_type_tree(m: &ParsedMail) -> String {
    const WIDTH: usize = 4;
    const SKIP_HEADERS: [&str; 4] = [
        "Authentication-Results",
        "DKIM-Signature",
        "Received",
        "Received-SPF",
    ];
    fn render_ct_rec(m: &ParsedMail, depth: usize) -> String {
        let mut parts = Vec::new();
        let msg = format!("{} {}", "-".repeat(depth * WIDTH), m.ctype.mimetype);
        parts.push(msg);
        for sp in &m.subparts {
            parts.push(render_ct_rec(sp, depth + 1))
        }
        parts.join("\n")
    }
    fn render_rec(m: &ParsedMail, depth: usize) -> String {
        let mut parts = Vec::new();
        let msg = format!("{} {}", "-".repeat(depth * WIDTH), m.ctype.mimetype);
        parts.push(msg);
        let indent = " ".repeat(depth * WIDTH);
        if !m.ctype.charset.is_empty() {
            parts.push(format!("{indent} Character Set: {}", m.ctype.charset));
        }
        for (k, v) in m.ctype.params.iter() {
            parts.push(format!("{indent} {k}: {v}"));
        }
        if !m.headers.is_empty() {
            parts.push(format!("{indent} == headers =="));
            for h in &m.headers {
                if h.get_key().starts_with('X') {
                    continue;
                }
                if SKIP_HEADERS.contains(&h.get_key().as_str()) {
                    continue;
                }

                parts.push(format!("{indent} {}: {}", h.get_key_ref(), h.get_value()));
            }
        }
        for sp in &m.subparts {
            parts.push(render_rec(sp, depth + 1))
        }
        parts.join("\n")
    }
    format!(
        "Outline:\n{}\n\nDetailed:\n{}\n\nNot showing headers:\n  {}\n  X.*",
        render_ct_rec(m, 1),
        render_rec(m, 1),
        SKIP_HEADERS.join("\n  ")
    )
}

#[instrument(name="nm::set_read_status", skip_all, fields(query=%query, unread=unread))]
pub async fn set_read_status<'ctx>(
    nm: &Notmuch,
    query: &Query,
    unread: bool,
) -> Result<bool, ServerError> {
    let uids: Vec<_> = query
        .uids
        .iter()
        .filter(|uid| is_notmuch_thread_or_id(uid))
        .collect();
    info!("set_read_status({unread} {uids:?})");
    for uid in uids {
        if unread {
            nm.tag_add("unread", uid)?;
        } else {
            nm.tag_remove("unread", uid)?;
        }
    }
    Ok(true)
}

async fn photo_url_for_email_address(
    pool: &PgPool,
    addr: &str,
) -> Result<Option<String>, ServerError> {
    let row = sqlx::query!(
        r#"
SELECT
  url
FROM email_photo ep
JOIN email_address ea
ON ep.id = ea.email_photo_id
WHERE
  address = $1
    "#,
        addr
    )
    .fetch_optional(pool)
    .await?;
    Ok(row.map(|r| r.url))
}

/*
* grab email_rules table from sql
* For each message with `unprocessed` label
*   parse the message
*   pass headers for each message through a matcher using email rules
*   for each match, add label to message
*   if any matches were found, remove unprocessed
* TODO: how to handle inbox label
*/

#[instrument(name="nm::label_unprocessed", skip_all, fields(dryrun=dryrun, limit=?limit, query=%query))]
pub async fn label_unprocessed(
    nm: &Notmuch,
    pool: &PgPool,
    dryrun: bool,
    limit: Option<usize>,
    query: &str,
) -> Result<Box<[String]>, ServerError> {
    use futures::StreamExt;
    let ids = nm.message_ids(query)?;
    info!(
        "Processing {limit:?} of {} messages with '{query}'",
        ids.len()
    );
    let rules: Vec<_> = sqlx::query!(
        r#"
        SELECT rule as "rule: Json<Rule>"
        FROM email_rule
        ORDER BY sort_order
        "#,
    )
    .fetch(pool)
    .map(|r| r.unwrap().rule.0)
    .collect()
    .await;
    /*
        use letterbox_shared::{Match, MatchType};
        let rules = vec![Rule {
            stop_on_match: false,
            matches: vec![Match {
                match_type: MatchType::From,
                needle: "eftours".to_string(),
            }],
            tag: "EFTours".to_string(),
        }];
    */
    info!("Loaded {} rules", rules.len());

    let limit = limit.unwrap_or(ids.len());
    let limit = limit.min(ids.len());
    let ids = &ids[..limit];

    let mut add_mutations = HashMap::new();
    let mut rm_mutations = HashMap::new();
    for id in ids {
        let id = format!("id:{id}");
        let files = nm.files(&id)?;
        // Only process the first file path is multiple files have the same id
        let Some(path) = files.iter().next() else {
            error!("No files for message-ID {id}");
            let t = "Letterbox/Bad";
            nm.tag_add(t, &id)?;
            let t = "unprocessed";
            nm.tag_remove(t, &id)?;
            continue;
        };
        let file = File::open(&path)?;
        info!("parsing {path}");
        let mmap = unsafe { MmapOptions::new().map(&file)? };
        let m = match info_span!("parse_mail", path = path).in_scope(|| parse_mail(&mmap)) {
            Ok(m) => m,
            Err(err) => {
                error!("Failed to parse {path}: {err}");
                let t = "Letterbox/Bad";
                nm.tag_add(t, &id)?;
                let t = "unprocessed";
                nm.tag_remove(t, &id)?;
                continue;
            }
        };
        let (matched_rule, add_tags) = find_tags(&rules, &m.headers);
        if matched_rule {
            if dryrun {
                info!(
                    "\nAdd tags: {add_tags:?}\nTo: {} From: {} Subject: {}\n",
                    m.headers.get_first_value("to").expect("no from header"),
                    m.headers.get_first_value("from").expect("no from header"),
                    m.headers
                        .get_first_value("subject")
                        .expect("no subject header")
                );
            }
            for t in &add_tags {
                //nm.tag_add(t, &id)?;
                add_mutations
                    .entry(t.to_string())
                    .or_insert_with(|| Vec::new())
                    .push(id.clone());
            }
            if add_tags.contains("spam") || add_tags.contains("Spam") {
                //nm.tag_remove("unread", &id)?;
                let t = "unread".to_string();
                rm_mutations
                    .entry(t)
                    .or_insert_with(|| Vec::new())
                    .push(id.clone());
            }
            if !add_tags.contains("inbox") {
                //nm.tag_remove("inbox", &id)?;
                let t = "inbox".to_string();
                rm_mutations
                    .entry(t)
                    .or_insert_with(|| Vec::new())
                    .push(id.clone());
            }
            //nm.tag_remove("unprocessed", &id)?;
        } else {
            if add_tags.is_empty() {
                let t = "Grey".to_string();
                add_mutations
                    .entry(t)
                    .or_insert_with(|| Vec::new())
                    .push(id.clone());
            }
            //nm.tag_remove("inbox", &id)?;
            let t = "inbox".to_string();
            rm_mutations
                .entry(t)
                .or_insert_with(|| Vec::new())
                .push(id.clone());
        }
        let t = "unprocessed".to_string();
        rm_mutations
            .entry(t)
            .or_insert_with(|| Vec::new())
            .push(id.clone());
    }
    info!("Adding {} distinct labels", add_mutations.len());
    for (tag, ids) in add_mutations.iter() {
        info!("  {tag}: {}", ids.len());
        if !dryrun {
            let ids: Vec<_> = ids.iter().map(|s| s.as_str()).collect();
            info_span!("tags_add", tag = tag, count = ids.len())
                .in_scope(|| nm.tags_add(tag, &ids))?;
        }
    }
    info!("Removing {} distinct labels", rm_mutations.len());
    for (tag, ids) in rm_mutations.iter() {
        info!("  {tag}: {}", ids.len());
        if !dryrun {
            let ids: Vec<_> = ids.iter().map(|s| s.as_str()).collect();
            info_span!("tags_remove", tag = tag, count = ids.len())
                .in_scope(|| nm.tags_remove(tag, &ids))?;
        }
    }

    Ok(ids.into())
}
fn find_tags<'a, 'b>(rules: &'a [Rule], headers: &'b [MailHeader]) -> (bool, HashSet<&'a str>) {
    let mut matched_rule = false;
    let mut add_tags = HashSet::new();
    for rule in rules {
        for hdr in headers {
            if rule.is_match(&hdr.get_key(), &hdr.get_value()) {
                //info!("Matched {rule:?}");
                matched_rule = true;
                add_tags.insert(rule.tag.as_str());
                if rule.stop_on_match {
                    return (true, add_tags);
                }
            }
        }
    }
    return (matched_rule, add_tags);
}
