use std::collections::{HashMap, HashSet, VecDeque, hash_map};
use std::ops::RangeInclusive;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::{convert, slice};

use chrono::{DateTime, Utc};
use data::capabilities::{
    LabeledResponseContext, MultilineBatchKind, multiline_concat_lines,
};
use data::config::buffer::{ScrollPosition, UsernameFormat};
use data::dashboard::{self, BufferAction};
use data::environment::{RELEASE_WEBSITE, WIKI_WEBSITE};
use data::history::ReadMarker;
use data::history::filter::Filter;
use data::history::manager::EchoEvent;
use data::history::reroute::RerouteRules;
use data::isupport::{self, ChatHistorySubcommand, MessageReference};
use data::message::{self, Broadcast};
use data::rate_limit::TokenPriority;
use data::target::{self, Target};
use data::user::Nick;
use data::{
    Config, Image, Notification, Server, User, Version, cache, client, command,
    config, environment, file_transfer, history, preview, reaction, redaction,
    server, server_icon, stream,
};
use iced::widget::pane_grid::{self, PaneGrid};
use iced::widget::{Space, center, column, container, row, stack, text};
use iced::{Length, Size, Task, Vector, advanced, clipboard, padding};
use irc::proto;

use self::command_bar::CommandBar;
use self::modal::{reaction as reaction_modal, redaction as redaction_modal};
use self::pane::Pane;
use self::sidebar::Sidebar;
use self::theme_editor::ThemeEditor;
use crate::buffer::{self, Buffer};
use crate::widget::{
    Column, Element, Row, anchored_overlay, context_menu, selectable_text,
    shortcut,
};
use crate::window::Window;
use crate::{
    Theme, event, filehost, notification, open_url, platform_specific, theme,
    window,
};

mod command_bar;
pub mod modal;
pub mod pane;
pub mod sidebar;
mod theme_editor;

const FOCUS_HISTORY_LEN: usize = 8;
const SAVE_AFTER: Duration = Duration::from_secs(3);

pub struct Dashboard {
    panes: Panes,
    focus: Focus,
    focus_history: VecDeque<pane_grid::Pane>,
    side_menu: Sidebar,
    history: history::Manager,
    last_changed: Option<Instant>,
    command_bar: Option<CommandBar>,
    command_bar_window: Option<window::Id>,
    file_transfers: file_transfer::Manager,
    theme_editor: Option<ThemeEditor>,
    notifications: notification::Notifications,
    previews: preview::Collection,
    previews_cache: Arc<cache::FileCache>,
    server_icons: server_icon::Manager,
    typing_animation: Option<buffer::typing::Animation>,
    http_client: Option<Arc<reqwest::Client>>,
    buffer_settings: dashboard::BufferSettings,
    pub filehost: filehost::Manager,
}

#[derive(Debug)]
pub enum Message {
    Pane(window::Id, pane::Message),
    Sidebar(sidebar::Message),
    SelectedText(
        Vec<(RangeInclusive<f32>, String)>,
        advanced::clipboard::Kind,
    ),
    History(history::manager::Message),
    DashboardSaved(Result<(), data::dashboard::Error>),
    Task(command_bar::Message),
    Shortcut(shortcut::Command),
    FileTransfer(file_transfer::task::Update),
    SendFileSelected(Server, User, Option<PathBuf>),
    CloseContextMenu(window::Id, bool),
    ThemeEditor(theme_editor::Message),
    ConfigReloaded(Result<Config, config::Error>),
    Client(client::Message),
    ServerIcon(server_icon::Message),
    LoadPreview((url::Url, Result<data::Preview, data::preview::LoadError>)),
    NewWindow(window::Id, Pane),
    Filehost(filehost::Message),
    ProceedWithFilehostUpload,
    CancelFilehostUpload,
}

#[derive(Debug)]
pub enum Event {
    ConfigReloaded(Result<Config, config::Error>),
    ReloadThemes,
    QuitServer(Server, Option<String>),
    IrcError(anyhow::Error),
    Exit,
    OpenUrl(String, bool),
    OpenAbout {
        version: String,
        commit: String,
        system_information: Option<iced::system::Information>,
    },
    OpenServer(String),
    ImagePreview(Image),
    ToggleFullscreen,
    Remove(Server),
    PromptBeforeFileUpload {
        upload_url: String,
        has_credentials: bool,
        window: window::Id,
    },
    EchoEvents(Server, Vec<EchoEvent>),
}

impl Dashboard {
    pub fn empty(
        main_window: &Window,
        config: &Config,
    ) -> (Self, Task<Message>) {
        let (main_panes, pane) =
            pane_grid::State::new(Pane::new(Buffer::Empty));

        let (sidebar, sidebar_task) = Sidebar::new();

        let mut dashboard = Dashboard {
            panes: Panes {
                main_window: main_window.id,
                main: main_panes,
                popout: HashMap::new(),
            },
            focus: Focus {
                window: main_window.id,
                pane,
            },
            focus_history: VecDeque::new(),
            side_menu: sidebar,
            history: history::Manager::default(),
            last_changed: None,
            command_bar: None,
            command_bar_window: None,
            file_transfers: file_transfer::Manager::default(),
            theme_editor: None,
            notifications: notification::Notifications::new(config),
            previews: preview::Collection::default(),
            previews_cache: Arc::new(preview_cache(&config.preview)),
            server_icons: server_icon::Manager::default(),
            typing_animation: None,
            http_client: http_client_from_config(config).map(Arc::new),
            buffer_settings: dashboard::BufferSettings::default(),
            filehost: filehost::Manager::new(),
        };

        if config.buffer.text_input.persist {
            dashboard
                .history
                .preload_drafts(data::input::load_drafts_sync());
        }

        let command = dashboard.track(None);
        let sidebar_task = sidebar_task.map(Message::Sidebar);

        (dashboard, Task::batch(vec![command, sidebar_task]))
    }

    pub fn restore(
        dashboard: data::Dashboard,
        config: &Config,
        main_window: &Window,
    ) -> (Self, Task<Message>) {
        let (mut dashboard, task) =
            Dashboard::from_data(dashboard, config, main_window);

        let tasks = Task::batch(vec![task, dashboard.track(None)]);

        (dashboard, tasks)
    }

    pub fn init_filters(
        &mut self,
        servers: &server::Map,
        clients: &client::Map,
    ) {
        self.history
            .set_filters(Filter::list_from_servers(servers, clients));
    }

    pub fn update_channel_discoveries(
        &self,
        clients: &mut client::Map,
        server: &data::Server,
    ) {
        if let Some(pane) = self.panes.iter().find_map(|(_, _, pane)| {
            if let Some(data::Buffer::Internal(
                buffer::Internal::ChannelDiscovery(Some(pane_server)),
            )) = pane.buffer.data()
                && pane_server == *server
            {
                Some(pane)
            } else {
                None
            }
        }) {
            Self::send_list_command_if_needed(server, pane, clients);
        }
    }

    pub fn update_filters(
        &mut self,
        servers: &server::Map,
        clients: &client::Map,
        buffer_config: &config::Buffer,
    ) {
        self.init_filters(servers, clients);

        self.reprocess_history(clients, buffer_config);
    }

    pub fn refresh_cache_limits(&mut self, config: &Config) {
        self.previews_cache = Arc::new(preview_cache(&config.preview));
    }

    pub fn set_reroute_rules(
        &mut self,
        servers: &server::Map,
        clients: &client::Map,
    ) {
        let reroute_rules = self.get_reroute_rules_mut();

        *reroute_rules = RerouteRules::from_server_map(servers, clients);
    }

    pub fn reload_visible_previews(
        &mut self,
        clients: &client::Map,
        config: &Config,
    ) -> Task<Message> {
        Task::batch(
            self.visible_urls_with_preview_clients(clients)
                .into_iter()
                .map(|(url, client)| {
                    Task::perform(
                        data::preview::load(
                            url.clone(),
                            client.clone(),
                            config.preview.clone(),
                            self.previews_cache.clone(),
                        ),
                        move |result| {
                            Message::LoadPreview((url.clone(), result))
                        },
                    )
                }),
        )
    }

    pub fn reprocess_history(
        &mut self,
        clients: &client::Map,
        buffer_config: &config::Buffer,
    ) {
        let open_pane_kinds: Vec<history::Kind> = self
            .panes
            .iter()
            .filter_map(|(_window_id, _grid_pane, pane)| {
                if matches!(
                    pane.buffer,
                    Buffer::Channel(_)
                        | Buffer::Server(_)
                        | Buffer::Query(_)
                        | Buffer::Highlights(_)
                ) {
                    pane.buffer.data().and_then(history::Kind::from_buffer)
                } else {
                    None
                }
            })
            .collect();

        open_pane_kinds.into_iter().for_each(|kind| {
            self.history.process_messages(kind, clients, buffer_config);
        });
    }

    pub fn renormalize_history(
        &mut self,
        server: &data::Server,
        clients: &client::Map,
    ) {
        let open_pane_kinds: Vec<history::Kind> = self
            .panes
            .iter()
            .filter_map(|(_window_id, _grid_pane, pane)| {
                if pane
                    .buffer
                    .server()
                    .is_some_and(|buffer_server| buffer_server == *server)
                    || matches!(pane.buffer, Buffer::Highlights(_))
                {
                    pane.buffer.data().and_then(history::Kind::from_buffer)
                } else {
                    None
                }
            })
            .collect();

        open_pane_kinds.into_iter().for_each(|kind| {
            self.history.renormalize_messages(&kind, clients);
        });
    }

