use crate::transcript::{Transcript, TranscriptAction, TranscriptAttachment, TranscriptMessage};
use chrono::{Datelike, Local, TimeZone};
use cosmic::app::{Core, Task};
use cosmic::dialog::file_chooser::{self, FileFilter};
use cosmic::iced::advanced::text::EllipsizeHeightLimit;
use cosmic::iced::keyboard;
use cosmic::iced::runtime::clipboard;
use cosmic::iced::widget::scrollable::{self, RelativeOffset};
use cosmic::iced::widget::text::{Ellipsize, Wrapping};
use cosmic::iced::window;
use cosmic::iced::window::screenshot::Screenshot;
use cosmic::iced::{self, Alignment, Length, Subscription};
use cosmic::widget::Id;
use cosmic::{Application, ApplicationExt, Element, executor, widget};
use image::ColorType;
use kordophoned_client as daemon;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::time::Duration;
use url::Url;

#[derive(Clone, Debug, Default)]
pub struct Flags {
    pub demo: bool,
    pub screenshot_path: Option<PathBuf>,
}

impl Flags {
    pub fn from_env() -> Self {
        let mut flags = Self::default();
        let mut args = std::env::args().skip(1);
        while let Some(arg) = args.next() {
            match arg.as_str() {
                "--demo" => flags.demo = true,
                "--screenshot" => {
                    if let Some(path) = args.next() {
                        flags.screenshot_path = Some(PathBuf::from(path));
                    }
                }
                _ => {}
            }
        }
        flags
    }
}

#[derive(Clone, Debug)]
pub enum Message {
    Tick,
    SelectConversation(String),
    OpenSelectedOnNarrow,
    BackToList,
    ManualSync,
    ComposerChanged(String),
    SendMessage,
    AttachFile,
    FileDialogCancelled,
    FileDialogError(String),
    FileSelected(Url),
    RemoveUpload(String),
    Transcript(TranscriptAction),
    ToggleSettings(bool),
    SettingsServerChanged(String),
    SettingsUsernameChanged(String),
    SettingsPasswordChanged(String),
    SaveSettings,
    CloseError,
    RequestScreenshot,
    Screenshotted(Screenshot),
    ScreenshotSaved(Result<PathBuf, String>),
}

#[derive(Clone, Debug)]
struct ComposerUpload {
    upload_guid: String,
    path: String,
    attachment_guid: Option<String>,
}

pub struct App {
    core: Core,
    request_tx: Option<mpsc::Sender<daemon::Request>>,
    event_rx: Option<mpsc::Receiver<daemon::Event>>,
    conversations: Vec<daemon::ConversationSummary>,
    selected_conversation_id: Option<String>,
    active_conversation_id: Option<String>,
    active_title: String,
    messages: Vec<daemon::ChatMessage>,
    known_message_ids: HashSet<String>,
    fresh_message_ids: HashSet<String>,
    composer: String,
    uploads: Vec<ComposerUpload>,
    attachment_info: HashMap<String, daemon::AttachmentInfo>,
    requested_previews: HashSet<String>,
    pending_open_attachments: HashSet<String>,
    transcript_scroll_id: Id,
    show_list_on_narrow: bool,
    show_settings: bool,
    settings_server_url: String,
    settings_username: String,
    settings_password: String,
    status: Option<String>,
    tick_count: u32,
    screenshot_path: Option<PathBuf>,
    screenshot_requested: bool,
    demo: bool,
}

impl Application for App {
    type Executor = executor::Default;
    type Flags = Flags;
    type Message = Message;

    const APP_ID: &'static str = "net.buzzert.kordophone.Cosmic";

    fn core(&self) -> &Core {
        &self.core
    }

    fn core_mut(&mut self) -> &mut Core {
        &mut self.core
    }

