//! # Message summary for chatlist.

use std::borrow::Cow;
use std::fmt;
use std::str;

use anyhow::Result;
use num_traits::FromPrimitive;

use crate::calls::{CallState, call_state};
use crate::chat::Chat;
use crate::constants::Chattype;
use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::message::{Message, MessageState, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::stock_str;
use crate::stock_str::msg_reacted;
use crate::tools::truncate;

/// Prefix displayed before message and separated by ":" in the chatlist.
#[derive(Debug)]
pub enum SummaryPrefix {
    /// Username.
    Username(String),

    /// Stock string saying "Draft".
    Draft(String),

    /// Stock string saying "Me".
    Me(String),
}

impl fmt::Display for SummaryPrefix {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            SummaryPrefix::Username(username) => write!(f, "{username}"),
            SummaryPrefix::Draft(text) => write!(f, "{text}"),
            SummaryPrefix::Me(text) => write!(f, "{text}"),
        }
    }
}

/// Message summary.
#[derive(Debug, Default)]
pub struct Summary {
    /// Part displayed before ":", such as an username or a string "Draft".
    pub prefix: Option<SummaryPrefix>,

    /// Summary text, always present.
    pub text: String,

    /// Message timestamp.
    pub timestamp: i64,

    /// Message state.
    pub state: MessageState,

    /// Message preview image path
    pub thumbnail_path: Option<String>,
}

impl Summary {
    /// Constructs chatlist summary
    /// from the provided message, chat and message author contact snapshots.
    pub async fn new_with_reaction_details(
        context: &Context,
        msg: &Message,
        chat: &Chat,
        contact: Option<&Contact>,
    ) -> Result<Summary> {
        if let Some((reaction_msg, reaction_contact_id, reaction)) = chat
            .get_last_reaction_if_newer_than(context, msg.timestamp_sort)
            .await?
        {
            // there is a reaction newer than the latest message, show that.
            // sorting and therefore date is still the one of the last message,
            // the reaction is is more sth. that overlays temporarily.
            let summary = reaction_msg.get_summary_text_without_prefix(context).await;
            return Ok(Summary {
                prefix: None,
                text: msg_reacted(context, reaction_contact_id, &reaction, &summary).await,
                timestamp: msg.get_timestamp(), // message timestamp (not reaction) to make timestamps more consistent with chats ordering
                state: msg.state, // message state (not reaction) - indicating if it was me sending the last message
                thumbnail_path: None,
            });
        }
        Self::new(context, msg, chat, contact).await
    }

    /// Constructs search result summary
    /// from the provided message, chat and message author contact snapshots.
    pub async fn new(
        context: &Context,
        msg: &Message,
        chat: &Chat,
        contact: Option<&Contact>,
    ) -> Result<Summary> {
        let prefix = if msg.state == MessageState::OutDraft {
            Some(SummaryPrefix::Draft(stock_str::draft(context)))
        } else if msg.from_id == ContactId::SELF {
            if msg.is_info() || msg.viewtype == Viewtype::Call || chat.typ == Chattype::OutBroadcast
            {
                None
            } else {
                Some(SummaryPrefix::Me(stock_str::self_msg(context)))
            }
        } else if chat.typ == Chattype::Group
            || chat.typ == Chattype::Mailinglist
            || chat.is_self_talk()
        {
            if msg.is_info() || contact.is_none() {
                None
            } else {
                msg.get_override_sender_name()
                    .or_else(|| contact.map(|contact| msg.get_sender_name(contact)))
                    .map(SummaryPrefix::Username)
            }
        } else {
            None
        };

        let mut text = msg.get_summary_text(context).await;

        if text.is_empty() && msg.quoted_text().is_some() {
            text = stock_str::reply_noun(context)
        }

        let thumbnail_path = if msg.viewtype == Viewtype::Image
            || msg.viewtype == Viewtype::Gif
            || msg.viewtype == Viewtype::Sticker
        {
            msg.get_file(context)
                .and_then(|path| path.to_str().map(|p| p.to_owned()))
        } else if msg.viewtype == Viewtype::Webxdc {
            Some("webxdc-icon://last-msg-id".to_string())
        } else {
            None
        };

        Ok(Summary {
            prefix,
            text,
            timestamp: msg.get_timestamp(),
            state: msg.state,
            thumbnail_path,
        })
    }