    pub fn update(
        &mut self,
        message: Message,
        clients: &mut client::Map,
        controllers: &mut stream::Map,
        servers: &server::Map,
        theme: &mut Theme,
        version: &Version,
        config: &Config,
        main_window: &Window,
    ) -> (Task<Message>, Option<Event>) {
        match message {
            Message::Pane(window, message) => {
                match message {
                    pane::Message::PaneClicked(pane) => {
                        return (self.focus_pane(window, pane), None);
                    }
                    pane::Message::PaneResized(pane_grid::ResizeEvent {
                        split,
                        ratio,
                    }) => {
                        // Pane grid interactions only enabled for main window panegrid
                        self.panes.main.resize(split, ratio);
                        self.last_changed = Some(Instant::now());
                    }
                    pane::Message::PaneDragged(
                        pane_grid::DragEvent::Dropped { pane, target },
                    ) => {
                        // Pane grid interactions only enabled for main window panegrid
                        self.panes.main.drop(pane, target);
                        self.last_changed = Some(Instant::now());
                    }
                    pane::Message::PaneDragged(_) => {}
                    pane::Message::ClosePane => {
                        return (
                            self.close_pane(
                                clients,
                                config,
                                self.focus.window,
                                self.focus.pane,
                            ),
                            None,
                        );
                    }
                    pane::Message::SplitPane(axis) => {
                        return (self.split_pane(axis), None);
                    }
                    pane::Message::Buffer(id, message) => {
                        if let Some(pane) = self.panes.get_mut(window, id) {
                            let (command, event) = pane.buffer.update(
                                message,
                                clients,
                                &mut self.history,
                                &mut self.file_transfers,
                                main_window,
                                config,
                            );

                            let task = command.map(move |message| {
                                Message::Pane(
                                    window,
                                    pane::Message::Buffer(id, message),
                                )
                            });

                            let Some(event) = event else {
                                return (task, None);
                            };

                            let (buffer_task, buffer_event) = self
                                .handle_buffer_event(
                                    window,
                                    id,
                                    event,
                                    clients,
                                    controllers,
                                    config,
                                );

                            return (
                                Task::batch(vec![task, buffer_task]),
                                buffer_event,
                            );
                        }
                    }
                    pane::Message::ToggleShowUserList => {
                        if let Some((_, _, pane)) = self.get_focused_mut() {
                            if let Some(buffer) = pane.buffer.data() {
                                let settings = self.buffer_settings.entry(
                                    &buffer,
                                    Some(config.buffer.clone().into()),
                                );
                                settings.channel.nicklist.toggle_visibility();
                            }

                            self.last_changed = Some(Instant::now());
                            return (Task::none(), None);
                        }
                    }
                    pane::Message::ToggleShowTopic => {
                        if let Some((_, _, pane)) = self.get_focused_mut() {
                            if let Some(buffer) = pane.buffer.data() {
                                let settings = self.buffer_settings.entry(
                                    &buffer,
                                    Some(config.buffer.clone().into()),
                                );
                                settings
                                    .channel
                                    .topic_banner
                                    .toggle_visibility();
                            }

                            self.last_changed = Some(Instant::now());
                            return (Task::none(), None);
                        }
                    }
                    pane::Message::MaximizePane => self.maximize_pane(),
                    pane::Message::Popout => {
                        return (self.popout_pane(clients, config), None);
                    }
                    pane::Message::Merge => {
                        return (self.merge_pane(clients, config), None);
                    }
                    pane::Message::ScrollToBottom => {
                        let Focus { window, pane } = self.focus;

                        if let Some(state) = self.panes.get_mut(window, pane) {
                            let mut task = state
                                .buffer
                                .scroll_to_end(config)
                                .map(move |message| {
                                    Message::Pane(
                                        window,
                                        pane::Message::Buffer(pane, message),
                                    )
                                });

                            if config.buffer.mark_as_read.on_scroll_to_bottom {
                                task = task.chain(Task::done(Message::Pane(
                                    window,
                                    pane::Message::MarkAsRead,
                                )));
                            }

                            return (task, None);
                        }
                    }
                    pane::Message::MarkAsRead => {
                        if let Some((_, _, pane)) = self.get_focused_mut()
                            && let Some(kind) = pane
                                .buffer
                                .data()
                                .and_then(history::Kind::from_buffer)
                        {
                            mark_as_read(
                                kind,
                                &mut self.history,
                                clients,
                                TokenPriority::User,
                            );
                        }
                    }
                    pane::Message::ClearBuffer => {
                        if let Some((_, _, pane)) = self.get_focused_mut()
                            && let Some(kind) = pane
                                .buffer
                                .data()
                                .and_then(history::Kind::from_buffer)
                        {
                            let task = self
                                .history
                                .clear_messages(kind, clients)
                                .map_or_else(Task::none, |task| {
                                    Task::perform(task, Message::History)
                                });

                            return (task, None);
                        }
                    }
                    pane::Message::ContentResized(id, size) => {
                        if let Some(state) = self.panes.get_mut(window, id) {
                            state.size = size;
                            state.buffer.update_pane_size(size, config);
                        }
                    }
                    pane::Message::Modal(id, message) => {
                        let (task, buffer_event) = {
                            let Some(pane) = self.panes.get_mut(window, id)
                            else {
                                return (Task::none(), None);
                            };

                            let Some(event) = pane
                                .modal
                                .as_mut()
                                .and_then(|modal| modal.update(message))
                            else {
                                return (Task::none(), None);
                            };

                            match event {
                                modal::Event::ToggleReaction {
                                    msgid,
                                    text,
                                    unreact,
                                } => {
                                    pane.close_buffer_modal();

                                    let Some(buffer_message) = pane
                                        .buffer
                                        .reaction_message(msgid, text, unreact)
                                    else {
                                        return (self.refocus_pane(), None);
                                    };

                                    let (command, event) = pane.buffer.update(
                                        buffer_message,
                                        clients,
                                        &mut self.history,
                                        &mut self.file_transfers,
                                        main_window,
                                        config,
                                    );

                                    let task = self.refocus_pane().chain(
                                        command.map(move |message| {
                                            Message::Pane(
                                                window,
                                                pane::Message::Buffer(
                                                    id, message,
                                                ),
                                            )
                                        }),
                                    );

                                    (task, event)
                                }
                                modal::Event::RedactReason {
                                    msgid,
                                    reason,
                                } => {
                                    pane.close_buffer_modal();

                                    if let Some(buffer) = pane.buffer.upstream()
                                        && let Some(target) = buffer.target()
                                    {
                                        let command = command::Irc::Redact {
                                            target: target.to_string(),
                                            msgid: msgid.clone(),
                                            reason: if reason.is_empty() {
                                                None
                                            } else {
                                                Some(reason.clone())
                                            },
                                        };

                                        let input: data::Input =
                                            data::Input::from_command(
                                                buffer.clone(),
                                                command,
                                            );

                                        if let Some(encoded) = input.encoded() {
                                            clients.send(
                                                &input.buffer,
                                                encoded,
                                                TokenPriority::User,
                                            );
                                        }
                                    }

                                    (self.refocus_pane(), None)
                                }
                            }
                        };

                        let Some(event) = buffer_event else {
                            return (task, None);
                        };

                        let (buffer_task, buffer_event) = self
                            .handle_buffer_event(
                                window,
                                id,
                                event,
                                clients,
                                controllers,
                                config,
                            );

                        return (
                            Task::batch(vec![task, buffer_task]),
                            buffer_event,
                        );
                    }
                    pane::Message::CloseBufferModal(id) => {
                        if let Some(state) = self.panes.get_mut(window, id) {
                            state.close_buffer_modal();
                        }
                        return (self.refocus_pane(), None);
                    }
                }
            }
            Message::Sidebar(message) => {
                let (command, event) = self.side_menu.update(message);

                let Some(event) = event else {
                    return (command.map(Message::Sidebar), None);
                };

                let (event_task, event) = match event {
                    sidebar::Event::CloseAllQueries(server, queries) => (
                        self.leave_all_queries(
                            clients, config, server, queries,
                        ),
                        None,
                    ),
                    sidebar::Event::QuitApplication => {
                        (self.exit(clients, config), None)
                    }
                    sidebar::Event::New(buffer) => (
                        self.open_buffer(
                            data::Buffer::Upstream(buffer),
                            BufferAction::NewPane,
                            clients,
                            config,
                        ),
                        None,
                    ),
                    sidebar::Event::Popout(buffer) => (
                        self.open_buffer(
                            data::Buffer::Upstream(buffer),
                            BufferAction::NewWindow,
                            clients,
                            config,
                        ),
                        None,
                    ),
                    sidebar::Event::Focus(window, pane) => {
                        (self.focus_pane(window, pane), None)
                    }
                    sidebar::Event::Replace(buffer) => (
                        self.open_buffer(
                            data::Buffer::Upstream(buffer),
                            BufferAction::ReplacePane,
                            clients,
                            config,
                        ),
                        None,
                    ),
                    sidebar::Event::Close(window, pane) => {
                        (self.close_pane(clients, config, window, pane), None)
                    }
                    sidebar::Event::Swap(window, pane) => {
                        (self.swap_pane_with_focus(window, pane), None)
                    }
                    sidebar::Event::Detach(buffer) => {
                        if let Some(target) = buffer.target() {
                            let server = buffer.server();

                            (
                                self.leave_server_target(
                                    clients,
                                    config,
                                    server.clone(),
                                    target,
                                    Some("detach".to_string()),
                                ),
                                None,
                            )
                        } else {
                            (Task::none(), None)
                        }
                    }
                    sidebar::Event::Leave(buffer) => {
                        self.leave_buffer(clients, config, buffer)
                    }
                    sidebar::Event::ToggleInternalBuffer(buffer) => (
                        self.toggle_internal_buffer(clients, config, buffer),
                        None,
                    ),
                    sidebar::Event::ToggleCommandBar => (
                        self.toggle_command_bar(
                            servers,
                            clients,
                            &closed_buffers(self, clients),
                            version,
                            config,
                            theme,
                        ),
                        None,
                    ),
                    sidebar::Event::ConfigReloaded(conf) => {
                        (Task::none(), Some(Event::ConfigReloaded(conf)))
                    }
                    sidebar::Event::OpenReleaseWebsite => {
                        let _ = open_url::open(RELEASE_WEBSITE);
                        (Task::none(), None)
                    }
                    sidebar::Event::ToggleThemeEditor => (
                        self.toggle_theme_editor(theme, main_window, config),
                        None,
                    ),
                    sidebar::Event::OpenDocumentation => {
                        let _ = open_url::open(WIKI_WEBSITE);
                        (Task::none(), None)
                    }
                    sidebar::Event::OpenAbout {
                        version,
                        commit,
                        system_information,
                    } => (
                        Task::none(),
                        Some(Event::OpenAbout {
                            version,
                            commit,
                            system_information,
                        }),
                    ),
                    sidebar::Event::MarkServerAsRead(server) => {
                        mark_server_as_read(server, &mut self.history, clients);

                        (Task::none(), None)
                    }
                    sidebar::Event::MarkAsRead(buffer) => {
                        if let Some(kind) = history::Kind::from_buffer(
                            data::Buffer::Upstream(buffer),
                        ) {
                            mark_as_read(
                                kind,
                                &mut self.history,
                                clients,
                                TokenPriority::User,
                            );
                        }

                        (Task::none(), None)
                    }
                    sidebar::Event::OpenConfigFile => {
                        let _ = open_url::open(Config::path());
                        (Task::none(), None)
                    }
                    sidebar::Event::Connect(server) => {
                        connect_server(server, controllers, servers);

                        (Task::none(), None)
                    }
                    sidebar::Event::Remove(server) => {
                        (Task::none(), Some(Event::Remove(server)))
                    }
                };

                let window = main_window.id;

                return (
                    Task::batch(vec![
                        context_menu::close(convert::identity).map(
                            move |any_closed| {
                                Message::CloseContextMenu(window, any_closed)
                            },
                        ),
                        event_task,
                        command.map(Message::Sidebar),
                    ]),
                    event,
                );
            }
            Message::SelectedText(contents, clipboard_kind) => {
                let mut last_y_range: Option<RangeInclusive<f32>> = None;
                let contents = contents.into_iter().fold(
                    String::new(),
                    |acc, (y_range, content)| {
                        let content = if let Some(last_y_range) = &last_y_range
                        {
                            // If the widget's y ranges do not overlap, then add
                            // a new line before appending the selected content.
                            let new_line = if y_range.start()
                                < last_y_range.end()
                                && last_y_range.start() < y_range.end()
                            {
                                ""
                            } else {
                                "\n"
                            };

                            format!("{acc}{new_line}{content}")
                        } else {
                            content
                        };

                        last_y_range = Some(y_range);

                        content
                    },
                );

                if !contents.is_empty() {
                    return (
                        match clipboard_kind {
                            advanced::clipboard::Kind::Standard => {
                                clipboard::write(contents)
                            }
                            advanced::clipboard::Kind::Primary => {
                                clipboard::write_primary(contents)
                            }
                        },
                        None,
                    );
                }
            }
            Message::History(message) => {
                if let Some(event) =
                    self.history.update(message, clients, &config.buffer)
                {
                    match event {
                        history::manager::Event::Loaded(kind) => {
                            let buffer = kind.clone().into();

                            if let Some((window, pane, state)) =
                                self.panes.get_mut_by_buffer(&buffer)
                            {
                                if let Some(draft_reply) =
                                    state.buffer.get_draft_reply()
                                    && let Some(reply_preview) =
                                        self.history.generate_reply_preview(
                                            kind,
                                            &draft_reply.id,
                                            &draft_reply.server_time,
                                        )
                                {
                                    state
                                        .buffer
                                        .set_reply_preview(reply_preview);
                                }

                                if state.buffer.has_pending_scroll_to() {
                                    return (
                                        state
                                            .buffer
                                            .prepare_for_pending_scroll_to(
                                                &self.history,
                                                config,
                                            )
                                            .map(move |message| {
                                                Message::Pane(
                                                    window,
                                                    pane::Message::Buffer(
                                                        pane, message,
                                                    ),
                                                )
                                            }),
                                        None,
                                    );
                                } else {
                                    return (
                                        match config
                                            .buffer
                                            .scroll_position_on_open
                                        {
                                            ScrollPosition::OldestUnread => {
                                                state
                                                    .buffer
                                                    .scroll_to_backlog(
                                                        &self.history,
                                                        config,
                                                    )
                                                    .map(move |message| {
                                                        Message::Pane(
                                                    window,
                                                    pane::Message::Buffer(
                                                        pane, message,
                                                    ),
                                                )
                                                    })
                                            }
                                            ScrollPosition::Newest => {
                                                Task::none()
                                            }
                                        },
                                        None,
                                    );
                                }
                            }
                        }
                        history::manager::Event::Exited => {
                            return (Task::none(), Some(Event::Exit));
                        }
                        history::manager::Event::SentMessageUpdated(
                            kind,
                            read_marker,
                        ) => {
                            if config.buffer.mark_as_read.on_message_sent
                                && let (Some(server), Some(target)) =
                                    (kind.server(), kind.target())
                            {
                                clients.send_markread(
                                    server,
                                    target,
                                    read_marker,
                                    TokenPriority::High,
                                );
                            }
                        }
                        history::manager::Event::ResendMessage(
                            kind,
                            message,
                        ) => {
                            return (
                                self.resend_message(
                                    clients, kind, message, config,
                                ),
                                None,
                            );
                        }
                        history::manager::Event::EchoEvents(server, events) => {
                            return (
                                Task::none(),
                                Some(Event::EchoEvents(server, events)),
                            );
                        }
                    }
                }
            }
            Message::DashboardSaved(Ok(())) => {
                log::debug!("dashboard saved");
            }
            Message::DashboardSaved(Err(error)) => {
                log::warn!("error saving dashboard: {error}");
            }
            Message::Task(message) => {
                let Some(command_bar) = &mut self.command_bar else {
                    return (Task::none(), None);
                };

                match command_bar.update(message) {
                    Some(command_bar::Event::ThemePreview(preview)) => {
                        match preview {
                            Some(preview) => *theme = theme.preview(preview),
                            None => *theme = theme.selected(),
                        }
                    }
                    Some(command_bar::Event::Command(command)) => {
                        let (command, event) = self.handle_command_bar_command(
                            command,
                            clients,
                            controllers,
                            servers,
                            theme,
                            config,
                            main_window,
                        );

                        return (
                            Task::batch(vec![
                                command,
                                self.toggle_command_bar(
                                    servers,
                                    clients,
                                    &closed_buffers(self, clients),
                                    version,
                                    config,
                                    theme,
                                ),
                            ]),
                            event,
                        );
                    }
                    Some(command_bar::Event::Unfocused) => {
                        return (
                            self.toggle_command_bar(
                                servers,
                                clients,
                                &closed_buffers(self, clients),
                                version,
                                config,
                                theme,
                            ),
                            None,
                        );
                    }
                    None => {}
                }
            }
            Message::Shortcut(shortcut) => {
                use shortcut::Command::*;

                // Only works on main window / pane_grid
                let mut move_focus = |direction: pane_grid::Direction| {
                    let Focus { window, pane } = self.focus;

                    if window == self.main_window()
                        && let Some(adjacent) =
                            self.panes.main.adjacent(pane, direction)
                    {
                        return self.focus_pane(window, adjacent);
                    }

                    Task::none()
                };

                match shortcut {
                    MoveUp => {
                        return (move_focus(pane_grid::Direction::Up), None);
                    }
                    MoveDown => {
                        return (move_focus(pane_grid::Direction::Down), None);
                    }
                    MoveLeft => {
                        return (move_focus(pane_grid::Direction::Left), None);
                    }
                    MoveRight => {
                        return (move_focus(pane_grid::Direction::Right), None);
                    }
                    NewHorizontalBuffer => {
                        return (
                            self.new_pane(pane_grid::Axis::Horizontal),
                            None,
                        );
                    }
                    NewVerticalBuffer => {
                        return (
                            self.new_pane(pane_grid::Axis::Vertical),
                            None,
                        );
                    }
                    CloseBuffer => {
                        let Focus { window, pane } = self.focus;
                        return (
                            self.close_pane(clients, config, window, pane),
                            None,
                        );
                    }
                    MaximizeBuffer => {
                        let Focus { window, pane } = self.focus;
                        // Only main window has >1 pane to maximize
                        if window == self.main_window() {
                            self.panes.main.maximize(pane);
                        }
                    }
                    RestoreBuffer => {
                        self.panes.main.restore();
                    }
                    CycleNextBuffer => {
                        let all_buffers = all_buffers(clients, &self.history);
                        let open_buffers = open_buffers(self);

                        if let Some((window, pane, state, history)) =
                            self.get_focused_with_history_mut()
                            && let Some(buffer) = cycle_next_buffer(
                                state.buffer.upstream(),
                                all_buffers,
                                &open_buffers,
                            )
                        {
                            mark_as_read_on_buffer_close(
                                &state.buffer,
                                history,
                                clients,
                                config,
                            );

                            state.buffer = Buffer::from_data(
                                data::Buffer::Upstream(buffer),
                                clients,
                                history,
                                state.size,
                                config,
                            );
                            self.last_changed = Some(Instant::now());
                            return (self.focus_pane(window, pane), None);
                        }
                    }
                    CyclePreviousBuffer => {
                        let all_buffers = all_buffers(clients, &self.history);
                        let open_buffers = open_buffers(self);

                        if let Some((window, pane, state, history)) =
                            self.get_focused_with_history_mut()
                            && let Some(buffer) = cycle_previous_buffer(
                                state.buffer.upstream(),
                                all_buffers,
                                &open_buffers,
                            )
                        {
                            mark_as_read_on_buffer_close(
                                &state.buffer,
                                history,
                                clients,
                                config,
                            );

                            state.buffer = Buffer::from_data(
                                data::Buffer::Upstream(buffer),
                                clients,
                                history,
                                state.size,
                                config,
                            );
                            self.last_changed = Some(Instant::now());
                            return (self.focus_pane(window, pane), None);
                        }
                    }
                    LeaveBuffer => {
                        if let Some((_, _, state)) = self.get_focused_mut()
                            && let Some(buffer) =
                                state.buffer.upstream().cloned()
                        {
                            return self.leave_buffer(clients, config, buffer);
                        }
                    }
                    ToggleNicklist => {
                        if let Some((_, _, pane)) = self.get_focused_mut() {
                            if let Some(buffer) = pane.buffer.data() {
                                let settings = self.buffer_settings.entry(
                                    &buffer,
                                    Some(config.buffer.clone().into()),
                                );
                                settings.channel.nicklist.toggle_visibility();
                            }

                            self.last_changed = Some(Instant::now());
                            return (Task::none(), None);
                        }
                    }
                    ToggleTopic => {
                        if let Some((_, _, pane)) = self.get_focused_mut() {
                            if let Some(buffer) = pane.buffer.data() {
                                let settings = self.buffer_settings.entry(
                                    &buffer,
                                    Some(config.buffer.clone().into()),
                                );
                                settings
                                    .channel
                                    .topic_banner
                                    .toggle_visibility();
                            }

                            self.last_changed = Some(Instant::now());
                            return (Task::none(), None);
                        }
                    }
                    ToggleSidebar => {
                        self.side_menu.toggle_visibility();
                    }
                    CommandBar => {
                        return (
                            self.toggle_command_bar(
                                servers,
                                clients,
                                &closed_buffers(self, clients),
                                version,
                                config,
                                theme,
                            ),
                            None,
                        );
                    }
                    ReloadConfiguration => {
                        return (
                            Task::perform(
                                Config::load(),
                                Message::ConfigReloaded,
                            ),
                            None,
                        );
                    }
                    FileTransfers => {
                        return (
                            self.toggle_internal_buffer(
                                clients,
                                config,
                                buffer::Internal::FileTransfers,
                            ),
                            None,
                        );
                    }
                    Logs => {
                        return (
                            self.toggle_internal_buffer(
                                clients,
                                config,
                                buffer::Internal::Logs,
                            ),
                            None,
                        );
                    }
                    ThemeEditor => {
                        return (
                            self.toggle_theme_editor(
                                theme,
                                main_window,
                                config,
                            ),
                            None,
                        );
                    }
                    Highlights => {
                        return (
                            self.toggle_internal_buffer(
                                clients,
                                config,
                                buffer::Internal::Highlights,
                            ),
                            None,
                        );
                    }
                    ToggleFullscreen => {
                        return (
                            window::toggle_fullscreen(),
                            Some(Event::ToggleFullscreen),
                        );
                    }
                    QuitApplication => {
                        return (self.exit(clients, config), None);
                    }
                    ScrollUpPage => {
                        return (
                            self.get_focused_mut().map_or_else(
                                Task::none,
                                |(window, pane, state)| {
                                    state.buffer.scroll_up_page().map(
                                        move |message| {
                                            Message::Pane(
                                                window,
                                                pane::Message::Buffer(
                                                    pane, message,
                                                ),
                                            )
                                        },
                                    )
                                },
                            ),
                            None,
                        );
                    }
                    ScrollDownPage => {
                        return (
                            self.get_focused_mut().map_or_else(
                                Task::none,
                                |(window, pane, state)| {
                                    state.buffer.scroll_down_page().map(
                                        move |message| {
                                            Message::Pane(
                                                window,
                                                pane::Message::Buffer(
                                                    pane, message,
                                                ),
                                            )
                                        },
                                    )
                                },
                            ),
                            None,
                        );
                    }
                    ScrollToTop => {
                        if config.buffer.chathistory.infinite_scroll
                            && let Some((_, _, state)) = self.get_focused()
                            && let Some(buffer) = state.buffer.data()
                        {
                            self.request_older_chathistory(clients, &buffer);
                        }

                        return (
                            self.get_focused_mut().map_or_else(
                                Task::none,
                                |(window, id, pane)| {
                                    pane.buffer.scroll_to_start(config).map(
                                        move |message| {
                                            Message::Pane(
                                                window,
                                                pane::Message::Buffer(
                                                    id, message,
                                                ),
                                            )
                                        },
                                    )
                                },
                            ),
                            None,
                        );
                    }
                    ScrollToBottom => {
                        let task = self.get_focused_mut().map_or_else(
                            Task::none,
                            |(window, pane, state)| {
                                let mut task = state
                                    .buffer
                                    .scroll_to_end(config)
                                    .map(move |message| {
                                        Message::Pane(
                                            window,
                                            pane::Message::Buffer(
                                                pane, message,
                                            ),
                                        )
                                    });

                                if config
                                    .buffer
                                    .mark_as_read
                                    .on_scroll_to_bottom
                                {
                                    task =
                                        task.chain(Task::done(Message::Pane(
                                            window,
                                            pane::Message::MarkAsRead,
                                        )));
                                }

                                task
                            },
                        );

                        return (task, None);
                    }
                    CycleNextUnreadBuffer => {
                        let all_buffers =
                            all_buffers_with_has_unread(clients, &self.history);
                        let open_buffers = open_buffers(self);

                        if let Some((window, pane, state, history)) =
                            self.get_focused_with_history_mut()
                            && let Some(buffer) = cycle_next_unread_buffer(
                                state.buffer.upstream(),
                                all_buffers,
                                &open_buffers,
                            )
                        {
                            mark_as_read_on_buffer_close(
                                &state.buffer,
                                history,
                                clients,
                                config,
                            );

                            state.buffer = Buffer::from_data(
                                data::Buffer::Upstream(buffer),
                                clients,
                                history,
                                state.size,
                                config,
                            );
                            self.last_changed = Some(Instant::now());
                            return (self.focus_pane(window, pane), None);
                        }
                    }
                    CyclePreviousUnreadBuffer => {
                        let all_buffers =
                            all_buffers_with_has_unread(clients, &self.history);
                        let open_buffers = open_buffers(self);

                        if let Some((window, pane, state, history)) =
                            self.get_focused_with_history_mut()
                            && let Some(buffer) = cycle_previous_unread_buffer(
                                state.buffer.upstream(),
                                all_buffers,
                                &open_buffers,
                            )
                        {
                            mark_as_read_on_buffer_close(
                                &state.buffer,
                                history,
                                clients,
                                config,
                            );

                            state.buffer = Buffer::from_data(
                                data::Buffer::Upstream(buffer),
                                clients,
                                history,
                                state.size,
                                config,
                            );
                            self.last_changed = Some(Instant::now());
                            return (self.focus_pane(window, pane), None);
                        }
                    }
                    MarkAsRead => {
                        if let Some((_, _, pane)) = self.get_focused_mut()
                            && let Some(kind) = pane
                                .buffer
                                .data()
                                .and_then(history::Kind::from_buffer)
                        {
                            mark_as_read(
                                kind,
                                &mut self.history,
                                clients,
                                TokenPriority::User,
                            );
                        }
                    }
                    OpenConfigFile => {
                        let _ = open_url::open(Config::path());
                    }
                }
            }
            Message::FileTransfer(update) => {
                self.file_transfers.update(update, config);
            }
            Message::SendFileSelected(server, to, path) => {
                if let Some(server_handle) = clients.get_server_handle(&server)
                {
                    let query = target::Query::from(&to);

                    if let Some(path) = path
                        && let Some(event) = self.file_transfers.send(
                            file_transfer::SendRequest {
                                to,
                                path,
                                server: server.clone(),
                                server_handle: server_handle.clone(),
                            },
                            config,
                        )
                    {
                        return (
                            self.handle_file_transfer_event(
                                &server,
                                &query,
                                event,
                                &config.buffer,
                            ),
                            None,
                        );
                    }
                }
            }
            Message::CloseContextMenu(window, any_closed) => {
                if !any_closed {
                    if let Some((_, _, state, history)) =
                        self.get_focused_with_history_mut()
                    {
                        if state.buffer.close_picker() {
                            return (Task::none(), None);
                        // We only want to call clear_draft_reply if
                        // close_picker does not return true.
                        } else if state
                            .buffer
                            .clear_draft_reply(history, config)
                        {
                            return (Task::none(), None);
                        }
                    }

                    if self.is_pane_maximized() && window == self.main_window()
                    {
                        self.panes.main.restore();
                    }
                }
            }
            Message::ThemeEditor(message) => {
                let mut editor_event = None;
                let mut event = None;
                let mut tasks = vec![];

                if let Some(editor) = self.theme_editor.as_mut() {
                    let (task, event) = editor.update(message, theme);

                    tasks.push(task.map(Message::ThemeEditor));
                    editor_event = event;
                }

                if let Some(editor_event) = editor_event {
                    match editor_event {
                        theme_editor::Event::Close => {
                            if let Some(editor) = self.theme_editor.take() {
                                tasks.push(window::close(editor.window));
                            }
                        }
                        theme_editor::Event::ReloadThemes => {
                            event = Some(Event::ReloadThemes);
                        }
                    }
                }

                return (Task::batch(tasks), event);
            }
            Message::ConfigReloaded(config_result) => {
                return (
                    Task::none(),
                    Some(Event::ConfigReloaded(config_result)),
                );
            }
            Message::Client(message) => match message {
                client::Message::ChatHistoryRequest(server, subcommand) => {
                    clients.send_chathistory_request(
                        &server,
                        subcommand,
                        TokenPriority::High,
                    );
                }
                client::Message::ChatHistoryTargetsTimestampUpdated(
                    server,
                    timestamp,
                    Ok(()),
                ) => {
                    log::debug!(
                        "updated targets timestamp for {server} to {timestamp}"
                    );
                }
                client::Message::ChatHistoryTargetsTimestampUpdated(
                    server,
                    timestamp,
                    Err(error),
                ) => {
                    log::warn!(
                        "failed to update targets timestamp for {server} to {timestamp}: {error}"
                    );
                }
                client::Message::RequestNewerChatHistory(
                    server,
                    target,
                    server_time,
                    allow_at,
                ) => {
                    let message_reference_types = clients
                        .get_server_chathistory_message_reference_types(
                            &server,
                        );

                    let message_reference =
                        self.history.last_can_reference_before_or_at(
                            server.clone(),
                            target.clone(),
                            server_time,
                            allow_at,
                            &message_reference_types,
                        );

                    let limit = clients.get_server_chathistory_limit(&server);

                    clients.send_chathistory_request(
                        &server,
                        ChatHistorySubcommand::Latest(
                            target,
                            message_reference,
                            limit,
                        ),
                        TokenPriority::High,
                    );
                }
            },
            Message::LoadPreview((url, Ok(preview))) => {
                log::trace!("Preview loaded for {url}");
                if let hash_map::Entry::Occupied(mut entry) =
                    self.previews.entry(url)
                {
                    *entry.get_mut() = preview::State::Loaded(preview);
                }
            }
            Message::ServerIcon(message) => {
                self.server_icons.update(message);
            }
            Message::LoadPreview((url, Err(error))) => {
                if matches!(error, preview::LoadError::Disabled) {
                    log::trace!("Failed to load preview for {url}: {error}");
                } else {
                    log::debug!("Failed to load preview for {url}: {error}");
                }
                if self.previews.contains_key(&url) {
                    self.previews.insert(url, preview::State::Error(error));
                }
            }
            Message::NewWindow(window, pane) => {
                let (state, pane) = pane_grid::State::new(pane);
                self.panes.popout.insert(window, state);

                return (self.focus_pane(window, pane), None);
            }
            Message::Filehost(msg) => {
                return (
                    self.handle_filehost_message(msg, clients, config),
                    None,
                );
            }
            Message::ProceedWithFilehostUpload => {
                let task = self
                    .filehost
                    .proceed(clients, self.http_client.clone(), &config.proxy)
                    .map(Message::Filehost);
                return (task, None);
            }
            Message::CancelFilehostUpload => {
                let task = self.filehost.cancel().map(Message::Filehost);
                return (task, None);
            }
        }

        (Task::none(), None)
    }