    fn init(core: Core, flags: Self::Flags) -> (Self, Task<Self::Message>) {
        let (request_tx, event_rx, initial_task) = if flags.demo {
            (None, None, Task::none())
        } else {
            let (request_tx, request_rx) = mpsc::channel();
            let (event_tx, event_rx) = mpsc::channel();
            let _worker = daemon::spawn_worker(request_rx, event_tx);
            let _ = request_tx.send(daemon::Request::RefreshConversations);
            let _ = request_tx.send(daemon::Request::LoadSettings);
            (Some(request_tx), Some(event_rx), Task::none())
        };

        let mut app = Self {
            core,
            request_tx,
            event_rx,
            conversations: Vec::new(),
            selected_conversation_id: None,
            active_conversation_id: None,
            active_title: String::new(),
            messages: Vec::new(),
            known_message_ids: HashSet::new(),
            fresh_message_ids: HashSet::new(),
            composer: String::new(),
            uploads: Vec::new(),
            attachment_info: HashMap::new(),
            requested_previews: HashSet::new(),
            pending_open_attachments: HashSet::new(),
            transcript_scroll_id: Id::unique(),
            show_list_on_narrow: true,
            show_settings: false,
            settings_server_url: String::new(),
            settings_username: String::new(),
            settings_password: String::new(),
            status: None,
            tick_count: 0,
            screenshot_path: flags.screenshot_path,
            screenshot_requested: false,
            demo: flags.demo,
        };

        if app.demo {
            app.load_demo_data();
        }

        app.set_header_title("Kordophone".into());
        let title_task = app.set_window_title(
            "Kordophone".into(),
            app.core.main_window_id().expect("main window id"),
        );
        (app, Task::batch([initial_task, title_task]))
    }