    /// Returns the [`Summary::text`] attribute truncated to an approximate length.
    pub fn truncated_text(&self, approx_chars: usize) -> Cow<'_, str> {
        truncate(&self.text, approx_chars)
    }
}

impl Message {
    /// Returns a summary text.
    pub(crate) async fn get_summary_text(&self, context: &Context) -> String {
        let summary = self.get_summary_text_without_prefix(context).await;

        if self.is_forwarded() {
            format!("{}: {}", stock_str::forwarded(context), summary)
        } else {
            summary
        }
    }

    /// Returns a summary text without "Forwarded:" prefix.
    async fn get_summary_text_without_prefix(&self, context: &Context) -> String {
        let (emoji, type_name, type_file, append_text);
        let viewtype = match self
            .param
            .get_i64(Param::PostMessageViewtype)
            .and_then(Viewtype::from_i64)
        {
            Some(vt) => vt,
            None => self.viewtype,
        };
        match viewtype {
            Viewtype::Image => {
                emoji = Some("📷");
                type_name = Some(stock_str::image(context));
                type_file = None;
                append_text = true;
            }
            Viewtype::Gif => {
                emoji = None;
                type_name = Some(stock_str::gif(context));
                type_file = None;
                append_text = true;
            }
            Viewtype::Sticker => {
                emoji = None;
                type_name = Some(stock_str::sticker(context));
                type_file = None;
                append_text = true;
            }
            Viewtype::Video => {
                emoji = Some("🎥");
                type_name = Some(stock_str::video(context));
                type_file = None;
                append_text = true;
            }
            Viewtype::Voice => {
                emoji = Some("🎤");
                type_name = Some(stock_str::voice_message(context));
                type_file = None;
                append_text = true;
            }
            Viewtype::Audio => {
                emoji = Some("🎵");
                type_name = Some(stock_str::audio(context));
                type_file = self.get_filename();
                append_text = true
            }
            Viewtype::File => {
                emoji = Some("📎");
                type_name = Some(stock_str::file(context));
                type_file = self.get_filename();
                append_text = true
            }
            Viewtype::Webxdc => {
                emoji = Some("📱");
                type_name = None;
                if self.viewtype == Viewtype::Webxdc {
                    type_file = Some(
                        self.get_webxdc_info(context)
                            .await
                            .map(|info| info.name)
                            .unwrap_or_else(|_| "ErrWebxdcName".to_string()),
                    );
                } else {
                    type_file = self.get_filename();
                }
                append_text = true;
            }
            Viewtype::Vcard => {
                emoji = Some("👤");
                type_name = None;
                if self.viewtype == Viewtype::Vcard {
                    type_file = self.param.get(Param::Summary1).map(|s| s.to_string());
                } else {
                    type_file = None;
                }
                append_text = true;
            }
            Viewtype::Call => {
                let call_info = context.load_call_by_id(self.id).await.unwrap_or(None);
                let has_video = call_info.is_some_and(|c| c.has_video_initially());
                let call_state = call_state(context, self.id)
                    .await
                    .unwrap_or(CallState::Alerting);
                emoji = Some(if has_video { "🎥" } else { "📞" });
                type_name = Some(match call_state {
                    CallState::Alerting | CallState::Active | CallState::Completed { .. } => {
                        if self.from_id == ContactId::SELF {
                            stock_str::outgoing_call(context, has_video)
                        } else {
                            stock_str::incoming_call(context, has_video)
                        }
                    }
                    CallState::Missed => stock_str::missed_call(context),
                    CallState::Declined => stock_str::declined_call(context),
                    CallState::Canceled => stock_str::canceled_call(context),
                });
                type_file = None;
                append_text = false
            }
            Viewtype::Text | Viewtype::Unknown => {
                emoji = None;
                if self.param.get_cmd() == SystemMessage::LocationOnly {
                    type_name = Some(stock_str::location(context));
                    type_file = None;
                    append_text = false;
                } else {
                    type_name = None;
                    type_file = None;
                    append_text = true;
                }
            }
        };

        let text = self.text.clone();

        let summary = if let Some(type_file) = type_file {
            if append_text && !text.is_empty() {
                format!("{type_file} – {text}")
            } else {
                type_file
            }
        } else if append_text && !text.is_empty() {
            if emoji.is_some() {
                text
            } else if let Some(type_name) = type_name {
                format!("{type_name} – {text}")
            } else {
                text
            }
        } else if let Some(type_name) = type_name {
            type_name
        } else {
            "".to_string()
        };

        let summary = if let Some(emoji) = emoji {
            format!("{emoji} {summary}")
        } else {
            summary
        };

        summary.split_whitespace().collect::<Vec<&str>>().join(" ")
    }
}