    pub fn view_window<'a>(
        &'a self,
        window: window::Id,
        servers: &'a server::Map,
        clients: &'a client::Map,
        version: &'a Version,
        config: &'a Config,
        theme: &'a Theme,
    ) -> Element<'a, Message> {
        if let Some(state) = self.panes.popout.get(&window) {
            let pane_gap = config.pane.gap.outer;
            let top_padding =
                platform_specific::popped_out_window_padding(config) + pane_gap;
            let padding = padding::all(pane_gap).top(top_padding as f32);

            let content = container(
                PaneGrid::new(state, |id, pane, _maximized| {
                    let is_focused = self.focus == Focus { window, pane: id };
                    let buffer = pane.buffer.data();
                    let settings = buffer
                        .as_ref()
                        .and_then(|b| self.buffer_settings.get(b));

                    pane.view(
                        id,
                        1,
                        is_focused,
                        false,
                        self.typing_animation.as_ref(),
                        clients,
                        &self.file_transfers,
                        &self.history,
                        &self.previews,
                        &self.side_menu,
                        config,
                        theme,
                        settings,
                        window != self.main_window(),
                    )
                })
                .spacing(config.pane.gap.inner)
                .on_click(pane::Message::PaneClicked),
            )
            .width(Length::Fill)
            .height(Length::Fill)
            .padding(padding);

            let base = Element::new(content)
                .map(move |message| Message::Pane(window, message));
            let base = self.with_command_bar_overlay(
                base, window, servers, clients, version, config,
            );

            return self.with_keyboard_shortcuts(base, config);
        } else if let Some(editor) = self.theme_editor.as_ref()
            && editor.window == window
        {
            return editor.view(config, theme).map(Message::ThemeEditor);
        }

        column![].into()
    }

    pub fn view<'a>(
        &'a self,
        servers: &'a server::Map,
        clients: &'a client::Map,
        version: &'a Version,
        config: &'a Config,
        theme: &'a Theme,
    ) -> Element<'a, Message> {
        let pane_grid: Element<_> =
            PaneGrid::new(&self.panes.main, |id, pane, maximized| {
                let is_focused = self.focus
                    == Focus {
                        window: self.main_window(),
                        pane: id,
                    };
                let panes = self.panes.main.panes.len();
                let buffer = pane.buffer.data();
                let settings =
                    buffer.as_ref().and_then(|b| self.buffer_settings.get(b));

                pane.view(
                    id,
                    panes,
                    is_focused,
                    maximized,
                    self.typing_animation.as_ref(),
                    clients,
                    &self.file_transfers,
                    &self.history,
                    &self.previews,
                    &self.side_menu,
                    config,
                    theme,
                    settings,
                    false,
                )
            })
            .on_click(pane::Message::PaneClicked)
            .on_resize(6, pane::Message::PaneResized)
            .on_drag(pane::Message::PaneDragged)
            .spacing(config.pane.gap.inner)
            .into();

        let pane_padding = match config.sidebar.position {
            data::config::sidebar::Position::Left => {
                padding::all(config.pane.gap.outer).left(config.pane.gap.inner)
            }
            data::config::sidebar::Position::Top => {
                padding::all(config.pane.gap.outer).top(config.pane.gap.inner)
            }
            data::config::sidebar::Position::Right => {
                padding::all(config.pane.gap.outer).right(config.pane.gap.inner)
            }
            data::config::sidebar::Position::Bottom => {
                padding::all(config.pane.gap.outer)
                    .bottom(config.pane.gap.inner)
            }
        };

        let pane_grid =
            container(pane_grid.map(move |message| {
                Message::Pane(self.main_window(), message)
            }))
            .width(Length::Fill)
            .height(Length::Fill)
            .padding(pane_padding);

        let side_menu = self
            .side_menu
            .view(
                servers,
                clients,
                &self.history,
                &self.panes,
                self.focus,
                &self.server_icons,
                config,
                &self.file_transfers,
                version,
                theme,
            )
            .map(|e| e.map(Message::Sidebar));

        let content = match config.sidebar.position {
            data::config::sidebar::Position::Left
            | data::config::sidebar::Position::Top => {
                vec![
                    side_menu.unwrap_or_else(|| row![].into()),
                    pane_grid.into(),
                ]
            }
            data::config::sidebar::Position::Right
            | data::config::sidebar::Position::Bottom => {
                vec![
                    pane_grid.into(),
                    side_menu.unwrap_or_else(|| row![].into()),
                ]
            }
        };

        let base: Element<Message> = if config.sidebar.position.is_horizontal()
        {
            Column::with_children(content)
                .width(Length::Fill)
                .height(Length::Fill)
                .into()
        } else {
            Row::with_children(content)
                .width(Length::Fill)
                .height(Length::Fill)
                .into()
        };

        let base = self.with_command_bar_overlay(
            base,
            self.main_window(),
            servers,
            clients,
            version,
            config,
        );

        self.with_keyboard_shortcuts(base, config)
    }

    fn with_command_bar_overlay<'a>(
        &'a self,
        base: Element<'a, Message>,
        window: window::Id,
        servers: &'a server::Map,
        clients: &'a client::Map,
        version: &'a Version,
        config: &'a Config,
    ) -> Element<'a, Message> {
        let base = if self.command_bar_window == Some(window)
            && let Some(command_bar) = self.command_bar.as_ref()
        {
            let background = anchored_overlay(
                base,
                container(
                    Space::new().width(Length::Fill).height(Length::Fill),
                )
                .width(Length::Fill)
                .height(Length::Fill)
                .style(theme::container::transparent_overlay),
                anchored_overlay::Anchor::BelowTopCentered,
                0.0,
            );

            anchored_overlay(
                background,
                command_bar
                    .view(
                        servers,
                        clients,
                        &all_buffers(clients, &self.history),
                        self.focus,
                        self.buffer_resize_action(),
                        version,
                        config,
                        self.main_window(),
                    )
                    .map(Message::Task),
                anchored_overlay::Anchor::BelowTopCentered,
                10.0,
            )
        } else {
            // Align `base` into same view tree shape
            // as `anchored_overlay` to prevent diff
            // from firing when displaying command bar
            column![column![base]].into()
        };

        let focused_target_info = self
            .panes
            .get(self.focus.window, self.focus.pane)
            .and_then(|pane| {
                let target = pane.buffer.target()?;
                let server = pane.buffer.server()?;
                Some((target, server))
            });

        if self.filehost.file_being_hovered {
            let (overlay_text, is_error) = if !config.filehost.file_drop() {
                (String::from("File drop is disabled here"), true)
            } else if let Some((target, server)) = focused_target_info {
                (format!("Drop to upload file to {target} @ {server}"), false)
            } else {
                (String::from("Upload is not valid here"), true)
            };

            let text_style: fn(&Theme) -> iced::widget::text::Style =
                if is_error {
                    theme::text::error
                } else {
                    theme::text::primary
                };

            stack![
                base,
                container(
                    center(
                        container(text(overlay_text).style(text_style))
                            .style(theme::container::tooltip)
                            .padding(8),
                    )
                    .width(Length::Fill)
                    .height(Length::Fill),
                )
                .width(Length::Fill)
                .height(Length::Fill)
                .style(theme::container::transparent_overlay),
            ]
            .into()
        } else {
            // Wrap in stack so iced can track base properly
            stack![base].into()
        }
    }

    fn with_keyboard_shortcuts<'a>(
        &'a self,
        base: Element<'a, Message>,
        config: &'a Config,
    ) -> Element<'a, Message> {
        shortcut(base, config.keyboard.shortcuts(), Message::Shortcut)
    }

    pub fn handle_buffer_event(
        &mut self,
        window: window::Id,
        id: pane_grid::Pane,
        event: buffer::Event,
        clients: &mut data::client::Map,
        controllers: &mut stream::Map,
        config: &Config,
    ) -> (Task<Message>, Option<Event>) {
        let Some(pane) = self.panes.get_mut(window, id) else {
            return (Task::none(), None);
        };

        match event {
            buffer::Event::OpenInternalBuffer(buffer) => {
                // If opening channel discovery with a server, send LIST command if needed
                if let buffer::Internal::ChannelDiscovery(Some(server)) =
                    &buffer
                {
                    Self::send_list_command_if_needed(server, pane, clients);
                }

                return (
                    self.open_buffer(
                        data::Buffer::Internal(buffer),
                        config.actions.buffer.click_channel_name,
                        clients,
                        config,
                    ),
                    None,
                );
            }
            buffer::Event::ContextMenu(event) => {
                let mut tasks = if matches!(
                    event,
                    buffer::context_menu::Event::LoadUserAvatar(..)
                ) {
                    vec![]
                } else {
                    vec![context_menu::close(convert::identity).map(
                        move |any_closed| {
                            Message::CloseContextMenu(window, any_closed)
                        },
                    )]
                };

                let event = match event {
                    buffer::context_menu::Event::LoadUserAvatar(
                        server,
                        url,
                    ) => {
                        let client = if clients
                            .get_server_proxy_config(&server)
                            .is_some()
                        {
                            clients.get_server_http_client(&server)
                        } else {
                            self.http_client.clone()
                        };

                        if let Some(client) = client
                            && !self.previews.contains_key(&url)
                        {
                            self.previews
                                .insert(url.clone(), preview::State::Loading);
                            tasks.push(Task::perform(
                                data::preview::load_avatar(
                                    url.clone(),
                                    client,
                                    config.metadata.avatar.clone(),
                                    config.preview.clone(),
                                    self.previews_cache.clone(),
                                ),
                                move |result| {
                                    Message::LoadPreview((url.clone(), result))
                                },
                            ));
                        }

                        None
                    }
                    buffer::context_menu::Event::CopyUrl(url) => {
                        tasks.push(clipboard::write(url));
                        None
                    }
                    buffer::context_menu::Event::CopyText(text) => {
                        tasks.push(clipboard::write(text));
                        None
                    }
                    buffer::context_menu::Event::OpenUrl(url) => {
                        Some(Event::OpenUrl(
                            url,
                            config.buffer.url.prompt_before_open,
                        ))
                    }
                    buffer::context_menu::Event::HidePreview(hash, url) => {
                        let kind = pane
                            .buffer
                            .data()
                            .and_then(history::Kind::from_buffer);
                        let parsed = url::Url::parse(&url).ok();

                        if let (Some(kind), Some(url)) = (kind, parsed) {
                            self.history.hide_preview(kind, hash, url);
                        }

                        None
                    }
                    buffer::context_menu::Event::ShowPreview(hash, url) => {
                        let kind = pane
                            .buffer
                            .data()
                            .and_then(history::Kind::from_buffer);
                        let parsed = url::Url::parse(&url).ok();

                        if let (Some(kind), Some(url)) = (kind, parsed) {
                            self.history.show_preview(kind, hash, &url);
                            tasks.push(
                                self.reload_visible_previews(clients, config),
                            );
                        }

                        None
                    }
                    buffer::context_menu::Event::ToggleAccessLevel(
                        server,
                        channel,
                        nick,
                        mode,
                    ) => {
                        let buffer = buffer::Upstream::Channel(
                            server.clone(),
                            channel.clone(),
                        );

                        let command = command::Irc::Mode(
                            channel.to_string(),
                            Some(mode),
                            Some(vec![nick.to_string()]),
                        );
                        let input = data::Input::from_command(buffer, command);

                        if let Some(encoded) = input.encoded() {
                            clients.send(
                                &input.buffer,
                                encoded,
                                TokenPriority::User,
                            );
                        }

                        None
                    }
                    buffer::context_menu::Event::SendWhois(server, nick) => {
                        let buffer =
                            pane.buffer.upstream().cloned().unwrap_or_else(
                                || buffer::Upstream::Server(server.clone()),
                            );

                        let command =
                            command::Irc::Whois(None, nick.to_string());

                        let input =
                            data::Input::from_command(buffer.clone(), command);

                        if let Some(encoded) = input.encoded() {
                            clients.send(
                                &input.buffer,
                                encoded,
                                TokenPriority::User,
                            );
                        }

                        None
                    }
                    buffer::context_menu::Event::SendWhowas(server, nick) => {
                        let buffer =
                            pane.buffer.upstream().cloned().unwrap_or_else(
                                || buffer::Upstream::Server(server.clone()),
                            );

                        let command =
                            command::Irc::Whowas(nick.to_string(), None);

                        let input =
                            data::Input::from_command(buffer.clone(), command);

                        if let Some(encoded) = input.encoded() {
                            clients.send(
                                &input.buffer,
                                encoded,
                                TokenPriority::User,
                            );
                        }

                        None
                    }
                    buffer::context_menu::Event::OpenQuery(
                        server,
                        query,
                        buffer_action,
                    ) => {
                        let buffer = buffer::Upstream::Query(server, query);

                        tasks.push(self.open_buffer(
                            data::Buffer::Upstream(buffer),
                            buffer_action,
                            clients,
                            config,
                        ));

                        None
                    }
                    buffer::context_menu::Event::InsertNickname(nick) => {
                        if let Some((_, _, pane, history)) =
                            self.get_focused_with_history_mut()
                        {
                            pane.buffer.insert_user_to_input(
                                nick,
                                history,
                                &config.buffer.text_input.autocomplete,
                            );
                        }

                        None
                    }
                    buffer::context_menu::Event::SendFile(server, nick) => {
                        tasks.push(Task::perform(
                            async move {
                                rfd::AsyncFileDialog::new()
                                    .pick_file()
                                    .await
                                    .map(|handle| handle.path().to_path_buf())
                            },
                            move |file| {
                                Message::SendFileSelected(
                                    server.clone(),
                                    nick.clone(),
                                    file,
                                )
                            },
                        ));

                        None
                    }
                    buffer::context_menu::Event::CtcpRequest(
                        command,
                        server,
                        nick,
                        params,
                    ) => {
                        let buffer =
                            pane.buffer.upstream().cloned().unwrap_or_else(
                                || buffer::Upstream::Server(server.clone()),
                            );

                        let command = command::Irc::Ctcp(
                            command,
                            nick.to_string(),
                            params,
                        );

                        let input =
                            data::Input::from_command(buffer.clone(), command);

                        if let Some(encoded) = input.encoded() {
                            clients.send(
                                &input.buffer,
                                encoded,
                                TokenPriority::High,
                            );
                        }

                        None
                    }
                    buffer::context_menu::Event::CopyTimestamp(date_time) => {
                        let date_time =
                            config.buffer.format_copy_timestamp(&date_time);

                        tasks.push(clipboard::write(date_time));

                        None
                    }
                    buffer::context_menu::Event::DeleteMessage(
                        server_time,
                        hash,
                    ) => {
                        if let Some(kind) =
                            pane.buffer.upstream().map(|buffer| {
                                history::Kind::from_input_buffer(buffer.clone())
                            })
                            && let Some(future) = self.history.remove_message(
                                kind,
                                server_time,
                                hash,
                                false,
                            )
                        {
                            tasks.push(
                                Task::future(future).map(Message::History),
                            );
                        }

                        None
                    }
                    buffer::context_menu::Event::ResendMessage(
                        server_time,
                        hash,
                    ) => {
                        if let Some(kind) =
                            pane.buffer.upstream().map(|buffer| {
                                history::Kind::from_input_buffer(buffer.clone())
                            })
                            && let Some(future) = self.history.remove_message(
                                kind,
                                server_time,
                                hash,
                                true,
                            )
                        {
                            tasks.push(
                                Task::future(future).map(Message::History),
                            );
                        }

                        None
                    }
                    buffer::context_menu::Event::OpenReactionModal(
                        msgid,
                        selected_reactions,
                    ) => {
                        tasks.push(
                            pane.open_modal(
                                id,
                                modal::Modal::AddReaction(
                                    reaction_modal::State::new(
                                        msgid,
                                        selected_reactions,
                                    ),
                                ),
                            )
                            .map(move |message| Message::Pane(window, message)),
                        );
                        None
                    }
                    buffer::context_menu::Event::RedactMessage(msgid) => {
                        tasks.push(
                            pane.open_modal(
                                id,
                                modal::Modal::RedactReason(
                                    redaction_modal::State::new(msgid),
                                ),
                            )
                            .map(move |message| Message::Pane(window, message)),
                        );

                        None
                    }
                    buffer::context_menu::Event::Reply {
                        msgid,
                        server_time,
                        ..
                    } => {
                        if let Some(kind) =
                            pane.buffer.upstream().map(|buffer| {
                                history::Kind::from_input_buffer(buffer.clone())
                            })
                            && let Some(reply_preview) =
                                self.history.generate_reply_preview(
                                    kind,
                                    &msgid,
                                    &server_time,
                                )
                        {
                            pane.buffer.set_reply_preview(reply_preview);
                        }

                        tasks.push(self.focus_pane(window, id));

                        None
                    }
                    buffer::context_menu::Event::ExpandMessage(
                        server_time,
                        hash,
                    ) => {
                        if let Some(kind) =
                            pane.buffer.upstream().map(|buffer| {
                                history::Kind::from_input_buffer(buffer.clone())
                            })
                        {
                            self.history.expand_message(
                                kind,
                                server_time,
                                hash,
                                &config.buffer.server_messages.condense,
                            );
                        }

                        None
                    }
                    buffer::context_menu::Event::ContractMessage(
                        server_time,
                        hash,
                    ) => {
                        if let Some(kind) =
                            pane.buffer.upstream().map(|buffer| {
                                history::Kind::from_input_buffer(buffer.clone())
                            })
                        {
                            self.history.contract_message(
                                kind,
                                server_time,
                                hash,
                                &config.buffer.server_messages.condense,
                            );
                        }

                        None
                    }
                };

                return (Task::batch(tasks), event);
            }
            buffer::Event::OpenBuffers(server, targets) => {
                let mut tasks = vec![];

                for (target, buffer_action) in targets {
                    tasks.push(self.open_target(
                        server.clone(),
                        target,
                        clients,
                        buffer_action,
                        config,
                    ));
                }

                return (Task::batch(tasks), None);
            }
            buffer::Event::LeaveBuffers(targets, reason) => {
                if let Some(server) = pane
                    .buffer
                    .upstream()
                    .map(buffer::Upstream::server)
                    .cloned()
                {
                    let mut tasks = vec![];

                    for target in targets {
                        tasks.push(self.leave_server_target(
                            clients,
                            config,
                            server.clone(),
                            target,
                            reason.clone(),
                        ));
                    }

                    return (Task::batch(tasks), None);
                }
            }
            buffer::Event::History(history_task) => {
                return (history_task.map(Message::History), None);
            }
            buffer::Event::GoToMessage(server, channel, message) => {
                let buffer = data::Buffer::Upstream(buffer::Upstream::Channel(
                    server, channel,
                ));

                let mut tasks = vec![];

                if self.panes.get_mut_by_buffer(&buffer).is_none() {
                    tasks.push(self.open_buffer(
                        buffer.clone(),
                        config.actions.buffer.click_highlight,
                        clients,
                        config,
                    ));
                }

                if let Some((window, pane, state)) =
                    self.panes.get_mut_by_buffer(&buffer)
                {
                    tasks.push(
                        state
                            .buffer
                            .scroll_to_message(message, &self.history, config)
                            .map(move |message| {
                                Message::Pane(
                                    window,
                                    pane::Message::Buffer(pane, message),
                                )
                            }),
                    );
                }

                return (Task::batch(tasks), None);
            }
            buffer::Event::RequestOlderChatHistory => {
                if let Some(buffer) = pane.buffer.data() {
                    self.request_older_chathistory(clients, &buffer);
                }
            }
            buffer::Event::PreviewChanged => {
                let visible = self.visible_urls_with_preview_clients(clients);
                let tracking =
                    self.previews.keys().cloned().collect::<HashSet<_>>();
                let missing = visible
                    .iter()
                    .filter_map(|(url, preview_client)| {
                        (!tracking.contains(url))
                            .then_some((url.clone(), preview_client.clone()))
                    })
                    .collect::<Vec<_>>();
                let removed = tracking
                    .into_iter()
                    .filter(|url| !visible.contains_key(url))
                    .collect::<HashSet<_>>();

                for (url, _) in &missing {
                    self.previews.insert(url.clone(), preview::State::Loading);
                }

                for url in removed {
                    self.previews.remove(&url);
                }

                return (
                    Task::batch(missing.into_iter().map(
                        |(url, preview_client)| {
                            Task::perform(
                                data::preview::load(
                                    url.clone(),
                                    preview_client.clone(),
                                    config.preview.clone(),
                                    self.previews_cache.clone(),
                                ),
                                move |result| {
                                    Message::LoadPreview((url.clone(), result))
                                },
                            )
                        },
                    )),
                    None,
                );
            }
            buffer::Event::HidePreview(kind, hash, url) => {
                self.history.hide_preview(kind, hash, url);
            }
            buffer::Event::MarkAsRead(kind) => {
                mark_as_read(
                    kind,
                    &mut self.history,
                    clients,
                    TokenPriority::User,
                );
            }
            buffer::Event::OpenUrl(url) => {
                return (
                    Task::none(),
                    Some(Event::OpenUrl(
                        url,
                        config.buffer.url.prompt_before_open,
                    )),
                );
            }
            buffer::Event::ImagePreview(image) => {
                return (Task::none(), Some(Event::ImagePreview(image)));
            }
            buffer::Event::ExpandMessage(server_time, hash) => {
                if let Some(kind) =
                    pane.buffer.data().and_then(history::Kind::from_buffer)
                {
                    self.history.expand_message(
                        kind,
                        server_time,
                        hash,
                        &config.buffer.server_messages.condense,
                    );
                }
            }
            buffer::Event::ContractMessage(server_time, hash) => {
                if let Some(kind) =
                    pane.buffer.data().and_then(history::Kind::from_buffer)
                {
                    self.history.contract_message(
                        kind,
                        server_time,
                        hash,
                        &config.buffer.server_messages.condense,
                    );
                }
            }
            buffer::Event::InputSent {
                history_task,
                open_buffers,
            } => {
                let mut tasks = vec![];

                if let Some(server) = pane
                    .buffer
                    .upstream()
                    .map(buffer::Upstream::server)
                    .cloned()
                {
                    for (target, buffer_action) in open_buffers {
                        tasks.push(self.open_target(
                            server.clone(),
                            target,
                            clients,
                            buffer_action,
                            config,
                        ));
                    }
                }

                return (
                    history_task
                        .map(Message::History)
                        .chain(Task::batch(tasks)),
                    None,
                );
            }
            buffer::Event::SelectedServer(server) => {
                Self::send_list_command_if_needed(&server, pane, clients);

                self.last_changed = Some(Instant::now());

                return (Task::none(), None);
            }
            buffer::Event::SendUnsafeList(server) => {
                Self::send_list_command(&server, pane, clients);

                return (Task::none(), None);
            }
            buffer::Event::OpenServer(server) => {
                return (Task::none(), Some(Event::OpenServer(server)));
            }
            buffer::Event::Reconnect(server) => {
                controllers.connect(&server);
            }
            buffer::Event::FilehostUpload {
                server,
                target,
                file_paths,
                upload_ids,
                abort_registrations,
            } => {
                let Some(upload_url) =
                    clients.get_filehost(&server).map(String::from)
                else {
                    let casemapping =
                        clients.get_server_casemapping_or_default(&server);
                    let task = self.broadcast(
                        &server,
                        casemapping,
                        config,
                        Utc::now(),
                        false,
                        Broadcast::FilehostUploadFailed {
                            error: format!(
                                "no filehost configured for server {server}"
                            ),
                            target,
                        },
                    );
                    return (task, None);
                };

                let pending = filehost::PendingUpload {
                    window,
                    pane_id: id,
                    is_override_url: clients.get_filehost_is_override(&server),
                    has_credentials: clients
                        .get_filehost_auth(&server)
                        .is_some(),
                    server,
                    target,
                    upload_url,
                    file_paths,
                    upload_ids,
                    abort_registrations,
                };

                let (task, event) = self.filehost.upload(
                    pending,
                    clients,
                    self.http_client.clone(),
                    &config.proxy,
                );

                let task = task.map(Message::Filehost);

                if let Some(filehost::Event::PromptBeforeUpload {
                    upload_url,
                    has_credentials,
                    window,
                }) = event
                {
                    return (
                        task,
                        Some(Event::PromptBeforeFileUpload {
                            upload_url,
                            has_credentials,
                            window,
                        }),
                    );
                }

                return (task, None);
            }
        }

        (Task::none(), None)
    }

    fn handle_filehost_message(
        &mut self,
        msg: filehost::Message,
        clients: &client::Map,
        config: &Config,
    ) -> Task<Message> {
        match msg {
            filehost::Message::UploadDone {
                window,
                pane_id,
                target,
                id,
                url,
            } => {
                let buf_msg = match &target {
                    Some(Target::Channel(_)) => buffer::Message::Channel(
                        buffer::channel::Message::FilehostUploadDone {
                            id,
                            url,
                        },
                    ),
                    Some(Target::Query(_)) => buffer::Message::Query(
                        buffer::query::Message::FilehostUploadDone { id, url },
                    ),
                    None => buffer::Message::Server(
                        buffer::server::Message::FilehostUploadDone { id, url },
                    ),
                };
                Task::done(Message::Pane(
                    window,
                    pane::Message::Buffer(pane_id, buf_msg),
                ))
            }
            filehost::Message::UploadFailed {
                window,
                pane_id,
                server,
                target,
                id,
                error,
            } => {
                let casemapping =
                    clients.get_server_casemapping_or_default(&server);
                let history_task = self.broadcast(
                    &server,
                    casemapping,
                    config,
                    Utc::now(),
                    false,
                    Broadcast::FilehostUploadFailed {
                        error,
                        target: target.clone(),
                    },
                );
                let decrement_msg = match &target {
                    Some(Target::Channel(_)) => buffer::Message::Channel(
                        buffer::channel::Message::FilehostUploadDone {
                            id,
                            url: None,
                        },
                    ),
                    Some(Target::Query(_)) => buffer::Message::Query(
                        buffer::query::Message::FilehostUploadDone {
                            id,
                            url: None,
                        },
                    ),
                    None => buffer::Message::Server(
                        buffer::server::Message::FilehostUploadDone {
                            id,
                            url: None,
                        },
                    ),
                };
                let decrement_task = Task::done(Message::Pane(
                    window,
                    pane::Message::Buffer(pane_id, decrement_msg),
                ));
                Task::batch(vec![history_task, decrement_task])
            }
            filehost::Message::KnownSaved(Err(e)) => {
                log::error!("failed to save known filehost URLs: {e}");
                Task::none()
            }
            filehost::Message::KnownSaved(Ok(())) => Task::none(),
        }
    }

    pub fn handle_event(
        &mut self,
        window: window::Id,
        event: event::Event,
        servers: &server::Map,
        clients: &mut data::client::Map,
        version: &Version,
        config: &Config,
        theme: &mut Theme,
    ) -> Task<Message> {
        use event::Event::*;

        match event {
            Escape => {
                // Order of operations
                //
                // - Close command bar (if this window owns it)
                // - Close context menu
                // - Close command/emoji picker
                // - Restore maximized pane (if main window)
                if self.command_bar_window == Some(window) {
                    self.toggle_command_bar(
                        servers,
                        clients,
                        &closed_buffers(self, clients),
                        version,
                        config,
                        theme,
                    )
                } else {
                    context_menu::close(convert::identity).map(
                        move |any_closed| {
                            Message::CloseContextMenu(window, any_closed)
                        },
                    )
                }
            }
            Copy => selectable_text::selected(|selected_text| {
                Message::SelectedText(
                    selected_text,
                    advanced::clipboard::Kind::Standard,
                )
            }),
            LeftClick => self.refocus_pane(),
            UpdatePrimaryClipboard => {
                selectable_text::selected(|selected_text| {
                    Message::SelectedText(
                        selected_text,
                        advanced::clipboard::Kind::Primary,
                    )
                })
            }
        }
    }

    fn handle_command_bar_command(
        &mut self,
        command: command_bar::Command,
        clients: &mut client::Map,
        controllers: &mut stream::Map,
        servers: &server::Map,
        theme: &mut Theme,
        config: &Config,
        main_window: &Window,
    ) -> (Task<Message>, Option<Event>) {
        match command {
            command_bar::Command::Version(command) => match command {
                command_bar::Version::Application(_) => {
                    let _ = open_url::open(RELEASE_WEBSITE);
                    (Task::none(), None)
                }
            },
            command_bar::Command::Buffer(command) => match command {
                command_bar::Buffer::Maximize(_) => {
                    self.maximize_pane();
                    (Task::none(), None)
                }
                command_bar::Buffer::NewHorizontal => {
                    (self.new_pane(pane_grid::Axis::Horizontal), None)
                }
                command_bar::Buffer::NewVertical => {
                    (self.new_pane(pane_grid::Axis::Vertical), None)
                }
                command_bar::Buffer::Close => {
                    let Focus { window, pane } = self.focus;
                    (self.close_pane(clients, config, window, pane), None)
                }
                command_bar::Buffer::Replace(buffer) => (
                    self.open_buffer(
                        data::Buffer::Upstream(buffer),
                        BufferAction::ReplacePane,
                        clients,
                        config,
                    ),
                    None,
                ),
                command_bar::Buffer::Popout => {
                    (self.popout_pane(clients, config), None)
                }
                command_bar::Buffer::Merge => {
                    (self.merge_pane(clients, config), None)
                }
                command_bar::Buffer::ToggleInternal(buffer) => {
                    (self.toggle_internal_buffer(clients, config, buffer), None)
                }
            },
            command_bar::Command::Configuration(command) => match command {
                command_bar::Configuration::OpenConfigDirectory => {
                    let _ = open_url::open(Config::config_dir());
                    (Task::none(), None)
                }
                command_bar::Configuration::OpenCacheDirectory => {
                    let _ = open_url::open(environment::cache_dir());
                    (Task::none(), None)
                }
                command_bar::Configuration::OpenDataDirectory => {
                    let _ = open_url::open(environment::data_dir());
                    (Task::none(), None)
                }
                command_bar::Configuration::OpenWebsite => {
                    let _ = open_url::open(environment::WIKI_WEBSITE);
                    (Task::none(), None)
                }
                command_bar::Configuration::Reload => (
                    Task::perform(Config::load(), Message::ConfigReloaded),
                    None,
                ),
                command_bar::Configuration::OpenConfigFile => {
                    let _ = open_url::open(Config::path());
                    (Task::none(), None)
                }
            },
            command_bar::Command::Theme(command) => match command {
                command_bar::Theme::Switch(new) => {
                    *theme = Theme::from(new);
                    (Task::none(), None)
                }
                command_bar::Theme::OpenEditor => {
                    if let Some(editor) = &self.theme_editor {
                        (window::gain_focus(editor.window), None)
                    } else {
                        let (editor, task) =
                            ThemeEditor::open(main_window, config);

                        self.theme_editor = Some(editor);

                        (task.then(|_| Task::none()), None)
                    }
                }
                command_bar::Theme::OpenThemesWebsite => {
                    let _ = open_url::open(environment::THEME_WEBSITE);
                    (Task::none(), None)
                }
            },
            command_bar::Command::Application(application) => match application
            {
                command_bar::Application::Quit => {
                    (self.exit(clients, config), None)
                }
                command_bar::Application::ToggleFullscreen => {
                    (window::toggle_fullscreen(), Some(Event::ToggleFullscreen))
                }
                command_bar::Application::ToggleSidebarVisibility => {
                    self.side_menu.toggle_visibility();
                    (Task::none(), None)
                }
            },
            command_bar::Command::Server(server) => match server {
                command_bar::Server::Connect(server) => {
                    connect_server(server, controllers, servers);
                    (Task::none(), None)
                }
                command_bar::Server::Disconnect(server) => (
                    Task::none(),
                    Some(Event::QuitServer(
                        server,
                        config.buffer.commands.quit.default_reason.clone(),
                    )),
                ),
                command_bar::Server::ReloadIcon(server) => (
                    self.remove_server_icon(clients, &server)
                        .chain(self.request_server_icon(clients, &server)),
                    None,
                ),
            },
        }
    }

    fn toggle_theme_editor(
        &mut self,
        theme: &mut Theme,
        main_window: &Window,
        config: &Config,
    ) -> Task<Message> {
        if let Some(editor) = self.theme_editor.take() {
            *theme = theme.selected();
            window::close(editor.window)
        } else {
            let (editor, task) = ThemeEditor::open(main_window, config);

            self.theme_editor = Some(editor);

            task.then(|_| Task::none())
        }
    }

    fn toggle_internal_buffer(
        &mut self,
        clients: &mut data::client::Map,
        config: &Config,
        buffer: buffer::Internal,
    ) -> Task<Message> {
        let open = self.panes.iter().find_map(|(window_id, pane, state)| {
            (state.buffer.internal().as_ref() == Some(&buffer))
                .then_some((window_id, pane))
        });

        if let Some((window, pane)) = open {
            self.close_pane(clients, config, window, pane)
        } else {
            self.open_buffer(
                data::Buffer::Internal(buffer),
                config.actions.buffer.local,
                clients,
                config,
            )
        }
    }

    fn send_list_command_if_needed(
        server: &data::Server,
        pane: &Pane,
        clients: &mut data::client::Map,
    ) {
        let is_needed = if clients.get_server_supports_list(server) {
            clients.get_channel_discovery_manager(server).is_none_or(
                data::channel_discovery::Manager::needs_fetch_or_refetch,
            )
        } else {
            clients
                .get_channel_discovery_manager(server)
                .is_some_and(data::channel_discovery::Manager::needs_refetch)
        };

        if is_needed {
            Self::send_list_command(server, pane, clients);
        }
    }

    fn send_list_command(
        server: &data::Server,
        pane: &Pane,
        clients: &mut data::client::Map,
    ) {
        let buffer = pane
            .buffer
            .upstream()
            .cloned()
            .unwrap_or_else(|| buffer::Upstream::Server(server.clone()));

        let command = command::Irc::List(None, None);
        let input = data::Input::from_command(buffer, command);

        if let Some(encoded) = input.encoded() {
            clients.send(&input.buffer, encoded, TokenPriority::User);
        }
    }

    fn open_buffer(
        &mut self,
        buffer: data::Buffer,
        buffer_action: BufferAction,
        clients: &mut data::client::Map,
        config: &Config,
    ) -> Task<Message> {
        // TODO(pounce) reduce clones
        let panes = self.panes.clone();

        self.last_changed = Some(Instant::now());

        if let Some(buffer::Upstream::Query(server, query)) = buffer.upstream()
            && let Some(client) = clients.client_mut(server)
            && let user = User::from(Nick::from(query))
            && !client.is_monitored_user(&user)
        {
            client.add_monitored_user_automated(&user);
        }

        match buffer_action {
            BufferAction::ReplacePane => {
                // If buffer already is open, we swap it with focused pane.
                for (window, id, pane) in panes.iter() {
                    if pane.buffer.data().as_ref() == Some(&buffer) {
                        if window != self.focus.window || id != self.focus.pane
                        {
                            return self.swap_pane_with_focus(window, id);
                        } else {
                            return Task::none();
                        }
                    }
                }

                let Focus { window, pane } = self.focus;

                if let Some(state) = self.panes.get_mut(window, pane) {
                    mark_as_read_on_buffer_close(
                        &state.buffer,
                        &mut self.history,
                        clients,
                        config,
                    );

                    state.buffer = Buffer::from_data(
                        buffer,
                        clients,
                        &self.history,
                        state.size,
                        config,
                    );
                    self.last_changed = Some(Instant::now());

                    Task::batch(vec![
                        self.reset_pane(window, pane),
                        self.focus_pane(window, pane),
                    ])
                } else {
                    log::error!("Didn't find any panes to replace");
                    Task::none()
                }
            }
            BufferAction::NewPane => {
                // If buffer already is open, we focus it.
                for (window, id, pane) in panes.iter() {
                    if pane.buffer.data().as_ref() == Some(&buffer) {
                        self.focus = Focus { window, pane: id };

                        return self.focus_pane(window, id);
                    }
                }

                // If we only have one pane, and its empty, we replace it.
                if self.panes.len() == 1 {
                    for (id, pane) in panes.main.iter() {
                        if matches!(pane.buffer, Buffer::Empty) {
                            self.panes.main.panes.entry(*id).and_modify(|p| {
                                p.buffer = Buffer::from_data(
                                    buffer.clone(),
                                    clients,
                                    &self.history,
                                    p.size,
                                    config,
                                );
                            });
                            self.last_changed = Some(Instant::now());

                            return self.focus_pane(self.main_window(), *id);
                        }
                    }
                }

                let (pane_to_split, pane_to_split_state) = {
                    if matches!(
                        config.pane.split_axis,
                        config::pane::SplitAxis::LargestShorter
                    ) && let Some((pane, pane_state)) =
                        self.panes.main.panes.iter().reduce(
                            |(acc_pane, acc_pane_state), (pane, pane_state)| {
                                let pane_area = pane_state.size.width
                                    * pane_state.size.height;
                                let acc_pane_area = acc_pane_state.size.width
                                    * acc_pane_state.size.height;

                                if pane_area > acc_pane_area {
                                    (pane, pane_state)
                                } else {
                                    (acc_pane, acc_pane_state)
                                }
                            },
                        )
                    {
                        (*pane, pane_state)
                    } else if self.focus.window == self.main_window()
                        && let Some(pane_state) =
                            self.panes.main.panes.get(&self.focus.pane)
                    {
                        (self.focus.pane, pane_state)
                    } else if let Some((pane, pane_state)) =
                        self.panes.main.panes.iter().last()
                    {
                        (*pane, pane_state)
                    } else {
                        log::error!("Didn't find any panes to split");
                        return Task::none();
                    }
                };

                let split_axis = match config.pane.split_axis {
                    config::pane::SplitAxis::Horizontal => {
                        pane_grid::Axis::Horizontal
                    }
                    config::pane::SplitAxis::Vertical => {
                        pane_grid::Axis::Vertical
                    }
                    config::pane::SplitAxis::Shorter
                    | config::pane::SplitAxis::LargestShorter => {
                        if pane_to_split_state.size.height
                            < pane_to_split_state.size.width
                        {
                            pane_grid::Axis::Vertical
                        } else {
                            pane_grid::Axis::Horizontal
                        }
                    }
                };

                let result = self.panes.main.split(
                    split_axis,
                    pane_to_split,
                    Pane::new(Buffer::from_data(
                        buffer,
                        clients,
                        &self.history,
                        pane_to_split_state.size,
                        config,
                    )),
                );

                if let Some((pane, _)) = result {
                    return self.focus_pane(self.main_window(), pane);
                }

                Task::none()
            }
            BufferAction::NewWindow => {
                iced::window::position(self.main_window()).then({
                    let pane = Pane::new(Buffer::from_data(
                        buffer.clone(),
                        clients,
                        &self.history,
                        Size::default(),
                        config,
                    ));

                    let config = config.clone();
                    move |main_window_position| {
                        let (_, task) = window::open(window::Settings {
                            // Just big enough to show all components in combobox
                            position: main_window_position
                                .map(|point| {
                                    window::Position::Specific(
                                        point + Vector::new(20.0, 20.0),
                                    )
                                })
                                .unwrap_or_default(),
                            exit_on_close_request: false,
                            ..window::settings(&config)
                        });

                        task.map({
                            let pane = pane.clone();
                            move |id| Message::NewWindow(id, pane.clone())
                        })
                    }
                })
            }
        }
    }

    pub fn leave_all_queries(
        &mut self,
        clients: &mut data::client::Map,
        config: &Config,
        server: Server,
        queries: Vec<target::Query>,
    ) -> Task<Message> {
        let tasks: Vec<Task<Message>> = queries
            .into_iter()
            .map(|query| {
                let buffer =
                    buffer::Upstream::Query(server.clone(), query.clone());

                self.leave_buffer(clients, config, buffer).0
            })
            .collect();

        Task::batch(tasks)
    }

    pub fn leave_buffer(
        &mut self,
        clients: &mut data::client::Map,
        config: &Config,
        buffer: buffer::Upstream,
    ) -> (Task<Message>, Option<Event>) {
        let open = self.panes.iter().find_map(|(window, pane, state)| {
            (state.buffer.upstream() == Some(&buffer)).then_some((window, pane))
        });

        let mut tasks = vec![];

        // Close pane
        if let Some((window, pane)) = open {
            tasks.push(self.close_pane(clients, config, window, pane));
        }

        match buffer.clone() {
            buffer::Upstream::Server(server) => (
                Task::batch(tasks),
                Some(Event::QuitServer(
                    server,
                    config.buffer.commands.quit.default_reason.clone(),
                )),
            ),
            buffer::Upstream::Channel(server, channel) => {
                // Send part & close history file
                let command = command::Irc::Part(
                    channel.to_string(),
                    config.buffer.commands.part.default_reason.clone(),
                );
                let input = data::Input::from_command(buffer.clone(), command);

                if let Some(encoded) = input.encoded() {
                    clients.send(&buffer, encoded, TokenPriority::High);
                }

                tasks.push(
                    self.history
                        .close(history::Kind::Channel(server, channel), clients)
                        .map_or_else(Task::none, |task| {
                            Task::perform(task, Message::History)
                        }),
                );

                (Task::batch(tasks), None)
            }
            buffer::Upstream::Query(server, nick) => {
                if let Some(client) = clients.client_mut(&server)
                    && let user = User::from(Nick::from(&nick))
                    && client.is_monitored_user_automated(&user)
                {
                    client.remove_monitored_user(&user);
                }

                tasks.push(
                    self.history
                        .close(history::Kind::Query(server, nick), clients)
                        .map_or_else(Task::none, |task| {
                            Task::perform(task, Message::History)
                        }),
                );

                // No PART to send, just close history
                (Task::batch(tasks), None)
            }
        }
    }

    pub fn leave_server_target(
        &mut self,
        clients: &mut data::client::Map,
        config: &Config,
        server: Server,
        target: Target,
        reason: Option<String>,
    ) -> Task<Message> {
        let open = self.panes.iter().find_map(|(window, pane, state)| {
            (state.buffer.server() == Some(server.clone())
                && state.buffer.target() == Some(target.clone()))
            .then_some((window, pane))
        });

        let mut tasks = vec![];

        // Close pane
        if let Some((window, pane)) = open {
            tasks.push(self.close_pane(clients, config, window, pane));
        }

        match target {
            Target::Channel(channel) => {
                let buffer = data::buffer::Upstream::Channel(
                    server.clone(),
                    channel.clone(),
                );

                // Send part & close history file
                let command = command::Irc::Part(channel.to_string(), reason);
                let input = data::Input::from_command(buffer.clone(), command);

                if let Some(encoded) = input.encoded() {
                    clients.send(&buffer, encoded, TokenPriority::User);
                }

                tasks.push(
                    self.history
                        .close(history::Kind::Channel(server, channel), clients)
                        .map_or_else(Task::none, |task| {
                            Task::perform(task, Message::History)
                        }),
                );

                Task::batch(tasks)
            }
            Target::Query(nick) => {
                tasks.push(
                    self.history
                        .close(history::Kind::Query(server, nick), clients)
                        .map_or_else(Task::none, |task| {
                            Task::perform(task, Message::History)
                        }),
                );

                // No PART to send, just close history
                Task::batch(tasks)
            }
        }
    }

    pub fn record_message(
        &mut self,
        server: &Server,
        message: data::Message,
        labeled_response_context: Option<LabeledResponseContext>,
        buffer_config: &config::Buffer,
    ) -> Task<Message> {
        let tasks = self.history.record_message(
            server,
            message,
            labeled_response_context,
            buffer_config,
        );

        Task::batch(
            tasks
                .into_iter()
                .map(|task| Task::perform(task, Message::History)),
        )
    }

    pub fn resend_message(
        &mut self,
        clients: &mut client::Map,
        kind: history::Kind,
        message: data::Message,
        config: &Config,
    ) -> Task<Message> {
        if let Some(buffer) = data::Buffer::from(kind).upstream()
            && let Some(user) = clients
                .nickname(buffer.server())
                .map(|nick| User::from(nick.to_owned()))
            && let Some(command) = message.command
        {
            let multiline = if let Some((target, text, batch_kind)) =
                match &command {
                    data::command::Irc::Msg(target, text) => {
                        Some((target, text, MultilineBatchKind::PRIVMSG))
                    }
                    data::command::Irc::Notice(target, text) => {
                        Some((target, text, MultilineBatchKind::NOTICE))
                    }
                    _ => None,
                } {
                if text.lines().count() > 1 {
                    true
                } else if let Some(multiline_limits) =
                    clients.get_multiline_limits(buffer.server())
                {
                    let multiline_concat_bytes = multiline_limits.concat_bytes(
                        clients.get_relay_bytes(buffer.server()),
                        batch_kind,
                        target,
                    );

                    multiline_concat_lines(multiline_concat_bytes, text).len()
                        > 1
                } else {
                    false
                }
            } else {
                false
            };
            let reply_id = message.reply_to.as_ref();

            let (user, channel_users) =
                if let buffer::Upstream::Channel(server, channel) = &buffer {
                    (
                        clients
                            .resolve_user_attributes(server, channel, &user)
                            .cloned()
                            .unwrap_or(user),
                        clients.get_channel_users(server, channel),
                    )
                } else {
                    (user, None)
                };
            let chantypes =
                clients.get_server_chantypes_or_default(buffer.server());
            let statusmsg =
                clients.get_server_statusmsg_or_default(buffer.server());
            let casemapping =
                clients.get_server_casemapping_or_default(buffer.server());
            let supports_echoes =
                clients.get_server_supports_echoes(buffer.server());

            // The command stored with each sent message is expected to
            // correspond to a single message (i.e. it is not a multi-target
            // command; if the original command was multi-target, then it was
            // split into single-target commands).
            if let Some(message) = command
                .messages(
                    user,
                    channel_users,
                    buffer.server(),
                    chantypes,
                    statusmsg,
                    casemapping,
                    supports_echoes,
                    self.history.get_reroute_rules(),
                )
                .and_then(|messages| messages.into_iter().next())
                && let Some(mut encoded) = proto::Command::try_from(command)
                    .ok()
                    .map(proto::Message::from)
                    .map(message::Encoded::from)
            {
                let labeled_response_context = if multiline {
                    clients.send_multiline_batch(
                        buffer,
                        vec![encoded],
                        TokenPriority::User,
                        reply_id,
                    )
                } else {
                    encoded.set_reply_to(reply_id);

                    clients.send(buffer, encoded, TokenPriority::User)
                };

                return Task::batch(
                    self.history
                        .record_input_message(
                            message,
                            labeled_response_context.clone(),
                            buffer.server(),
                            casemapping,
                            config,
                        )
                        .into_iter()
                        .map(Task::future),
                )
                .map(Message::History);
            }
        }

        Task::none()
    }

    pub fn record_reaction(
        &mut self,
        server: &Server,
        reaction: reaction::Context,
        notification_enabled: bool,
        labeled_response_context: Option<LabeledResponseContext>,
    ) -> Task<Message> {
        let future = self.history.record_reaction(
            server,
            reaction,
            notification_enabled,
            labeled_response_context,
        );
        if let Some(f) = future {
            Task::perform(f, Message::History)
        } else {
            Task::none()
        }
    }

    pub fn redact_message(
        &mut self,
        server: &Server,
        redaction: redaction::Context,
        display_redacted: bool,
    ) {
        self.history
            .redact_message(server, redaction, display_redacted);
    }

    pub fn is_focused_and_at_bottom(&self, kind: &history::Kind) -> bool {
        let Some((kind_window, kind_pane)) =
            self.panes.iter().find_map(|(window, _, state)| {
                state
                    .buffer
                    .data()
                    .and_then(history::Kind::from_buffer)
                    .is_some_and(|pane_kind| pane_kind == *kind)
                    .then_some((window, state))
            })
        else {
            return false;
        };

        if kind_pane
            .buffer
            .is_scrolled_to_bottom()
            .is_none_or(|is_scrolled_to_bottom| !is_scrolled_to_bottom)
        {
            return false;
        }

        self.get_focused()
            .is_some_and(|(focused_window, _, focused_pane)| {
                let is_focused_window = kind_window == focused_window;

                let focused_kind = focused_pane
                    .buffer
                    .data()
                    .and_then(history::Kind::from_buffer);
                let is_focused_pane = focused_kind.as_ref() == Some(kind);

                is_focused_window && is_focused_pane
            })
    }

    pub fn is_open_and_at_bottom(&self, kind: &history::Kind) -> bool {
        let Some((kind_window, kind_pane)) =
            self.panes.iter().find_map(|(window, _, state)| {
                state
                    .buffer
                    .data()
                    .and_then(history::Kind::from_buffer)
                    .is_some_and(|pane_kind| pane_kind == *kind)
                    .then_some((window, state))
            })
        else {
            return false;
        };

        if kind_pane
            .buffer
            .is_scrolled_to_bottom()
            .is_none_or(|is_scrolled_to_bottom| !is_scrolled_to_bottom)
        {
            return false;
        }

        self.get_focused()
            .is_some_and(|(focused_window, _, _)| kind_window == focused_window)
    }

    pub fn block_and_record_message(
        &mut self,
        server: &Server,
        casemapping: isupport::CaseMap,
        message: data::Message,
        labeled_response_context: Option<LabeledResponseContext>,
        buffer_config: &config::Buffer,
    ) -> Task<Message> {
        let tasks = self.history.block_and_record_message(
            server,
            casemapping,
            message,
            labeled_response_context,
            buffer_config,
        );

        Task::batch(
            tasks
                .into_iter()
                .map(|task| Task::perform(task, Message::History)),
        )
    }

    pub fn record_log(&mut self, record: data::log::Record) -> Task<Message> {
        if let Some(task) = self.history.record_log(record) {
            Task::perform(task, Message::History)
        } else {
            Task::none()
        }
    }

    pub fn record_highlight(
        &mut self,
        message: data::Message,
    ) -> Task<Message> {
        self.history
            .record_highlight(message)
            .map_or_else(Task::none, |task| {
                Task::perform(task, Message::History)
            })
    }

    pub fn get_oldest_message_reference(
        &self,
        server: &Server,
        target: Target,
        message_reference_types: &[isupport::MessageReferenceType],
    ) -> Option<MessageReference> {
        if let Some(first_can_reference) = self
            .history
            .first_can_reference(server.clone(), target.clone())
        {
            for message_reference_type in message_reference_types {
                match message_reference_type {
                    isupport::MessageReferenceType::MessageId => {
                        if let Some(id) = &first_can_reference.id {
                            return Some(MessageReference::MessageId(
                                id.clone(),
                            ));
                        }
                    }
                    isupport::MessageReferenceType::Timestamp => {
                        return Some(MessageReference::Timestamp(
                            first_can_reference.server_time,
                        ));
                    }
                }
            }
        }

        None
    }

    pub fn request_older_chathistory(
        &self,
        clients: &mut data::client::Map,
        buffer: &data::Buffer,
    ) {
        let Some(upstream) = buffer.upstream() else {
            return;
        };

        let server = upstream.server();

        if clients.get_server_supports_chathistory(server)
            && let Some(target) = upstream.target()
        {
            if clients.get_chathistory_exhausted(server, &target) {
                return;
            }

            let message_reference_types =
                clients.get_server_chathistory_message_reference_types(server);

            let first_can_reference = self.get_oldest_message_reference(
                server,
                target.clone(),
                &message_reference_types,
            );

            let subcommand =
                if let Some(first_can_reference) = first_can_reference {
                    ChatHistorySubcommand::Before(
                        target,
                        first_can_reference,
                        clients.get_server_chathistory_limit(server),
                    )
                } else {
                    ChatHistorySubcommand::Latest(
                        target,
                        first_can_reference,
                        clients.get_server_chathistory_limit(server),
                    )
                };

            clients.send_chathistory_request(
                server,
                subcommand,
                TokenPriority::User,
            );
        }
    }

    pub fn broadcast(
        &mut self,
        server: &Server,
        casemapping: isupport::CaseMap,
        config: &Config,
        server_time: DateTime<Utc>,
        received_with_server_time: bool,
        broadcast: Broadcast,
    ) -> Task<Message> {
        Task::batch(
            self.history
                .broadcast(
                    server,
                    casemapping,
                    broadcast,
                    config,
                    server_time,
                    received_with_server_time,
                )
                .into_iter()
                .map(|task| Task::perform(task, Message::History)),
        )
    }

    pub fn block_message(
        &self,
        message: &mut data::Message,
        kind: &history::Kind,
        server: &Server,
        casemapping: isupport::CaseMap,
        buffer_config: &config::Buffer,
    ) {
        self.history.block_message(
            message,
            kind,
            server,
            casemapping,
            buffer_config,
        );
    }

    pub fn update_read_marker(
        &mut self,
        kind: impl Into<history::Kind> + 'static,
        read_marker: ReadMarker,
    ) -> Task<Message> {
        if let Some(task) = self.history.update_read_marker(kind, read_marker) {
            Task::perform(task, Message::History)
        } else {
            Task::none()
        }
    }

    pub fn update_display_read_marker(
        &mut self,
        kind: impl Into<history::Kind> + 'static,
        read_marker: ReadMarker,
    ) {
        self.history.update_display_read_marker(kind, read_marker);
    }

    pub fn remove_server_icon(
        &mut self,
        clients: &mut client::Map,
        server: &Server,
    ) -> Task<Message> {
        let icon_url = clients.get_icon_url(server);

        self.server_icons
            .remove(server, icon_url)
            .map(Message::ServerIcon)
    }

    pub fn request_server_icon(
        &mut self,
        clients: &mut client::Map,
        server: &Server,
    ) -> Task<Message> {
        let icon_url = clients.get_icon_url(server);

        let http_client = if clients.get_server_proxy_config(server).is_some() {
            clients.get_server_http_client(server)
        } else {
            self.http_client.clone()
        };

        self.server_icons
            .request(server, icon_url, http_client)
            .map(Message::ServerIcon)
    }

    pub fn request_override_server_icons(
        &mut self,
        servers: &server::Map,
    ) -> Task<Message> {
        Task::batch(servers.entries().filter_map(|entry| {
            if entry.config.icon.enabled
                && let Some(override_url) = &entry.config.icon.override_url
            {
                let http_client =
                    if let Some(proxy_config) = &entry.config.proxy {
                        config::proxy::build_client(Some(proxy_config), None)
                            .ok()
                            .map(Arc::from)
                    } else {
                        self.http_client.clone()
                    };

                Some(
                    self.server_icons
                        .request(&entry.server, Some(override_url), http_client)
                        .map(Message::ServerIcon),
                )
            } else {
                None
            }
        }))
    }

    pub fn mark_as_read(
        &mut self,
        kind: history::Kind,
        clients: &mut client::Map,
    ) {
        mark_as_read(kind, &mut self.history, clients, TokenPriority::High);
    }

    pub fn load_metadata_and_request_newer_chathistory(
        &mut self,
        clients: &data::client::Map,
        server: Server,
        target: Target,
        server_time: DateTime<Utc>,
        allow_at: bool,
    ) -> Task<Message> {
        let command = self
            .history
            .load_metadata(server.clone(), target.clone())
            .map_or(Task::none(), |task| Task::perform(task, Message::History));

        if clients.get_server_supports_chathistory(&server) {
            command.chain(Task::done(Message::Client(
                data::client::Message::RequestNewerChatHistory(
                    server,
                    target,
                    server_time,
                    allow_at,
                ),
            )))
        } else {
            command
        }
    }

    pub fn load_chathistory_targets_timestamp(
        &self,
        clients: &data::client::Map,
        server: &Server,
        server_time: DateTime<Utc>,
    ) -> Option<Task<Message>> {
        clients
            .load_chathistory_targets_timestamp(server, server_time)
            .map(|task| Task::perform(task, Message::Client))
    }

    pub fn overwrite_chathistory_targets_timestamp(
        &self,
        clients: &data::client::Map,
        server: &Server,
        timestamp: DateTime<Utc>,
    ) -> Option<Task<Message>> {
        clients
            .overwrite_chathistory_targets_timestamp(server, timestamp)
            .map(|task| Task::perform(task, Message::Client))
    }

    pub fn get_focused(&self) -> Option<(window::Id, pane_grid::Pane, &Pane)> {
        let Focus { window, pane } = self.focus;
        self.panes
            .get(window, pane)
            .map(|state| (window, pane, state))
    }

    fn get_focused_mut(
        &mut self,
    ) -> Option<(window::Id, pane_grid::Pane, &mut Pane)> {
        let Focus { window, pane } = self.focus;
        self.panes
            .get_mut(window, pane)
            .map(|state| (window, pane, state))
    }

    fn get_focused_with_history_mut(
        &mut self,
    ) -> Option<(
        window::Id,
        pane_grid::Pane,
        &mut Pane,
        &mut history::Manager,
    )> {
        let Focus { window, pane } = self.focus;
        self.panes
            .get_mut(window, pane)
            .map(|state| (window, pane, state, &mut self.history))
    }

    pub fn get_unique_queries(&self, server: &Server) -> Vec<&target::Query> {
        self.history.get_unique_queries(server)
    }

    pub fn add_to_sidebar(&mut self, server: Server, query: target::Query) {
        let kind = history::Kind::Query(server, query);

        self.history.open(kind);
    }

    pub fn refocus_pane(&mut self) -> Task<Message> {
        let Focus { window, pane } = self.focus;

        return self
            .panes
            .iter()
            .find_map(|(w, p, state)| {
                (w == window && p == pane).then(|| {
                    state.buffer.focus().map(move |message| {
                        Message::Pane(
                            window,
                            pane::Message::Buffer(pane, message),
                        )
                    })
                })
            })
            .unwrap_or_else(Task::none);
    }

    fn focus_pane(
        &mut self,
        window: window::Id,
        pane: pane_grid::Pane,
    ) -> Task<Message> {
        if (self.focus != Focus { window, pane })
            || self.focus_history.is_empty()
        {
            self.focus = Focus { window, pane };

            self.last_changed = Some(Instant::now());

            if window == self.main_window() {
                self.focus_history.push_front(pane);

                self.focus_history.truncate(FOCUS_HISTORY_LEN);

                if self.is_pane_maximized() {
                    self.panes.main.restore();
                    self.panes.main.maximize(pane);
                }
            }
        }

        self.refocus_pane()
    }

    fn focus_first_pane(&mut self, window: window::Id) -> Task<Message> {
        let pane = self
            .panes
            .iter()
            .find_map(|(w, pane, _)| (w == window).then_some(pane));

        pane.map_or(Task::none(), |pane| self.focus_pane(window, pane))
    }

    pub fn focus_window_pane(&mut self, window: window::Id) -> Task<Message> {
        if self.focus.window == window {
            Task::none()
        } else if let Some(pane) = self.focus_history.front()
            && window == self.main_window()
        {
            self.focus_pane(window, *pane)
        } else {
            self.focus_first_pane(window)
        }
    }

    pub fn focus_window(&mut self, window: window::Id) -> Task<Message> {
        let task = self.focus_window_pane(window);

        window::gain_focus(window).chain(task)
    }

    fn maximize_pane(&mut self) {
        if self.is_pane_maximized() {
            self.panes.main.restore();
        } else if self.focus.window == self.main_window() {
            self.panes.main.maximize(self.focus.pane);
        }
    }

    fn is_pane_maximized(&self) -> bool {
        self.panes.main.maximized().is_some()
    }

    fn new_pane(&mut self, axis: pane_grid::Axis) -> Task<Message> {
        if self.focus.window == self.main_window() {
            // If there is any focused pane on main window, split it
            return self.split_pane(axis);
        } else {
            // If there is no focused pane, split the last pane or create a new empty grid
            let pane =
                self.panes.main.iter().last().map(|(pane, _)| pane).copied();

            if let Some(pane) = pane {
                let result =
                    self.panes.main.split(axis, pane, Pane::new(Buffer::Empty));
                self.last_changed = Some(Instant::now());

                if let Some((pane, _)) = result {
                    return self.focus_pane(self.main_window(), pane);
                }
            } else {
                let (state, pane) =
                    pane_grid::State::new(Pane::new(Buffer::Empty));
                self.panes.main = state;
                self.last_changed = Some(Instant::now());
                return self.focus_pane(self.main_window(), pane);
            }
        }

        Task::none()
    }

    fn split_pane(&mut self, axis: pane_grid::Axis) -> Task<Message> {
        if self.focus.window == self.main_window() {
            let result = self.panes.main.split(
                axis,
                self.focus.pane,
                Pane::new(Buffer::Empty),
            );
            self.last_changed = Some(Instant::now());
            if let Some((pane, _)) = result {
                return self.focus_pane(self.main_window(), pane);
            }
        }

        Task::none()
    }

    fn reset_pane(
        &mut self,
        window: window::Id,
        pane: pane_grid::Pane,
    ) -> Task<Message> {
        if let Some(state) = self.panes.get_mut(window, pane) {
            state.buffer.reset();
        }

        Task::none()
    }

    fn close_pane(
        &mut self,
        clients: &mut data::client::Map,
        config: &Config,
        window: window::Id,
        pane: pane_grid::Pane,
    ) -> Task<Message> {
        let mut tasks = vec![];

        if let Some(state) = self.panes.get(window, pane) {
            mark_as_read_on_buffer_close(
                &state.buffer,
                &mut self.history,
                clients,
                config,
            );

            if config.buffer.close.query.close()
                && let Some(history::Kind::Query(server, nick)) =
                    state.buffer.data().and_then(history::Kind::from_buffer)
            {
                tasks.push(
                    self.history
                        .close(history::Kind::Query(server, nick), clients)
                        .map_or_else(Task::none, |task| {
                            Task::perform(task, Message::History)
                        }),
                );
            }
        }

        self.last_changed = Some(Instant::now());

        if window == self.main_window() {
            self.focus_history.retain(|p| *p != pane);

            if let Some((_, sibling)) = self.panes.main.close(pane) {
                if (Focus { window, pane } == self.focus) {
                    tasks.push(self.focus_pane(self.main_window(), sibling));
                    return Task::batch(tasks);
                }
            } else if let Some(pane) = self.panes.main.get_mut(pane) {
                pane.buffer = Buffer::Empty;
            }
        } else if self.panes.popout.remove(&window).is_some() {
            if self.command_bar_window == Some(window) {
                self.close_command_bar();
            }

            tasks.push(
                window::close(window)
                    .chain(self.focus_window(self.main_window())),
            );
            return Task::batch(tasks);
        }

        Task::batch(tasks)
    }

    fn popout_pane(
        &mut self,
        clients: &mut data::client::Map,
        config: &Config,
    ) -> Task<Message> {
        let Focus { pane, .. } = self.focus;

        self.focus_history.retain(|p| *p != pane);

        if let Some((pane, _)) = self.panes.main.close(pane)
            && let Some(buffer) = pane.buffer.data()
        {
            return self.open_buffer(
                buffer,
                BufferAction::NewWindow,
                clients,
                config,
            );
        }

        Task::none()
    }

    fn merge_pane(
        &mut self,
        clients: &mut data::client::Map,
        config: &Config,
    ) -> Task<Message> {
        let Focus { window, pane } = self.focus;

        if let Some(pane) = self
            .panes
            .popout
            .remove(&window)
            .and_then(|panes| panes.get(pane).cloned())
        {
            let task = match pane.buffer.data() {
                Some(buffer) => self.open_buffer(
                    buffer,
                    BufferAction::NewPane,
                    clients,
                    config,
                ),
                None => self.new_pane(pane_grid::Axis::Horizontal),
            };

            return Task::batch(vec![
                window::close(window),
                window::gain_focus(self.main_window()).chain(task),
            ]);
        }

        Task::none()
    }

    fn swap_pane_with_focus(
        &mut self,
        from_window: window::Id,
        from_pane: pane_grid::Pane,
    ) -> Task<Message> {
        self.last_changed = Some(Instant::now());

        let Focus {
            window: to_window,
            pane: to_pane,
        } = self.focus;

        if from_window == self.main_window() && to_window == self.main_window()
        {
            self.panes.main.swap(from_pane, to_pane);

            self.focus_pane(from_window, from_pane)
        } else {
            if let Some((from_state, to_state)) = self
                .panes
                .get(from_window, from_pane)
                .cloned()
                .zip(self.panes.get(to_window, to_pane).cloned())
            {
                if let Some(state) = self.panes.get_mut(from_window, from_pane)
                {
                    *state = to_state;
                }
                if let Some(state) = self.panes.get_mut(to_window, to_pane) {
                    *state = from_state;
                }
            }

            Task::none()
        }
    }

    pub fn track(
        &mut self,
        clients: Option<&data::client::Map>,
    ) -> Task<Message> {
        let resources = self.panes.resources().collect();

        Task::batch(
            self.history
                .track(resources, clients)
                .into_iter()
                .map(|fut| Task::perform(fut, Message::History))
                .collect::<Vec<_>>(),
        )
    }

    pub fn tick(
        &mut self,
        now: Instant,
        clients: &data::client::Map,
        config: &Config,
    ) -> Task<Message> {
        if !self.has_typing_activity(clients) {
            self.typing_animation = None;
        }

        let history_ticks = Task::batch(
            self.history
                .tick(now.into(), clients)
                .into_iter()
                .map(|task| Task::perform(task, Message::History))
                .collect::<Vec<_>>(),
        );

        let draft_save = config
            .buffer
            .text_input
            .persist
            .then(|| {
                self.history
                    .maybe_save_drafts(now.into())
                    .map(|fut| Task::perform(fut, Message::History))
            })
            .flatten();

        if let Some(last_changed) = self.last_changed
            && now.duration_since(last_changed) >= SAVE_AFTER
        {
            let dashboard = data::Dashboard::from(&*self);

            self.last_changed = None;

            return Task::batch(
                [
                    Task::perform(dashboard.save(), Message::DashboardSaved),
                    history_ticks,
                ]
                .into_iter()
                .chain(draft_save)
                .collect::<Vec<_>>(),
            );
        }

        Task::batch(
            [history_ticks]
                .into_iter()
                .chain(draft_save)
                .collect::<Vec<_>>(),
        )
    }

    pub fn animation_tick(
        &mut self,
        now: Instant,
        clients: &data::client::Map,
    ) -> Task<Message> {
        if self.has_typing_activity(clients) {
            buffer::typing::advance(&mut self.typing_animation, now);
        } else {
            self.typing_animation = None;
        }

        Task::none()
    }

    pub fn toggle_command_bar(
        &mut self,
        servers: &server::Map,
        clients: &client::Map,
        buffers: &[buffer::Upstream],
        version: &Version,
        config: &Config,
        theme: &mut Theme,
    ) -> Task<Message> {
        match self.command_bar_window {
            Some(window) if window == self.focus.window => {
                // Remove theme preview
                *theme = theme.selected();

                self.close_command_bar();
                // Refocus the pane so text input gets refocused
                let Focus { window, pane } = self.focus;
                self.focus_pane(window, pane)
            }
            Some(_) => {
                *theme = theme.selected();

                self.close_command_bar();
                self.open_command_bar(
                    servers, clients, buffers, version, config,
                );

                Task::none()
            }
            None => {
                self.open_command_bar(
                    servers, clients, buffers, version, config,
                );
                Task::none()
            }
        }
    }

    fn open_command_bar(
        &mut self,
        servers: &server::Map,
        clients: &client::Map,
        buffers: &[buffer::Upstream],
        version: &Version,
        config: &Config,
    ) {
        self.command_bar_window = Some(self.focus.window);
        self.command_bar = Some(CommandBar::new(
            servers,
            clients,
            buffers,
            version,
            config,
            self.focus,
            self.buffer_resize_action(),
            self.main_window(),
        ));
    }

    fn close_command_bar(&mut self) {
        self.command_bar = None;
        self.command_bar_window = None;
    }

    fn buffer_resize_action(&self) -> data::buffer::Resize {
        let can_resize_buffer =
            self.focus.window == self.main_window() && self.panes.len() > 1;
        data::buffer::Resize::action(
            can_resize_buffer,
            self.is_pane_maximized(),
        )
    }

    pub fn receive_file_transfer(
        &mut self,
        server: &Server,
        casemapping: isupport::CaseMap,
        request: file_transfer::ReceiveRequest,
        config: &Config,
    ) -> Option<Task<Message>> {
        if !config.file_transfer.enabled {
            log::info!(
                "file transfer request from {} ignored",
                request.from.formatted(UsernameFormat::Full)
            );

            return None;
        }

        let event = self.file_transfers.receive(request.clone(), config)?;

        let request_attention_window = self
            .find_window_with_file_transfers()
            .unwrap_or(self.main_window());

        let request_attention = self.notifications.notify(
            &config.notifications,
            &Notification::FileTransferRequest {
                nick: request.from.nickname().to_owned(),
                casemapping,
                filename: match event {
                    file_transfer::manager::Event::NewTransfer(
                        ref transfer,
                        _,
                    ) => transfer.filename.clone(),
                },
            },
            server,
            request_attention_window,
        );

        let query = target::Query::from(request.from);

        let task = self.handle_file_transfer_event(
            server,
            &query,
            event,
            &config.buffer,
        );

        if let Some(request_attention) = request_attention {
            Some(Task::batch(vec![task, request_attention]))
        } else {
            Some(task)
        }
    }

    pub fn handle_file_transfer_event(
        &mut self,
        server: &Server,
        query: &target::Query,
        event: file_transfer::manager::Event,
        buffer_config: &config::Buffer,
    ) -> Task<Message> {
        let mut tasks = vec![];

        match event {
            file_transfer::manager::Event::NewTransfer(transfer, task) => {
                match transfer.direction {
                    file_transfer::Direction::Received => {
                        tasks.push(self.record_message(
                            server,
                            data::Message::file_transfer_request_received(
                                &transfer.remote_user,
                                query,
                                &transfer.filename,
                            ),
                            None,
                            buffer_config,
                        ));
                    }
                    file_transfer::Direction::Sent => {
                        tasks.push(self.record_message(
                            server,
                            data::Message::file_transfer_request_sent(
                                &transfer.remote_user,
                                query,
                                &transfer.filename,
                            ),
                            None,
                            buffer_config,
                        ));
                    }
                }

                tasks.push(Task::run(task, Message::FileTransfer));
            }
        }

        Task::batch(tasks)
    }

    fn from_data(
        data: data::Dashboard,
        config: &Config,
        main_window: &Window,
    ) -> (Self, Task<Message>) {
        use pane_grid::Configuration;

        fn configuration(
            pane: data::Pane,
            clients: &data::client::Map,
            history: &history::Manager,
            config: &Config,
        ) -> Configuration<Pane> {
            match pane {
                data::Pane::Split { axis, ratio, a, b } => {
                    Configuration::Split {
                        axis: match axis {
                            data::pane::Axis::Horizontal => {
                                pane_grid::Axis::Horizontal
                            }
                            data::pane::Axis::Vertical => {
                                pane_grid::Axis::Vertical
                            }
                        },
                        ratio,
                        a: Box::new(configuration(
                            *a, clients, history, config,
                        )),
                        b: Box::new(configuration(
                            *b, clients, history, config,
                        )),
                    }
                }
                data::Pane::Buffer { buffer } => {
                    Configuration::Pane(Pane::new(Buffer::from_data(
                        buffer,
                        clients,
                        history,
                        Size::default(),
                        config,
                    )))
                }
                data::Pane::Empty => {
                    Configuration::Pane(Pane::new(Buffer::empty()))
                }
            }
        }

        let mut history = history::Manager::default();
        if config.buffer.text_input.persist {
            history.preload_drafts(data::input::load_drafts_sync());
        }

        let panes = Panes {
            main_window: main_window.id,
            main: pane_grid::State::with_configuration(configuration(
                data.pane,
                &data::client::Map::default(),
                &history,
                config,
            )),
            popout: HashMap::new(),
        };

        let focus = panes
            .iter()
            // This should never fail
            .find_map(|(window, pane, state)| {
                (state.buffer.data() == data.focus_buffer)
                    .then_some(Focus { window, pane })
            })
            // But if somehow it does, we just focus the "first" pane from the main window
            .unwrap_or_else(|| {
                let (_, pane) = pane_grid::State::new(());

                Focus {
                    window: main_window.id,
                    pane,
                }
            });

        let (sidebar, sidebar_task) = Sidebar::new();

        let mut dashboard = Self {
            panes,
            focus,
            focus_history: VecDeque::from([focus.pane]),
            side_menu: sidebar,
            history,
            last_changed: None,
            command_bar: None,
            command_bar_window: None,
            file_transfers: file_transfer::Manager::default(),
            theme_editor: None,
            notifications: notification::Notifications::new(config),
            previews: preview::Collection::default(),
            previews_cache: Arc::new(preview_cache(&config.preview)),
            server_icons: server_icon::Manager::default(),
            typing_animation: None,
            http_client: http_client_from_config(config).map(Arc::new),
            buffer_settings: data.buffer_settings.clone(),
            filehost: filehost::Manager::new(),
        };

        let mut tasks = vec![sidebar_task.map(Message::Sidebar)];

        for pane in data.popout_panes {
            // Popouts are only a single pane
            let Configuration::Pane(pane) = configuration(
                pane,
                &data::client::Map::default(),
                &dashboard.history,
                config,
            ) else {
                continue;
            };

            if let Some(buffer) = pane.buffer.data() {
                tasks.push(dashboard.open_buffer(
                    buffer,
                    BufferAction::NewWindow,
                    &mut data::client::Map::default(),
                    config,
                ));
            }
        }

        let tasks = Task::batch(tasks)
            .chain(dashboard.focus_pane(focus.window, focus.pane));

        (dashboard, tasks)
    }

    pub fn history(&self) -> &history::Manager {
        &self.history
    }

    pub fn get_filters(&mut self) -> &mut Vec<Filter> {
        self.history.get_filters()
    }

    pub fn handle_file_drop(&mut self, path: PathBuf) -> Task<Message> {
        let Focus { window, pane } = self.focus;

        let Some(pane_state) = self.panes.get(window, pane) else {
            return Task::none();
        };

        let msg = match &pane_state.buffer {
            Buffer::Channel(_) => Some(buffer::Message::Channel(
                buffer::channel::Message::FilesDropped(vec![path]),
            )),
            Buffer::Query(_) => Some(buffer::Message::Query(
                buffer::query::Message::FilesDropped(vec![path]),
            )),
            Buffer::Server(_) => Some(buffer::Message::Server(
                buffer::server::Message::FilesDropped(vec![path]),
            )),
            _ => None,
        };

        if let Some(msg) = msg {
            Task::done(Message::Pane(window, pane::Message::Buffer(pane, msg)))
        } else {
            Task::none()
        }
    }

    pub fn get_reroute_rules(&self) -> &RerouteRules {
        self.history.get_reroute_rules()
    }

    pub fn get_reroute_rules_mut(&mut self) -> &mut RerouteRules {
        self.history.get_reroute_rules_mut()
    }

    pub fn handle_window_event(
        &mut self,
        id: window::Id,
        event: window::Event,
        theme: &mut Theme,
        config: &Config,
    ) -> Task<Message> {
        if self.panes.popout.contains_key(&id) {
            match event {
                window::Event::CloseRequested => {
                    if self.command_bar_window == Some(id) {
                        *theme = theme.selected();
                        self.close_command_bar();
                    }

                    self.panes.popout.remove(&id);
                    return window::close(id);
                }
                window::Event::Focused => {
                    return self.focus_window_pane(id);
                }
                window::Event::Moved(_)
                | window::Event::Resized(_)
                | window::Event::Unfocused
                | window::Event::Opened { .. } => {}
                window::Event::FileHovered => {
                    self.filehost.file_being_hovered = true;
                }
                window::Event::FilesHoveredLeft => {
                    self.filehost.file_being_hovered = false;
                }
                window::Event::FileDropped(path) => {
                    self.filehost.file_being_hovered = false;
                    if config.filehost.file_drop() {
                        return self.handle_file_drop(path);
                    }
                }
            }
        } else if self.theme_editor.as_ref().is_some_and(|e| e.window == id) {
            match event {
                window::Event::CloseRequested => {
                    if let Some(editor) = self.theme_editor.take() {
                        *theme = theme.selected();
                        return window::close(editor.window);
                    }
                }
                window::Event::Moved(_)
                | window::Event::Resized(_)
                | window::Event::Focused
                | window::Event::Unfocused
                | window::Event::Opened { .. }
                | window::Event::FileHovered
                | window::Event::FilesHoveredLeft
                | window::Event::FileDropped(_) => {}
            }
        }

        Task::none()
    }

    pub fn preview_theme_in_editor(
        &mut self,
        styles: theme::Styles,
        main_window: &Window,
        theme: &mut Theme,
        config: &Config,
    ) -> Task<Message> {
        *theme = theme.preview(data::Theme::new("Custom Theme".into(), styles));

        if let Some(editor) = &self.theme_editor {
            window::gain_focus(editor.window)
        } else {
            let (editor, task) = ThemeEditor::open(main_window, config);

            self.theme_editor = Some(editor);

            task.then(|_| Task::none())
        }
    }

    pub fn exit(
        &mut self,
        clients: &mut data::client::Map,
        config: &Config,
    ) -> Task<Message> {
        if config.buffer.mark_as_read.on_application_exit {
            self.history.kinds()
        } else {
            self.panes
                .iter()
                .filter_map(|(_, _, state)| {
                    if config
                        .buffer
                        .mark_as_read
                        .on_buffer_close
                        .mark_as_read(state.buffer.is_scrolled_to_bottom())
                    {
                        state.buffer.data().and_then(history::Kind::from_buffer)
                    } else {
                        None
                    }
                })
                .collect()
        }
        .into_iter()
        .for_each(|kind| {
            mark_as_read(kind, &mut self.history, clients, TokenPriority::High);
        });

        let history = self.history.exit(clients);
        let last_changed = self.last_changed.take();
        let dashboard = data::Dashboard::from(&*self);

        Task::perform(
            async move {
                if last_changed.is_some() {
                    match dashboard.save().await {
                        Ok(()) => {
                            log::debug!("dashboard saved");
                        }
                        Err(error) => {
                            log::warn!("error saving dashboard: {error}");
                        }
                    }
                }

                history.await
            },
            Message::History,
        )
    }

    fn open_channel(
        &mut self,
        server: Server,
        channel: target::Channel,
        clients: &mut data::client::Map,
        buffer_action: BufferAction,
        config: &Config,
    ) -> Task<Message> {
        let buffer = buffer::Upstream::Channel(server.clone(), channel.clone());

        // Need to join channel
        if !clients.contains_channel(&server, &channel) {
            clients.join(&server, slice::from_ref(&channel));
        }

        self.open_buffer(
            data::Buffer::Upstream(buffer),
            buffer_action,
            clients,
            config,
        )
    }

    pub fn open_target(
        &mut self,
        server: Server,
        target: Target,
        clients: &mut data::client::Map,
        buffer_action: BufferAction,
        config: &Config,
    ) -> Task<Message> {
        match target {
            Target::Channel(channel) => self.open_channel(
                server,
                channel,
                clients,
                buffer_action,
                config,
            ),
            Target::Query(query) => {
                let buffer = data::Buffer::Upstream(buffer::Upstream::Query(
                    server, query,
                ));

                self.open_buffer(buffer.clone(), buffer_action, clients, config)
            }
        }
    }

    pub fn find_window_with_file_transfers(&mut self) -> Option<window::Id> {
        self.panes.iter().find_map(|(window_id, _, state)| {
            matches!(state.buffer, Buffer::FileTransfers(_))
                .then_some(window_id)
        })
    }

    pub fn has_typing_activity(&self, clients: &client::Map) -> bool {
        self.panes
            .iter()
            .any(|(_, _, pane)| pane.buffer.has_typing_activity(clients))
    }

    pub fn has_typing_activity_in_focused_window(
        &self,
        clients: &client::Map,
        focused_window: Option<window::Id>,
    ) -> bool {
        focused_window.is_some_and(|focused_window| {
            self.panes.iter().any(|(window, _, pane)| {
                pane.buffer.has_typing_activity(clients)
                    && focused_window == window
            })
        })
    }

    pub fn find_window_with_history(
        &mut self,
        kind: &history::Kind,
    ) -> Option<window::Id> {
        self.panes.iter().find_map(|(window_id, _, state)| {
            state
                .buffer
                .data()
                .and_then(history::Kind::from_buffer)
                .is_some_and(|pane_kind| pane_kind == *kind)
                .then_some(window_id)
        })
    }

    pub fn find_window_with_server(
        &mut self,
        server: &Server,
    ) -> Option<window::Id> {
        self.panes.iter().find_map(|(window_id, _, state)| {
            state
                .buffer
                .server()
                .is_some_and(|pane_server| pane_server == *server)
                .then_some(window_id)
        })
    }

    pub fn process_server_inputs_completion_and_notice(
        &mut self,
        server: &Server,
        clients: &client::Map,
        config: &Config,
    ) {
        for buffer in self.panes.iter_mut().filter_map(|(_, _, state)| {
            state
                .buffer
                .server()
                .is_some_and(|pane_server| pane_server == *server)
                .then_some(&mut state.buffer)
        }) {
            buffer.process_input_completion_and_notice(
                clients,
                &self.history,
                config,
            );
        }
    }

    fn main_window(&self) -> window::Id {
        self.panes.main_window
    }

    fn visible_urls_with_preview_clients(
        &self,
        clients: &client::Map,
    ) -> HashMap<url::Url, Arc<reqwest::Client>> {
        let pane_map = |pane: &Pane| -> Vec<(url::Url, Arc<reqwest::Client>)> {
            let preview_client = if let Some(server) = pane.buffer.server()
                && clients.get_server_proxy_config(&server).is_some()
            {
                clients.get_server_http_client(&server)
            } else {
                self.http_client.clone()
            };

            if let Some(preview_client) = preview_client {
                pane.visible_urls()
                    .into_iter()
                    .map(move |url| (url.clone(), preview_client.clone()))
                    .collect()
            } else {
                vec![]
            }
        };

        self.panes
            .main
            .panes
            .values()
            .flat_map(pane_map)
            .chain(
                self.panes
                    .popout
                    .values()
                    .flat_map(|state| state.panes.values().flat_map(pane_map)),
            )
            .collect()
    }

    pub fn focused_buffer_name(&self, w: window::Id) -> Option<String> {
        if w == self.main_window() {
            self.focus_history
                .front()
                .and_then(|pane| self.panes.get(w, *pane))
                .filter(|pane| !matches!(pane.buffer, Buffer::Empty))
                .map(|pane| pane.buffer.to_string())
        } else {
            self.panes.iter().find_map(|(win, _, pane)| {
                (win == w && !matches!(pane.buffer, Buffer::Empty))
                    .then_some(pane.buffer.to_string())
            })
        }
    }

    pub fn open_pane_server_queries(
        &self,
        server: &Server,
    ) -> Vec<&target::Query> {
        self.panes
            .iter()
            .filter_map(|(_, _, pane)| match pane.buffer.upstream() {
                Some(buffer::Upstream::Query(buffer_server, query))
                    if buffer_server == server =>
                {
                    Some(query)
                }
                _ => None,
            })
            .collect()
    }
}