    fn header_end(&self) -> Vec<Element<'_, Self::Message>> {
        vec![
            widget::button::icon(widget::icon::from_name("view-refresh-symbolic"))
                .on_press(Message::ManualSync)
                .into(),
            widget::button::icon(widget::icon::from_name("emblem-system-symbolic"))
                .on_press(Message::ToggleSettings(true))
                .into(),
        ]
    }

    fn dialog(&self) -> Option<Element<'_, Self::Message>> {
        if !self.show_settings {
            return None;
        }

        let controls = widget::column::with_capacity(3)
            .spacing(10)
            .push(
                widget::text_input::text_input("Server URL", &self.settings_server_url)
                    .on_input(Message::SettingsServerChanged),
            )
            .push(
                widget::text_input::text_input("Username", &self.settings_username)
                    .on_input(Message::SettingsUsernameChanged),
            )
            .push(
                widget::text_input::secure_input(
                    "Password",
                    &self.settings_password,
                    None::<Message>,
                    true,
                )
                .on_input(Message::SettingsPasswordChanged),
            );

        Some(
            widget::dialog()
                .title("Connection Settings")
                .control(controls)
                .secondary_action(
                    widget::button::standard("Cancel").on_press(Message::ToggleSettings(false)),
                )
                .primary_action(widget::button::suggested("Save").on_press(Message::SaveSettings))
                .into(),
        )
    }

    fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
        match message {
            Message::Tick => {
                self.tick_count = self.tick_count.saturating_add(1);
                let mut tasks = Vec::new();
                while let Some(event) = self.try_recv_event() {
                    tasks.push(self.handle_daemon_event(event));
                }
                if self.screenshot_path.is_some()
                    && !self.screenshot_requested
                    && self.tick_count > 8
                {
                    self.screenshot_requested = true;
                    tasks.push(self.update(Message::RequestScreenshot));
                }
                Task::batch(tasks)
            }
            Message::SelectConversation(id) => self.select_conversation(id, true),
            Message::OpenSelectedOnNarrow => {
                self.show_list_on_narrow = false;
                Task::none()
            }
            Message::BackToList => {
                self.show_list_on_narrow = true;
                Task::none()
            }
            Message::ManualSync => {
                if let Some(id) = self.active_conversation_id.clone() {
                    self.request(daemon::Request::SyncConversation {
                        conversation_id: id,
                    });
                } else {
                    self.request(daemon::Request::SyncConversationList);
                }
                Task::none()
            }
            Message::ComposerChanged(value) => {
                self.composer = value;
                Task::none()
            }
            Message::SendMessage => self.send_message(),
            Message::AttachFile => self.open_file_dialog(),
            Message::FileDialogCancelled => Task::none(),
            Message::FileDialogError(error) => {
                self.status = Some(error);
                Task::none()
            }
            Message::FileSelected(url) => {
                let path = match url.to_file_path() {
                    Ok(path) => path,
                    Err(_) => {
                        self.status = Some(format!("{url} is not a local file"));
                        return Task::none();
                    }
                };
                self.request(daemon::Request::UploadAttachment {
                    path: path.display().to_string(),
                });
                self.status = Some(format!("Uploading {}", filename(&path)));
                Task::none()
            }
            Message::RemoveUpload(upload_guid) => {
                self.uploads
                    .retain(|upload| upload.upload_guid != upload_guid);
                Task::none()
            }
            Message::Transcript(action) => self.handle_transcript_action(action),
            Message::ToggleSettings(show) => {
                self.show_settings = show;
                if show {
                    self.request(daemon::Request::LoadSettings);
                }
                Task::none()
            }
            Message::SettingsServerChanged(value) => {
                self.settings_server_url = value;
                Task::none()
            }
            Message::SettingsUsernameChanged(value) => {
                self.settings_username = value;
                Task::none()
            }
            Message::SettingsPasswordChanged(value) => {
                self.settings_password = value;
                Task::none()
            }
            Message::SaveSettings => {
                self.request(daemon::Request::SaveSettings {
                    server_url: self.settings_server_url.clone(),
                    username: self.settings_username.clone(),
                    password: self.settings_password.clone(),
                });
                Task::none()
            }
            Message::CloseError => {
                self.status = None;
                Task::none()
            }
            Message::RequestScreenshot => window::latest()
                .and_then(window::screenshot)
                .map(|screenshot| cosmic::Action::App(Message::Screenshotted(screenshot))),
            Message::Screenshotted(screenshot) => {
                if let Some(path) = self.screenshot_path.clone() {
                    cosmic::task::future(async move {
                        Message::ScreenshotSaved(save_screenshot(path, screenshot).await)
                    })
                } else {
                    Task::none()
                }
            }
            Message::ScreenshotSaved(result) => {
                match result {
                    Ok(path) => {
                        eprintln!("saved screenshot to {}", path.display());
                        std::process::exit(0);
                    }
                    Err(error) => {
                        self.status = Some(error);
                    }
                }
                Task::none()
            }
        }
    }

    fn subscription(&self) -> Subscription<Self::Message> {
        let tick = iced::time::every(Duration::from_millis(100)).map(|_| Message::Tick);
        let keys = keyboard::listen().filter_map(|event| match event {
            keyboard::Event::KeyPressed {
                key: keyboard::Key::Named(keyboard::key::Named::Escape),
                ..
            } => Some(Message::BackToList),
            _ => None,
        });
        Subscription::batch([tick, keys])
    }

    fn view(&self) -> Element<'_, Self::Message> {
        let content = widget::responsive(|size| {
            if size.width < 760.0 {
                if self.show_list_on_narrow || self.active_conversation_id.is_none() {
                    self.conversation_list(true)
                } else {
                    self.chat_panel(true)
                }
            } else {
                widget::row::with_capacity(2)
                    .push(
                        widget::container(self.conversation_list(false))
                            .width(Length::Fixed(360.0)),
                    )
                    .push(widget::divider::vertical::default())
                    .push(widget::container(self.chat_panel(false)).width(Length::Fill))
                    .height(Length::Fill)
                    .into()
            }
        });

        Element::from(content)
    }
}

impl App {
    fn request(&mut self, request: daemon::Request) {
        if self.demo {
            return;
        }
        match &self.request_tx {
            Some(tx) => {
                if let Err(error) = tx.send(request) {
                    self.status = Some(format!("Failed to talk to daemon worker: {error}"));
                }
            }
            None => {
                self.status = Some("Daemon worker is not available".to_string());
            }
        }
    }