#[cfg(test)]
/// Asserts that the summary text with and w/o prefix is `expected`.
pub async fn assert_summary_texts(msg: &Message, ctx: &Context, expected: &str) {
    assert_eq!(msg.get_summary_text(ctx).await, expected);
    assert_eq!(msg.get_summary_text_without_prefix(ctx).await, expected);
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use super::*;
    use crate::chat::ChatId;
    use crate::param::Param;
    use crate::test_utils::TestContext;

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_get_summary_text() {
        let d = TestContext::new_alice().await;
        let ctx = &d.ctx;
        let chat_id = ChatId::create_for_contact(ctx, ContactId::SELF)
            .await
            .unwrap();
        let some_text = " bla \t\n\tbla\n\t".to_string();

        async fn write_file_to_blobdir(d: &TestContext) -> PathBuf {
            let bytes = &[38, 209, 39, 29]; // Just some random bytes
            let file = d.get_blobdir().join("random_filename_392438");
            tokio::fs::write(&file, bytes).await.unwrap();
            file
        }

        let msg = Message::new_text(some_text.to_string());
        assert_summary_texts(&msg, ctx, "bla bla").await; // for simple text, the type is not added to the summary

        let file = write_file_to_blobdir(&d).await;
        let mut msg = Message::new(Viewtype::Image);
        msg.set_file_and_deduplicate(&d, &file, Some("foo.jpg"), None)
            .unwrap();
        assert_summary_texts(&msg, ctx, "📷 Image").await; // file names are not added for images

        let file = write_file_to_blobdir(&d).await;
        let mut msg = Message::new(Viewtype::Image);
        msg.set_text(some_text.to_string());
        msg.set_file_and_deduplicate(&d, &file, Some("foo.jpg"), None)
            .unwrap();
        assert_summary_texts(&msg, ctx, "📷 bla bla").await; // type is visible by emoji if text is set

        let file = write_file_to_blobdir(&d).await;
        let mut msg = Message::new(Viewtype::Video);
        msg.set_file_and_deduplicate(&d, &file, Some("foo.mp4"), None)
            .unwrap();
        assert_summary_texts(&msg, ctx, "🎥 Video").await; // file names are not added for videos

        let file = write_file_to_blobdir(&d).await;
        let mut msg = Message::new(Viewtype::Video);
        msg.set_text(some_text.to_string());
        msg.set_file_and_deduplicate(&d, &file, Some("foo.mp4"), None)
            .unwrap();
        assert_summary_texts(&msg, ctx, "🎥 bla bla").await; // type is visible by emoji if text is set

        let file = write_file_to_blobdir(&d).await;
        let mut msg = Message::new(Viewtype::Gif);
        msg.set_file_and_deduplicate(&d, &file, Some("foo.gif"), None)
            .unwrap();
        assert_summary_texts(&msg, ctx, "GIF").await; // file names are not added for GIFs

        let file = write_file_to_blobdir(&d).await;
        let mut msg = Message::new(Viewtype::Gif);
        msg.set_text(some_text.to_string());
        msg.set_file_and_deduplicate(&d, &file, Some("foo.gif"), None)
            .unwrap();
        assert_summary_texts(&msg, ctx, "GIF \u{2013} bla bla").await; // file names are not added for GIFs

        let file = write_file_to_blobdir(&d).await;
        let mut msg = Message::new(Viewtype::Sticker);
        msg.set_file_and_deduplicate(&d, &file, Some("foo.png"), None)
            .unwrap();
        assert_summary_texts(&msg, ctx, "Sticker").await; // file names are not added for stickers

        let file = write_file_to_blobdir(&d).await;
        let mut msg = Message::new(Viewtype::Voice);
        msg.set_file_and_deduplicate(&d, &file, Some("foo.mp3"), None)
            .unwrap();
        assert_summary_texts(&msg, ctx, "🎤 Voice message").await; // file names are not added for voice messages

        let file = write_file_to_blobdir(&d).await;
        let mut msg = Message::new(Viewtype::Voice);
        msg.set_text(some_text.clone());
        msg.set_file_and_deduplicate(&d, &file, Some("foo.mp3"), None)
            .unwrap();
        assert_summary_texts(&msg, ctx, "🎤 bla bla").await;

        let file = write_file_to_blobdir(&d).await;
        let mut msg = Message::new(Viewtype::Audio);
        msg.set_file_and_deduplicate(&d, &file, Some("foo.mp3"), None)
            .unwrap();
        assert_summary_texts(&msg, ctx, "🎵 foo.mp3").await; // file name is added for audio

        let file = write_file_to_blobdir(&d).await;
        let mut msg = Message::new(Viewtype::Audio);
        msg.set_text(some_text.clone());
        msg.set_file_and_deduplicate(&d, &file, Some("foo.mp3"), None)
            .unwrap();
        assert_summary_texts(&msg, ctx, "🎵 foo.mp3 \u{2013} bla bla").await; // file name and text added for audio

        let mut msg = Message::new(Viewtype::File);
        let bytes = include_bytes!("../test-data/webxdc/with-minimal-manifest.xdc");
        msg.set_file_from_bytes(ctx, "foo.xdc", bytes, None)
            .unwrap();
        chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
        assert_eq!(msg.viewtype, Viewtype::Webxdc);
        assert_summary_texts(&msg, ctx, "📱 nice app!").await;
        msg.set_text(some_text.clone());
        chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
        assert_summary_texts(&msg, ctx, "📱 nice app! \u{2013} bla bla").await;

        let file = write_file_to_blobdir(&d).await;
        let mut msg = Message::new(Viewtype::File);
        msg.set_file_and_deduplicate(&d, &file, Some("foo.bar"), None)
            .unwrap();
        assert_summary_texts(&msg, ctx, "📎 foo.bar").await; // file name is added for files

        let file = write_file_to_blobdir(&d).await;
        let mut msg = Message::new(Viewtype::File);
        msg.set_text(some_text.clone());
        msg.set_file_and_deduplicate(&d, &file, Some("foo.bar"), None)
            .unwrap();
        assert_summary_texts(&msg, ctx, "📎 foo.bar \u{2013} bla bla").await; // file name is added for files

        let mut msg = Message::new(Viewtype::Vcard);
        msg.set_file_from_bytes(ctx, "foo.vcf", b"", None).unwrap();
        chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
        // If a vCard can't be parsed, the message becomes `Viewtype::File`.
        assert_eq!(msg.viewtype, Viewtype::File);
        assert_summary_texts(&msg, ctx, "📎 foo.vcf").await;
        msg.set_text(some_text.clone());
        assert_summary_texts(&msg, ctx, "📎 foo.vcf \u{2013} bla bla").await;

        for vt in [Viewtype::Vcard, Viewtype::File] {
            let mut msg = Message::new(vt);
            msg.set_file_from_bytes(
                ctx,
                "alice.vcf",
                b"BEGIN:VCARD\n\
                  VERSION:4.0\n\
                  FN:Alice Wonderland\n\
                  EMAIL;TYPE=work:alice@example.org\n\
                  END:VCARD",
                None,
            )
            .unwrap();
            chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
            assert_eq!(msg.viewtype, Viewtype::Vcard);
            assert_summary_texts(&msg, ctx, "👤 Alice Wonderland").await;
        }

        // Forwarded
        let mut msg = Message::new_text(some_text.clone());
        msg.param.set_int(Param::Forwarded, 1);
        assert_eq!(msg.get_summary_text(ctx).await, "Forwarded: bla bla"); // for simple text, the type is not added to the summary
        assert_eq!(msg.get_summary_text_without_prefix(ctx).await, "bla bla"); // skipping prefix used for reactions summaries

        let file = write_file_to_blobdir(&d).await;
        let mut msg = Message::new(Viewtype::File);
        msg.set_text(some_text.clone());
        msg.set_file_and_deduplicate(&d, &file, Some("foo.bar"), None)
            .unwrap();
        msg.param.set_int(Param::Forwarded, 1);
        assert_eq!(
            msg.get_summary_text(ctx).await,
            "Forwarded: 📎 foo.bar \u{2013} bla bla"
        );
        assert_eq!(
            msg.get_summary_text_without_prefix(ctx).await,
            "📎 foo.bar \u{2013} bla bla"
        ); // skipping prefix used for reactions summaries
    }
}