fn mark_server_as_read(
    server: Server,
    history: &mut history::Manager,
    clients: &mut data::client::Map,
) {
    for kind in history.server_kinds(server) {
        mark_as_read(kind, history, clients, TokenPriority::User);
    }
}

fn mark_as_read(
    kind: history::Kind,
    history: &mut history::Manager,
    clients: &mut data::client::Map,
    priority: TokenPriority,
) {
    let read_marker = history.mark_as_read(&kind);

    if let (Some(server), Some(target), Some(read_marker)) =
        (kind.server(), kind.target(), read_marker)
    {
        clients.send_markread(server, target, read_marker, priority);
    }
}

fn mark_as_read_on_buffer_close(
    buffer: &Buffer,
    history: &mut history::Manager,
    clients: &mut data::client::Map,
    config: &Config,
) {
    if config
        .buffer
        .mark_as_read
        .on_buffer_close
        .mark_as_read(buffer.is_scrolled_to_bottom())
        && let Some(kind) = buffer.data().and_then(history::Kind::from_buffer)
    {
        mark_as_read(kind, history, clients, TokenPriority::High);
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Focus {
    pub window: window::Id,
    pub pane: pane_grid::Pane,
}

impl<'a> From<&'a Dashboard> for data::Dashboard {
    fn from(dashboard: &'a Dashboard) -> Self {
        use pane_grid::Node;

        fn from_layout(
            panes: &pane_grid::State<Pane>,
            node: pane_grid::Node,
        ) -> data::Pane {
            match node {
                Node::Split {
                    axis, ratio, a, b, ..
                } => data::Pane::Split {
                    axis: match axis {
                        pane_grid::Axis::Horizontal => {
                            data::pane::Axis::Horizontal
                        }
                        pane_grid::Axis::Vertical => data::pane::Axis::Vertical,
                    },
                    ratio,
                    a: Box::new(from_layout(panes, *a)),
                    b: Box::new(from_layout(panes, *b)),
                },
                Node::Pane(pane) => panes
                    .get(pane)
                    .cloned()
                    .map_or(data::Pane::Empty, data::Pane::from),
            }
        }

        let layout = dashboard.panes.main.layout().clone();
        let focus = dashboard.focus;

        data::Dashboard {
            pane: from_layout(&dashboard.panes.main, layout),
            popout_panes: dashboard
                .panes
                .popout
                .values()
                .map(|state| from_layout(state, state.layout().clone()))
                .collect(),
            buffer_settings: dashboard.buffer_settings.clone(),
            focus_buffer: dashboard.panes.iter().find_map(|(w, p, state)| {
                (w == focus.window && p == focus.pane)
                    .then_some(state.buffer.data())
                    .flatten()
            }),
        }
    }
}

#[derive(Clone)]
pub struct Panes {
    main_window: window::Id,
    main: pane_grid::State<Pane>,
    popout: HashMap<window::Id, pane_grid::State<Pane>>,
}

impl Panes {
    fn len(&self) -> usize {
        self.main.panes.len() + self.popout.len()
    }

    fn get(&self, window: window::Id, pane: pane_grid::Pane) -> Option<&Pane> {
        if self.main_window == window {
            self.main.get(pane)
        } else {
            self.popout.get(&window).and_then(|panes| panes.get(pane))
        }
    }

    fn get_mut(
        &mut self,
        window: window::Id,
        pane: pane_grid::Pane,
    ) -> Option<&mut Pane> {
        if self.main_window == window {
            self.main.get_mut(pane)
        } else {
            self.popout
                .get_mut(&window)
                .and_then(|panes| panes.get_mut(pane))
        }
    }

    fn get_mut_by_buffer(
        &mut self,
        buffer: &data::Buffer,
    ) -> Option<(window::Id, pane_grid::Pane, &mut Pane)> {
        self.iter_mut().find(|(_, _, state)| {
            state.buffer.data().is_some_and(|b| b == *buffer)
        })
    }

    fn iter(
        &self,
    ) -> impl Iterator<Item = (window::Id, pane_grid::Pane, &Pane)> {
        self.main
            .iter()
            .map(move |(pane, state)| (self.main_window, *pane, state))
            .chain(self.popout.iter().flat_map(|(window_id, panes)| {
                panes.iter().map(|(pane, state)| (*window_id, *pane, state))
            }))
    }

    fn iter_mut(
        &mut self,
    ) -> impl Iterator<Item = (window::Id, pane_grid::Pane, &mut Pane)> {
        let main_window = self.main_window;

        self.main
            .iter_mut()
            .map(move |(pane, state)| (main_window, *pane, state))
            .chain(self.popout.iter_mut().flat_map(|(window_id, panes)| {
                panes
                    .iter_mut()
                    .map(|(pane, state)| (*window_id, *pane, state))
            }))
    }

    fn resources(&self) -> impl Iterator<Item = data::history::Resource> + '_ {
        self.main.panes.values().filter_map(Pane::resource).chain(
            self.popout.values().flat_map(|state| {
                state.panes.values().filter_map(Pane::resource)
            }),
        )
    }
}

fn all_buffers(
    clients: &client::Map,
    history: &history::Manager,
) -> Vec<buffer::Upstream> {
    clients
        .connected_servers()
        .flat_map(|server| {
            std::iter::once(buffer::Upstream::Server(server.clone()))
                .chain(clients.get_channels(server).map(|channel| {
                    buffer::Upstream::Channel(server.clone(), channel.clone())
                }))
                .chain(history.get_unique_queries(server).into_iter().map(
                    |nick| {
                        buffer::Upstream::Query(server.clone(), nick.clone())
                    },
                ))
        })
        .collect()
}

fn all_buffers_with_has_unread(
    clients: &client::Map,
    history: &history::Manager,
) -> Vec<(buffer::Upstream, bool)> {
    clients
        .connected_servers()
        .flat_map(|server| {
            std::iter::once((
                buffer::Upstream::Server(server.clone()),
                history.has_unread(&history::Kind::Server(server.clone())),
            ))
            .chain(clients.get_channels(server).map(|channel| {
                (
                    buffer::Upstream::Channel(server.clone(), channel.clone()),
                    history.has_unread(&history::Kind::Channel(
                        server.clone(),
                        channel.clone(),
                    )),
                )
            }))
            .chain(
                history.get_unique_queries(server).into_iter().map(|nick| {
                    (
                        buffer::Upstream::Query(server.clone(), nick.clone()),
                        history.has_unread(&history::Kind::Query(
                            server.clone(),
                            nick.clone(),
                        )),
                    )
                }),
            )
        })
        .collect()
}

fn open_buffers(dashboard: &Dashboard) -> Vec<buffer::Upstream> {
    dashboard
        .panes
        .iter()
        .filter_map(|(_, _, pane)| pane.buffer.upstream())
        .cloned()
        .collect()
}

fn closed_buffers(
    dashboard: &Dashboard,
    clients: &client::Map,
) -> Vec<buffer::Upstream> {
    let open_buffers = open_buffers(dashboard);

    all_buffers(clients, &dashboard.history)
        .into_iter()
        .filter(|buffer| !open_buffers.contains(buffer))
        .collect()
}

fn cycle_next_buffer(
    current: Option<&buffer::Upstream>,
    mut all: Vec<buffer::Upstream>,
    opened: &[buffer::Upstream],
) -> Option<buffer::Upstream> {
    all.retain(|buffer| Some(buffer) == current || !opened.contains(buffer));

    let next = || {
        let buffer = current?;
        let index = all.iter().position(|b| b == buffer)?;
        all.get(index + 1)
    };

    next().or_else(|| all.first()).cloned()
}

fn cycle_previous_buffer(
    current: Option<&buffer::Upstream>,
    mut all: Vec<buffer::Upstream>,
    opened: &[buffer::Upstream],
) -> Option<buffer::Upstream> {
    all.retain(|buffer| Some(buffer) == current || !opened.contains(buffer));

    let previous = || {
        let buffer = current?;
        let index = all.iter().position(|b| b == buffer).filter(|i| *i > 0)?;

        all.get(index - 1)
    };

    previous().or_else(|| all.last()).cloned()
}

fn cycle_next_unread_buffer(
    current: Option<&buffer::Upstream>,
    mut all: Vec<(buffer::Upstream, bool)>,
    opened: &[buffer::Upstream],
) -> Option<buffer::Upstream> {
    all.retain(|(buffer, _)| {
        Some(buffer) == current || !opened.contains(buffer)
    });

    let index = current
        .and_then(|buffer| all.iter().position(|(b, _)| b == buffer))
        .unwrap_or(all.len());

    let next_after = || {
        all.iter()
            .skip(index + 1)
            .find_map(|(b, has_unread)| has_unread.then_some(b))
    };

    let next_before = || {
        all.iter()
            .take(index)
            .find_map(|(b, has_unread)| has_unread.then_some(b))
    };

    next_after().or_else(|| next_before().or(None)).cloned()
}

fn cycle_previous_unread_buffer(
    current: Option<&buffer::Upstream>,
    mut all: Vec<(buffer::Upstream, bool)>,
    opened: &[buffer::Upstream],
) -> Option<buffer::Upstream> {
    all.retain(|(buffer, _)| {
        Some(buffer) == current || !opened.contains(buffer)
    });

    let index = current
        .and_then(|buffer| all.iter().rev().position(|(b, _)| b == buffer))
        .unwrap_or(all.len());

    let previous_before = || {
        all.iter()
            .rev()
            .skip(index + 1)
            .find_map(|(b, has_unread)| has_unread.then_some(b))
    };

    let previous_after = || {
        all.iter()
            .rev()
            .take(index)
            .find_map(|(b, has_unread)| has_unread.then_some(b))
    };

    previous_before()
        .or_else(|| previous_after().or(None))
        .cloned()
}

fn connect_server(
    server: Server,
    controllers: &mut stream::Map,
    servers: &server::Map,
) {
    if let Some(parent) = server.parent() {
        controllers.connect(&parent);
    }

    controllers.connect(&server);

    for bouncer_network in servers.get_bouncer_networks(&server) {
        controllers.connect(bouncer_network);
    }
}

fn http_client_from_config(config: &Config) -> Option<reqwest::Client> {
    let http_client = config::proxy::build_client(config.proxy.as_ref(), None);

    match http_client {
        Ok(http_client) => Some(http_client),
        Err(error) => {
            log::warn!(
                "Unable to build HTTP client, preview fetching and file upload disabled by default: {error}"
            );

            None
        }
    }
}

fn preview_cache(config: &config::Preview) -> cache::FileCache {
    let root = environment::cache_dir().join("previews");

    cache::FileCache::new(
        root,
        config.request.image_cache.max_size_bytes(),
        config.request.image_cache.trim_interval,
    )
}