    fn try_recv_event(&mut self) -> Option<daemon::Event> {
        self.event_rx.as_ref().and_then(|rx| rx.try_recv().ok())
    }

    fn handle_daemon_event(&mut self, event: daemon::Event) -> Task<Message> {
        match event {
            daemon::Event::Conversations(conversations) => {
                self.conversations = conversations;
                if self.selected_conversation_id.is_none() {
                    if let Some(first) = self.conversations.first() {
                        return self.select_conversation(first.id.clone(), false);
                    }
                }
            }
            daemon::Event::Messages {
                conversation_id,
                messages,
            } => {
                if self.active_conversation_id.as_deref() == Some(conversation_id.as_str()) {
                    self.fresh_message_ids.clear();
                    for message in &messages {
                        if !self.known_message_ids.contains(&message.id) {
                            self.fresh_message_ids.insert(message.id.clone());
                        }
                    }
                    self.known_message_ids = messages.iter().map(|m| m.id.clone()).collect();
                    self.messages = messages;
                    self.request_missing_attachment_info();
                    return scrollable::snap_to(
                        self.transcript_scroll_id.clone(),
                        RelativeOffset {
                            x: None,
                            y: Some(1.0),
                        },
                    );
                }
            }
            daemon::Event::MessageSent {
                conversation_id, ..
            } => {
                self.request(daemon::Request::RefreshMessages { conversation_id });
                self.request(daemon::Request::RefreshConversations);
            }
            daemon::Event::ConversationSyncTriggered { .. }
            | daemon::Event::ConversationListSyncTriggered
            | daemon::Event::AllConversationsSyncTriggered
            | daemon::Event::MarkedRead
            | daemon::Event::AttachmentDownloadQueued { .. } => {}
            daemon::Event::AttachmentDownloaded { attachment_guid } => {
                self.request(daemon::Request::GetAttachmentInfo { attachment_guid });
                if let Some(id) = self.active_conversation_id.clone() {
                    self.request(daemon::Request::RefreshMessages {
                        conversation_id: id,
                    });
                }
            }
            daemon::Event::AttachmentDownloadFailed {
                attachment_guid,
                error,
            } => {
                self.pending_open_attachments.remove(&attachment_guid);
                self.status = Some(format!("Attachment {attachment_guid} failed: {error}"));
            }
            daemon::Event::AttachmentUploadQueued { upload_guid, path } => {
                self.uploads.push(ComposerUpload {
                    upload_guid,
                    path,
                    attachment_guid: None,
                });
                self.status = None;
            }
            daemon::Event::AttachmentUploaded {
                upload_guid,
                attachment_guid,
            } => {
                if let Some(upload) = self
                    .uploads
                    .iter_mut()
                    .find(|upload| upload.upload_guid == upload_guid)
                {
                    upload.attachment_guid = Some(attachment_guid);
                }
                self.status = None;
            }
            daemon::Event::AttachmentInfo {
                attachment_guid,
                info,
            } => {
                let should_open = self.pending_open_attachments.contains(&attachment_guid)
                    && info.downloaded
                    && !info.path.is_empty();
                let path = info.path.clone();
                self.attachment_info.insert(attachment_guid.clone(), info);
                if should_open {
                    self.pending_open_attachments.remove(&attachment_guid);
                    return Self::open_path(path, "attachment");
                }
            }
            daemon::Event::SettingsLoaded(settings) => {
                self.settings_server_url = settings.server_url;
                self.settings_username = settings.username;
                self.settings_password = settings.password;
            }
            daemon::Event::SettingsSaved => {
                self.show_settings = false;
                self.status = Some("Settings saved".to_string());
            }
            daemon::Event::ConversationsUpdated | daemon::Event::UpdateStreamReconnected => {
                self.request(daemon::Request::RefreshConversations);
            }
            daemon::Event::MessagesUpdated { conversation_id } => {
                if self.active_conversation_id.as_deref() == Some(conversation_id.as_str()) {
                    self.request(daemon::Request::RefreshMessages { conversation_id });
                } else {
                    self.request(daemon::Request::RefreshConversations);
                }
            }
            daemon::Event::Error(error) => {
                self.status = Some(error);
            }
        }
        Task::none()
    }

    fn select_conversation(&mut self, id: String, user_initiated: bool) -> Task<Message> {
        self.selected_conversation_id = Some(id.clone());
        self.active_conversation_id = Some(id.clone());
        self.active_title = self
            .conversations
            .iter()
            .find(|conversation| conversation.id == id)
            .map(|conversation| conversation.title.clone())
            .unwrap_or_else(|| "Messages".to_string());
        self.messages.clear();
        self.known_message_ids.clear();
        self.fresh_message_ids.clear();
        self.attachment_info.clear();
        self.requested_previews.clear();
        self.show_list_on_narrow = false;

        self.request(daemon::Request::MarkRead {
            conversation_id: id.clone(),
        });
        self.request(daemon::Request::SyncConversation {
            conversation_id: id.clone(),
        });
        self.request(daemon::Request::RefreshMessages {
            conversation_id: id,
        });

        if user_initiated {
            scrollable::snap_to(
                self.transcript_scroll_id.clone(),
                RelativeOffset {
                    x: None,
                    y: Some(1.0),
                },
            )
        } else {
            Task::none()
        }
    }

    fn request_missing_attachment_info(&mut self) {
        let mut requests = Vec::new();
        for message in &self.messages {
            for attachment in &message.attachments {
                if !self.attachment_info.contains_key(&attachment.guid) {
                    requests.push(daemon::Request::GetAttachmentInfo {
                        attachment_guid: attachment.guid.clone(),
                    });
                }
                if !attachment.preview_downloaded
                    && self.requested_previews.insert(attachment.guid.clone())
                {
                    requests.push(daemon::Request::DownloadAttachment {
                        attachment_guid: attachment.guid.clone(),
                        preview: true,
                    });
                }
            }
        }
        for request in requests {
            self.request(request);
        }
    }

    fn send_message(&mut self) -> Task<Message> {
        let text = self.composer.trim().to_string();
        let attachment_guids = self
            .uploads
            .iter()
            .filter_map(|upload| upload.attachment_guid.clone())
            .collect::<Vec<_>>();

        if text.is_empty() && attachment_guids.is_empty() {
            return Task::none();
        }

        if self
            .uploads
            .iter()
            .any(|upload| upload.attachment_guid.is_none())
        {
            self.status = Some("Wait for attachments to finish uploading".to_string());
            return Task::none();
        }

        let Some(conversation_id) = self.active_conversation_id.clone() else {
            self.status = Some("No conversation selected".to_string());
            return Task::none();
        };

        self.request(daemon::Request::SendMessageWithAttachments {
            conversation_id,
            text,
            attachment_guids,
        });
        self.composer.clear();
        self.uploads.clear();
        Task::none()
    }

    fn open_file_dialog(&mut self) -> Task<Message> {
        cosmic::task::future(async move {
            let filter = FileFilter::new("Images")
                .glob("*.png")
                .glob("*.jpg")
                .glob("*.jpeg")
                .glob("*.gif")
                .glob("*.webp")
                .glob("*.bmp")
                .glob("*.tiff")
                .glob("*.svg");
            let dialog = file_chooser::open::Dialog::new()
                .title("Select attachment")
                .filter(filter);
            match dialog.open_file().await {
                Ok(response) => Message::FileSelected(response.url().to_owned()),
                Err(file_chooser::Error::Cancelled) => Message::FileDialogCancelled,
                Err(error) => Message::FileDialogError(format!("File dialog failed: {error}")),
            }
        })
    }

    fn handle_transcript_action(&mut self, action: TranscriptAction) -> Task<Message> {
        match action {
            TranscriptAction::CopyText(text) => clipboard::write(text),
            TranscriptAction::OpenUrl(url) => cosmic::task::future(async move {
                match open::that(url).map_err(|e| e.to_string()) {
                    Ok(()) => Message::CloseError,
                    Err(error) => Message::FileDialogError(format!("Failed to open URL: {error}")),
                }
            }),
            TranscriptAction::OpenAttachment(guid) => self.open_attachment(guid),
        }
    }

    fn open_attachment(&mut self, guid: String) -> Task<Message> {
        if let Some(info) = self.attachment_info.get(&guid) {
            if info.downloaded && !info.path.is_empty() {
                return Self::open_path(info.path.clone(), "attachment");
            }
        }

        self.pending_open_attachments.insert(guid.clone());
        self.request(daemon::Request::DownloadAttachment {
            attachment_guid: guid,
            preview: false,
        });
        self.status = Some("Downloading attachment".to_string());
        Task::none()
    }

    fn open_path(path: String, kind: &'static str) -> Task<Message> {
        cosmic::task::future(async move {
            match open::that(path).map_err(|e| e.to_string()) {
                Ok(()) => Message::CloseError,
                Err(error) => Message::FileDialogError(format!("Failed to open {kind}: {error}")),
            }
        })
    }

    fn conversation_list(&self, narrow: bool) -> Element<'_, Message> {
        let spacing = cosmic::theme::spacing();
        let mut list = widget::list_column()
            .list_item_padding([spacing.space_xs, spacing.space_s])
            .divider_padding(spacing.space_s);

        for conversation in &self.conversations {
            let selected =
                self.selected_conversation_id.as_deref() == Some(conversation.id.as_str());
            let unread = conversation.unread_count > 0;
            let timestamp = format_conversation_date(conversation.date_unix);
            let title = widget::row::with_capacity(3)
                .spacing(spacing.space_xxs)
                .align_y(Alignment::Center)
                .push(
                    widget::text::caption(if unread { "●" } else { "" }).width(Length::Fixed(10.0)),
                )
                .push(
                    widget::text::heading(&conversation.title)
                        .width(Length::Fill)
                        .height(Length::Fixed(21.0))
                        .wrapping(Wrapping::None)
                        .ellipsize(Ellipsize::End(EllipsizeHeightLimit::Lines(1))),
                )
                .push(widget::text::caption(timestamp).height(Length::Fixed(17.0)));
            let preview = widget::text::caption(&conversation.preview)
                .width(Length::Fill)
                .height(Length::Fixed(17.0))
                .wrapping(Wrapping::None)
                .ellipsize(Ellipsize::End(EllipsizeHeightLimit::Lines(1)));
            let row = widget::column::with_capacity(2)
                .push(title)
                .push(preview)
                .spacing(1)
                .width(Length::Fill);
            list = list.add(
                widget::list::button(row)
                    .on_press(Message::SelectConversation(conversation.id.clone()))
                    .selected(selected),
            );
        }

        let scroll: Element<'_, Message> = if self.conversations.is_empty() {
            widget::scrollable(
                widget::container(widget::text::body("No conversations"))
                    .width(Length::Fill)
                    .center_x(Length::Fill)
                    .padding(24),
            )
            .height(Length::Fill)
            .into()
        } else {
            widget::scrollable(
                widget::container(list)
                    .padding(spacing.space_xs)
                    .width(Length::Fill),
            )
            .height(Length::Fill)
            .into()
        };

        let mut panel = widget::column::with_capacity(2)
            .push(scroll)
            .height(Length::Fill);
        if narrow && self.active_conversation_id.is_some() {
            panel = panel.push(
                widget::button::standard("Open")
                    .on_press(Message::OpenSelectedOnNarrow)
                    .width(Length::Fill),
            );
        }
        widget::container(panel).height(Length::Fill).into()
    }

    fn chat_panel(&self, narrow: bool) -> Element<'_, Message> {
        let spacing = cosmic::theme::spacing();
        let mut content = widget::column::with_capacity(4).height(Length::Fill);
        if narrow {
            content = content.push(
                widget::button::text("Back")
                    .on_press(Message::BackToList)
                    .padding([6, 12]),
            );
        }

        if let Some(status) = &self.status {
            content = content.push(widget::warning(status).on_close(Message::CloseError));
        }

        if self.active_conversation_id.is_none() {
            return widget::container(widget::text::title3("Select a conversation"))
                .width(Length::Fill)
                .height(Length::Fill)
                .align_x(Alignment::Center)
                .align_y(Alignment::Center)
                .into();
        }

        content = content.push(
            widget::container(
                widget::text::title4(&self.active_title)
                    .width(Length::Fill)
                    .height(Length::Fixed(30.0))
                    .wrapping(Wrapping::None)
                    .ellipsize(Ellipsize::End(EllipsizeHeightLimit::Lines(1))),
            )
            .padding([spacing.space_xs, spacing.space_m])
            .width(Length::Fill),
        );

        let transcript_messages = self.transcript_messages();
        let transcript: Element<'_, Message> = Transcript::new(
            transcript_messages,
            self.is_group_chat(),
            Message::Transcript,
        )
        .into();
        let scroll = widget::scrollable(transcript)
            .id(self.transcript_scroll_id.clone())
            .height(Length::Fill);
        content = content.push(scroll);
        content = content.push(widget::divider::horizontal::default());
        content = content.push(self.upload_preview_row());
        content = content.push(self.composer_row());

        widget::container(content).height(Length::Fill).into()
    }

    fn composer_row(&self) -> Element<'_, Message> {
        let can_send = (!self.composer.trim().is_empty()
            || self
                .uploads
                .iter()
                .any(|upload| upload.attachment_guid.is_some()))
            && self
                .uploads
                .iter()
                .all(|upload| upload.attachment_guid.is_some());

        let input = widget::text_input::text_input("Message", &self.composer)
            .on_input(Message::ComposerChanged)
            .on_submit(|_| Message::SendMessage)
            .width(Length::Fill);

        let send = if can_send {
            widget::button::suggested("Send").on_press(Message::SendMessage)
        } else {
            widget::button::standard("Send")
        };

        widget::container(
            widget::row::with_capacity(3)
                .spacing(cosmic::theme::spacing().space_xxs)
                .align_y(Alignment::Center)
                .push(
                    widget::button::icon(widget::icon::from_name("mail-attachment-symbolic"))
                        .on_press(Message::AttachFile),
                )
                .push(input)
                .push(send),
        )
        .padding([10, 16])
        .into()
    }

    fn upload_preview_row(&self) -> Element<'_, Message> {
        if self.uploads.is_empty() {
            return widget::space::vertical().height(Length::Fixed(0.0)).into();
        }

        let mut row = widget::row::with_capacity(self.uploads.len())
            .spacing(6)
            .padding([6, 14]);
        for upload in &self.uploads {
            let complete = upload.attachment_guid.is_some();
            let label = format!(
                "{}{}",
                filename(Path::new(&upload.path)),
                if complete { "" } else { " uploading" }
            );
            let item = widget::container(
                widget::row::with_capacity(2)
                    .spacing(4)
                    .align_y(Alignment::Center)
                    .push(widget::text::caption(label))
                    .push(
                        widget::button::icon(widget::icon::from_name("window-close-symbolic"))
                            .on_press(Message::RemoveUpload(upload.upload_guid.clone())),
                    ),
            )
            .padding([4, 8])
            .class(cosmic::theme::Container::Card);
            row = row.push(item);
        }
        row.into()
    }

    fn transcript_messages(&self) -> Vec<TranscriptMessage> {
        self.messages
            .iter()
            .map(|message| TranscriptMessage {
                id: message.id.clone(),
                sender: message.sender.clone(),
                text: message.text.clone(),
                date_unix: message.date_unix,
                from_me: message.from_me(),
                should_animate: self.fresh_message_ids.contains(&message.id),
                attachments: message
                    .attachments
                    .iter()
                    .map(|attachment| {
                        let info = self.attachment_info.get(&attachment.guid);
                        let attribution = attachment
                            .metadata
                            .as_ref()
                            .and_then(|metadata| metadata.attribution_info.as_ref());
                        TranscriptAttachment {
                            guid: attachment.guid.clone(),
                            preview_path: info
                                .filter(|info| {
                                    info.preview_downloaded && !info.preview_path.is_empty()
                                })
                                .map(|info| info.preview_path.clone()),
                            downloaded: info.map_or(attachment.downloaded, |info| info.downloaded),
                            preview_downloaded: info
                                .map_or(attachment.preview_downloaded, |info| {
                                    info.preview_downloaded
                                }),
                            width: attribution.and_then(|a| a.width),
                            height: attribution.and_then(|a| a.height),
                        }
                    })
                    .collect(),
            })
            .collect()
    }

    fn is_group_chat(&self) -> bool {
        if self
            .conversations
            .iter()
            .find(|conversation| {
                self.active_conversation_id.as_deref() == Some(conversation.id.as_str())
            })
            .is_some_and(|conversation| {
                conversation.participants.len() > 2
                    || conversation
                        .participants
                        .iter()
                        .filter(|participant| participant.as_str() != "(Me)")
                        .take(2)
                        .count()
                        > 1
            })
        {
            return true;
        }

        self.messages
            .iter()
            .filter(|message| !message.from_me())
            .map(|message| message.sender.as_str())
            .collect::<HashSet<_>>()
            .len()
            > 1
    }

    fn load_demo_data(&mut self) {
        let now = Local::now().timestamp();
        self.conversations = vec![
            daemon::ConversationSummary {
                id: "demo-family".to_string(),
                title: "Family".to_string(),
                preview: "Can you bring the charger?".to_string(),
                participants: vec!["Avery".to_string(), "Sam".to_string(), "(Me)".to_string()],
                unread_count: 2,
                date_unix: now,
            },
            daemon::ConversationSummary {
                id: "demo-riley".to_string(),
                title: "Riley".to_string(),
                preview: "The prototype looks good.".to_string(),
                participants: vec!["Riley".to_string(), "(Me)".to_string()],
                unread_count: 0,
                date_unix: now - 7200,
            },
        ];
        self.active_conversation_id = Some("demo-family".to_string());
        self.selected_conversation_id = Some("demo-family".to_string());
        self.active_title = "Family".to_string();
        self.messages = vec![
            daemon::ChatMessage {
                id: "1".to_string(),
                sender: "Avery".to_string(),
                text: "Leaving in ten. Can you bring the charger?".to_string(),
                date_unix: now - 900,
                attachments: Vec::new(),
            },
            daemon::ChatMessage {
                id: "2".to_string(),
                sender: "(Me)".to_string(),
                text: "Yes. I also found the link: https://buzzert.net/kordophone".to_string(),
                date_unix: now - 720,
                attachments: Vec::new(),
            },
            daemon::ChatMessage {
                id: "3".to_string(),
                sender: "Sam".to_string(),
                text: "Great, see you soon.".to_string(),
                date_unix: now - 120,
                attachments: Vec::new(),
            },
        ];
        self.known_message_ids = self.messages.iter().map(|m| m.id.clone()).collect();
    }
}

fn format_conversation_date(timestamp: i64) -> String {
    let Some(date) = Local.timestamp_opt(timestamp, 0).single() else {
        return String::new();
    };
    let now = Local::now();
    if date.year() == now.year() && date.ordinal() == now.ordinal() {
        date.format("%H:%M").to_string()
    } else {
        date.format("%b %d").to_string()
    }
}

fn filename(path: &Path) -> String {
    path.file_name()
        .and_then(|name| name.to_str())
        .unwrap_or("attachment")
        .to_string()
}

async fn save_screenshot(path: PathBuf, screenshot: Screenshot) -> Result<PathBuf, String> {
    tokio::task::spawn_blocking(move || {
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
        }
        image::save_buffer(
            &path,
            &screenshot.rgba,
            screenshot.size.width,
            screenshot.size.height,
            ColorType::Rgba8,
        )
        .map_err(|e| e.to_string())?;
        Ok(path)
    })
    .await
    .map_err(|e| e.to_string())?
}
